• 七种方法增强代码可扩展性(多图详解)



    1 六大原则

    在设计模式中有六大设计原则:

    单一职责原则:一个类只做一件事

    里式替换原则:子类可以扩展父类

    依赖倒置原则:面向接口编程

    接口隔离原则:高内聚低耦合

    迪米特法则:最少知道原则

    开闭原则:关闭修改,开放新增

    我认为在这六个原则中开闭原则最为重要,开闭原则是可扩展性的重要基石。

    第一个原因是需求变化时应该通过新增而不是修改已有代码实现,这样保证了代码稳定性,避免牵一发而动全身。

    第二个原因是可以事先定义代码框架,扩展也是根据框架扩展,体现了用抽象构建框架,用实现扩展细节,保证了稳定性也保证了灵活性。

    第三个原因是其它五个原则虽然侧重点各有不同,但是都可以包含于开闭原则。

    第四个原因是标准二十三种设计模式最终都是在遵循开闭原则。

    既然开闭原则如此重要,我们应该怎么在系统设计时遵循开闭原则呢?


    2 数据库维度

    2.1 设计类型字段

    数据表设计时新增两列:biz_type、biz_sub_type,这两个列即使目前用不上,但是后续扩展可以派上大用场,可以先设置一个默认值。

    biz_type可以指业务类型,业务渠道、租户等等,归根结底是用来隔离大业务类型,假设后续业务新增了一个大业务类型,那么可以通过这个字段隔离。

    biz_sub_type可以在大业务类型中细分小业务,使得业务更加细化,灵活性更好。


    2.2 设计扩展字段

    数据表设计时新增三列:extend1、extend2、extend3,可以先设置为空。扩展字段存储JSON类型信息,存放一些扩展信息、附加信息、松散信息或者是之前未预估到的信息,保证了灵活性。

    之所以设置三个扩展字段是为了增加隔离性,例如extend1存放订单扩展,extend2存放商品扩展,extend3存放营销扩展。


    2.3 设计业务二进制字段

    2.3.1 需求背景

    数据表设计时新增业务二进制,这个字段可以很大程度上扩展业务表意能力。假设在系统中用户一共有三种角色:普通用户、管理员、超级管理员,现在需要设计一张用户角色表记录这类信息。我们不难设计出如下方案:


    id name super admin normal
    101 用户一 1 0 0
    102 用户二 0 1 0
    103 用户三 0 0 1
    104 用户四 1 1 1

    观察上表不难得出,用户一具有超级管理员角色,用户二具有管理员角色,用户三具有普通用户角色,用户四同时具有三种角色。如果此时新增加一种角色呢?那么新增一个字段即可。


    2.3.2 发现问题

    按照上述一个字段表示一种角色进行表设计功能上是没有问题的,优点是容易理解结构清晰,但是我们想一想有没有什么问题?笔者遇到过如下问题:在复杂业务环境一份数据可能会使用在不同的场景,例如上述数据存储在MySQL数据库,这一份数据还会被用在如下场景:

    检索数据需要同步一份到ES

    业务方使用此表通过Flink计算业务指标

    业务方订阅此表Binlog消息进行业务处理

    如果表结构发生变化,数据源之间就要重新进行对接,业务方也要进行代码修改,这样开发成本比较非常高。有没有办法避免此类问题?


    2.3.3 解决方案

    我们可以使用位图法,这样同一个字段可以表示多个业务含义。首先设计如下数据表,userFlag字段暂时不填。

    id name user_flag
    101 用户一 暂时不填
    102 用户二 暂时不填
    103 用户三 暂时不填
    104 用户四 暂时不填

    设计位图每一个bit表示一种角色:


    00 初始位图.jpg


    使用位图法表示如下数据表:


    id name super admin normal
    101 用户一 1 0 0
    102 用户二 0 1 0
    103 用户三 0 0 1
    104 用户四 1 1 1

    用户一位图如下其十进制数值等于4:


    01 用户一.jpg


    用户二位图如下其十进制数值等于2:


    02 用户二.jpg


    用户三位图如下其十进制数值等于1:


    03 用户三.jpg


    用户四位图如下其十进制数值等于7:


    04 用户四.jpg


    现在我们可以填写数据表第三列:


    id name user_flag
    101 用户一 4
    102 用户二 2
    103 用户三 1
    104 用户四 7

    2.3.4 代码实例

    定义枚举时不要直接定义为1、2、4这类数字,而应该采用位移方式进行定义,这样使用者可以明白设计者的意图。

    /**
     * 用户角色枚举
     *
     * @author 微信公众号「JAVA前线」
     *
     */
    public enum UserRoleEnum {
    
        // 1 -> 00000001
        NORMAL(1, "普通用户"),
    
        // 2 -> 00000010
        MANAGER(1 << 1, "管理员"),
    
        // 4 -> 00000100
        SUPER(1 << 2, "超级管理员")
    
        ;
    
        private int code;
        private String description;
    
        private UserRoleEnum(Integer code, String description) {
            this.code = code;
            this.description = description;
        }
    
        public String getDescription() {
            return description;
        }
    
        public int getCode() {
            return this.code;
        }
    }
    

    假设用户已经具有普通用户角色,我们需要为其增加管理员角色,这就是新增角色,与之对应还有删除角色和查询角色,这些操作需要用到为位运算,详见代码注释。

    /**
     * 用户角色枚举
     *
     * @author 微信公众号「JAVA前线」
     *
     */
    public enum UserRoleEnum {
    
        // 1 -> 00000001
        NORMAL(1, "普通用户"),
    
        // 2 -> 00000010
        MANAGER(1 << 1, "管理员"),
    
        // 4 -> 00000100
        SUPER(1 << 2, "超级管理员")
    
        ;
    
        // 新增角色 -> 位或操作
        // oldRole -> 00000001 -> 普通用户
        // addRole -> 00000010 -> 新增管理员
        // newRole -> 00000011 -> 普通用户和管理员
        public static Integer addRole(Integer oldRole, Integer addRole) {
            return oldRole | addRole;
        }
    
        // 删除角色 -> 位异或操作
        // oldRole -> 00000011 -> 普通用户和管理员
        // delRole -> 00000010 -> 删除管理员
        // newRole -> 00000001 -> 普通用户
        public static Integer removeRole(Integer oldRole, Integer delRole) {
            return oldRole ^ delRole;
        }
    
        // 是否有某种角色 -> 位与操作
        // allRole -> 00000011 -> 普通用户和管理员
        // qryRole -> 00000001 -> 是否有管理员角色
        // resRole -> 00000001 -> 有普通用户角色
        public static boolean hasRole(Integer allRole, Integer qryRole) {
            return qryRole == (role & qryRole);
        }
    
        private int code;
        private String description;
    
        private UserRoleEnum(Integer code, String description) {
            this.code = code;
            this.description = description;
        }
    
        public String getDescription() {
            return description;
        }
    
        public int getCode() {
            return this.code;
        }
    
        public static void main(String[] args) {
            System.out.println(addRole(1, 2));
            System.out.println(removeRole(3, 1));
            System.out.println(hasRole(3, 1));
        }
    }
    

    假设在运营后台查询界面中,需要查询具有普通用户角色的用户数据,可以使用如下SQL语句:

    select * from user_role where (user_flag & 1) = user_flag;
    select * from user_role where (user_flag & b'0001') = user_flag;
    

    我们也可以使用MyBatis语句:

    <select id="selectByUserRole" resultMap="BaseResultMap" parameterType="java.util.Map">
      select * from user_role 
      where user_flag & #{userFlag} = #{userFlag}
    select>
    
    <select id="selectByUserIdAndRole" resultMap="BaseResultMap" parameterType="java.util.Map">
      select * from user_role 
      where id = #{userId} and user_flag & #{userFlag} = #{userFlag}
    select>
    

    3 接口维度

    3.1 设计类型入参

    接口输入参数一定要设计类型入参,可以与数据库biz_type、biz_sub_type相对应,也可以自定义类型最终翻译为与数据库类型相对应,

    如果一开始不用区分类型可以设置为默认值,但是其核心思想是一定要可以通过类型对输入参数进行区分。

    public class OrderDTO {
        private Integer bizType;
        private Integer bizSubType;
        private Long amount;
        private String goodsId;
    }
    
    public Response createOrder(OrderDTO order);
    

    3.2 设计松散入参

    接口输入参数可以设计Map类型松散参数,松散参数缺点是表意能力弱,优点是灵活性强,可以以较低成本新增参数。

    public class OrderDTO {
        private Integer bizType;
        private Integer bizSubType;
        private Long amount;
        private String goodsId;
        private Map params;
    }
    
    public Response createOrder(OrderDTO order);
    

    3.3 设计接口版本号

    对外接口需要设计版本号,对于同一个接口,允许外部业务逐渐切到新版本,新老版本接口会共存一段时间,通过版本号区分。

    /order/1.0/createOrder
    /order/1.1/createOrder
    

    3.4 纵横做设计

    我们分析一个下单场景。当前有ABC三种订单类型:A订单价格9折,物流最大重量不能超过9公斤,不支持退款。B订单价格8折,物流最大重量不能超过8公斤,支持退款。C订单价格7折,物流最大重量不能超过7公斤,支持退款。按照需求字面含义平铺直叙地写代码也并不难:

    public class OrderServiceImpl implements OrderService {
    
        @Resource
        private OrderMapper orderMapper;
    
        @Override
        public void createOrder(OrderBO orderBO) {
            if (null == orderBO) {
                throw new RuntimeException("参数异常");
            }
            if (OrderTypeEnum.isNotValid(orderBO.getType())) {
                throw new RuntimeException("参数异常");
            }
            // A类型订单
            if (OrderTypeEnum.A_TYPE.getCode().equals(orderBO.getType())) {
                orderBO.setPrice(orderBO.getPrice() * 0.9);
                if (orderBO.getWeight() > 9) {
                    throw new RuntimeException("超过物流最大重量");
                }
                orderBO.setRefundSupport(Boolean.FALSE);
            }
            // B类型订单
            else if (OrderTypeEnum.B_TYPE.getCode().equals(orderBO.getType())) {
                orderBO.setPrice(orderBO.getPrice() * 0.8);
                if (orderBO.getWeight() > 8) {
                    throw new RuntimeException("超过物流最大重量");
                }
                orderBO.setRefundSupport(Boolean.TRUE);
            }
            // C类型订单
            else if (OrderTypeEnum.C_TYPE.getCode().equals(orderBO.getType())) {
                orderBO.setPrice(orderBO.getPrice() * 0.7);
                if (orderBO.getWeight() > 7) {
                    throw new RuntimeException("超过物流最大重量");
                }
                orderBO.setRefundSupport(Boolean.TRUE);
            }
            // 保存数据
            OrderDO orderDO = new OrderDO();
            BeanUtils.copyProperties(orderBO, orderDO);
            orderMapper.insert(orderDO);
        }
    }
    

    上述代码从功能上完全可以实现业务需求,但是程序员不仅要满足功能,还需要思考代码的可维护性。如果新增一种订单类型,或者新增一个订单属性处理逻辑,那么我们就要在上述逻辑中新增代码,如果处理不慎就会影响原有逻辑。

    如何改变平铺直叙的思考方式?这就要为问题分析加上纵向和横向两个维度,我选择使用分析矩阵方法,其中纵向表示策略,横向表示场景:


    05 订单_分析矩阵.jpg


    (1) 纵向做隔离

    纵向维度表示策略,不同策略在逻辑上和业务上应该是隔离的,本实例包括优惠策略、物流策略和退款策略,策略作为抽象,不同订单类型去扩展这个抽象,策略模式非常适合这种场景。本文详细分析优惠策略,物流策略和退款策略同理。

    // 优惠策略
    public interface DiscountStrategy {
        public void discount(OrderBO orderBO);
    }
    
    // A类型优惠策略
    @Component
    public class TypeADiscountStrategy implements DiscountStrategy {
    
        @Override
        public void discount(OrderBO orderBO) {
            orderBO.setPrice(orderBO.getPrice() * 0.9);
        }
    }
    
    // B类型优惠策略
    @Component
    public class TypeBDiscountStrategy implements DiscountStrategy {
    
        @Override
        public void discount(OrderBO orderBO) {
            orderBO.setPrice(orderBO.getPrice() * 0.8);
        }
    }
    
    // C类型优惠策略
    @Component
    public class TypeCDiscountStrategy implements DiscountStrategy {
    
        @Override
        public void discount(OrderBO orderBO) {
            orderBO.setPrice(orderBO.getPrice() * 0.7);
        }
    }
    
    // 优惠策略工厂
    @Component
    public class DiscountStrategyFactory implements InitializingBean {
        private Map strategyMap = new HashMap<>();
    
        @Resource
        private TypeADiscountStrategy typeADiscountStrategy;
        @Resource
        private TypeBDiscountStrategy typeBDiscountStrategy;
        @Resource
        private TypeCDiscountStrategy typeCDiscountStrategy;
    
        public DiscountStrategy getStrategy(String type) {
            return strategyMap.get(type);
        }
    
        @Override
        public void afterPropertiesSet() throws Exception {
            strategyMap.put(OrderTypeEnum.A_TYPE.getCode(), typeADiscountStrategy);
            strategyMap.put(OrderTypeEnum.B_TYPE.getCode(), typeBDiscountStrategy);
            strategyMap.put(OrderTypeEnum.C_TYPE.getCode(), typeCDiscountStrategy);
        }
    }
    
    // 优惠策略执行
    @Component
    public class DiscountStrategyExecutor {
        private DiscountStrategyFactory discountStrategyFactory;
    
        public void discount(OrderBO orderBO) {
            DiscountStrategy discountStrategy = discountStrategyFactory.getStrategy(orderBO.getType());
            if (null == discountStrategy) {
                throw new RuntimeException("无优惠策略");
            }
            discountStrategy.discount(orderBO);
        }
    }
    

    (2) 横向做编排

    横向维度表示场景,一种订单类型在广义上可以认为是一种业务场景,在场景中将独立的策略进行串联,模板方法设计模式适用于这种场景。

    模板方法模式一般使用抽象类定义算法骨架,同时定义一些抽象方法,这些抽象方法延迟到子类实现,这样子类不仅遵守了算法骨架约定,也实现了自己的算法。既保证了规约也兼顾灵活性,这就是用抽象构建框架,用实现扩展细节。

    // 创建订单服务
    public interface CreateOrderService {
        public void createOrder(OrderBO orderBO);
    }
    
    // 抽象创建订单流程
    public abstract class AbstractCreateOrderFlow {
    
        @Resource
        private OrderMapper orderMapper;
    
        public void createOrder(OrderBO orderBO) {
            // 参数校验
            if (null == orderBO) {
                throw new RuntimeException("参数异常");
            }
            if (OrderTypeEnum.isNotValid(orderBO.getType())) {
                throw new RuntimeException("参数异常");
            }
            // 计算优惠
            discount(orderBO);
            // 计算重量
            weighing(orderBO);
            // 退款支持
            supportRefund(orderBO);
            // 保存数据
            OrderDO orderDO = new OrderDO();
            BeanUtils.copyProperties(orderBO, orderDO);
            orderMapper.insert(orderDO);
        }
    
        public abstract void discount(OrderBO orderBO);
    
        public abstract void weighing(OrderBO orderBO);
    
        public abstract void supportRefund(OrderBO orderBO);
    }
    
    // 实现创建订单流程
    @Service
    public class CreateOrderFlow extends AbstractCreateOrderFlow {
    
        @Resource
        private DiscountStrategyExecutor discountStrategyExecutor;
        @Resource
        private ExpressStrategyExecutor expressStrategyExecutor;
        @Resource
        private RefundStrategyExecutor refundStrategyExecutor;
    
        @Override
        public void discount(OrderBO orderBO) {
            discountStrategyExecutor.discount(orderBO);
        }
    
        @Override
        public void weighing(OrderBO orderBO) {
            expressStrategyExecutor.weighing(orderBO);
        }
    
        @Override
        public void supportRefund(OrderBO orderBO) {
            refundStrategyExecutor.supportRefund(orderBO);
        }
    }
    

    4 文章总结

    本文介绍了设计类型字段、设计扩展字段、设计业务二进制字段、设计类型入参、设计松散入参、设计接口版本号、纵横做设计七种增加系统可扩展性具体方案,希望本文对大家有所帮助。


  • 相关阅读:
    [计算机网络] VPN技术
    CesiumJS 2022^ 源码解读[0] - 文章目录与源码工程结构
    文化融合与社交网络:Facebook的角色
    websocket多线程发送消息报错TEXT_PARTIAL_WRITING--自旋锁替换synchronized独占锁的使用案例
    Spring Boot 配置文件这样加密,才足够安全!
    WPF DataGrid详细列表手动显示与隐藏
    VScode折叠代码
    Python3操作redis百万级数据迁移,单机到单机,集群到集群
    Java面试题:MVCC
    Optimize File Size for Accessible PDFs
  • 原文地址:https://www.cnblogs.com/javafront/p/17418700.html