Seven ways to enhance code scalability (detailed explanation with multiple pictures)

1 Six Principles

There are six design principles in design patterns:

Single Responsibility Principle: A class does only one thing

Li-style substitution principle: subclasses can extend parent classes

Dependency Inversion Principle: Programming to Interfaces

Interface isolation principle: high cohesion and low coupling

Demeter's Law: The Principle of Least Knowing

Principle of opening and closing: close modification, open new addition

I think the principle of opening and closing is the most important among these six principles, and the principle of opening and closing is an important cornerstone of scalability.

The first reason is that when the requirements change, it should be implemented by adding new codes instead of modifying existing codes, so as to ensure the stability of the codes and avoid affecting the whole body.

The second reason is that the code framework can be defined in advance, and the extension is also based on the framework extension, which reflects the use of abstraction to build the framework and the implementation of the extension details to ensure stability and flexibility.

The third reason is that although the other five principles have different emphases, they can all be included in the principle of opening and closing.

The fourth reason is that the standard twenty-three design patterns are ultimately following the principle of opening and closing.

Since the principle of opening and closing is so important, how should we follow the principle of opening and closing in system design?

2 Database Dimensions

2.1 Design Type Field

Two new columns are added during the data table design: biz_type and biz_sub_type. Even if these two columns are not used at present, they can be used in subsequent extensions, and a default value can be set first.

biz_type can refer to business types, business channels, tenants, etc. In the final analysis, it is used to isolate large business types. If a new large business type is added to the subsequent business, it can be isolated through this field.

biz_sub_type can subdivide small businesses in large business types, making the business more detailed and more flexible.

2.2 Design extension fields

Three new columns are added during data table design: extend1, extend2, and extend3, which can be set to empty first. The extension field stores JSON type information, and stores some extended information, additional information, loose information or previously unestimated information, ensuring flexibility.

The reason why three extension fields are set is to increase isolation. For example, extend1 stores order extensions, extend2 stores commodity extensions, and extend3 stores marketing extensions.

2.3 Designing business binary fields

2.3.1 Requirement background

When designing the data table, a new business binary is added. This field can greatly expand the expressive ability of the business. Suppose there are three roles for users in the system: ordinary user, administrator, and super administrator. Now it is necessary to design a user role table to record such information. It is not difficult for us to design the following scheme:

id name super admin normal
101 user one 1 0 0
102 user two 0 1 0
103 user three 0 0 1
104 user four 1 1 1

Observing the above table, it is not difficult to find that user 1 has the role of super administrator, user 2 has the role of administrator, user 3 has the role of ordinary user, and user 4 has the role of three roles at the same time. What if a new role is added at this time? Then add a new field.

2.3.2 Problems found

There is no functional problem in table design according to the above-mentioned one field representing a role. The advantage is that it is easy to understand and the structure is clear, but let’s think about it. Is there any problem? The author has encountered the following problems: In a complex business environment, a piece of data may be used in different scenarios. For example, the above data is stored in the MySQL database, and this piece of data will also be used in the following scenarios:

Retrieving data needs to be synchronized to ES

The business side uses this table to calculate business indicators through Flink

The business side subscribes to this table Binlog message for business processing

If the table structure changes, the data sources will need to be connected again, and the business side will also need to modify the code, so the development cost is very high. Is there a way to avoid such problems?

2.3.3 Solutions

We can use the bitmap method so that the same field can represent multiple business meanings. First design the following data table, the userFlag field is temporarily left blank.

id name user_flag
101 user one leave blank
102 user two leave blank
103 user three leave blank
104 user four leave blank

Each bit of the design bitmap represents a role:

Use the bitmap method to represent the following data table:

id name super admin normal
101 user one 1 0 0
102 user two 0 1 0
103 user three 0 0 1
104 user four 1 1 1

The user bitmap is as follows, its decimal value is equal to 4:

The user bitmap is as follows, its decimal value is equal to 2:

 The three-bit map of the user is as follows, and its decimal value is equal to 1:

 The user four-bit map is as follows, and its decimal value is equal to 7:

 Now we can fill in the third column of the data table:

id name user_flag
101 user one 4
102 user two 2
103 user three 1
104 user four 7

2.3.4 Code Examples

When defining an enumeration, do not directly define it as a number such as 1, 2, or 4, but use a displacement method to define it, so that users can understand the designer's intention.

/**
 * 用户角色枚举
 *
 * @author 
 *
 */
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;
    }
}
复制代码

Assuming that the user already has a common user role, we need to add an administrator role to it, which is the new role, and correspondingly delete and query roles. These operations require bit operations, see code comments for details.

/**
 * 用户角色枚举
 *
 * @author 
 *
 */
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));
    }
}
复制代码

Assuming that in the operation background query interface, you need to query user data with a common user role, you can use the following SQL statement:

select * from user_role where (user_flag & 1) = user_flag;
select * from user_role where (user_flag & b'0001') = user_flag;
复制代码

We can also use the MyBatis statement:

<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 Interface Dimensions

3.1 Design Type Input Parameters

The input parameters of the interface must be designed with type input parameters, which can correspond to the database biz_type, biz_sub_type, or the custom type can be finally translated to correspond to the database type.

If you don't need to distinguish the type at the beginning, you can set it as the default value, but the core idea is that you must be able to distinguish the input parameters by type.

public class OrderDTO {
    private Integer bizType;
    private Integer bizSubType;
    private Long amount;
    private String goodsId;
}

public Response<String> createOrder(OrderDTO order);
复制代码

3.2 Design loose input parameters

Interface input parameters can be designed with Map type loose parameters. The disadvantage of loose parameters is weak expressive ability, and the advantage is strong flexibility, and new parameters can be added at a low cost.

public class OrderDTO {
    private Integer bizType;
    private Integer bizSubType;
    private Long amount;
    private String goodsId;
    private Map<String, String> params;
}

public Response<String> createOrder(OrderDTO order);
复制代码

3.3 Design interface version number

The external interface needs to be designed with a version number. For the same interface, external services are allowed to gradually switch to the new version. The old and new versions of the interface will coexist for a period of time and are distinguished by the version number.

/order/1.0/createOrder
/order/1.1/createOrder
复制代码

3.4 Vertical and horizontal design

Let's analyze an order scenario. There are currently three order types, ABC: 10% off the price of order A, the maximum weight of logistics cannot exceed 9 kg, and no refund is supported. B order price is 20% off, the maximum weight of logistics cannot exceed 8 kg, and refunds are supported. C order price is 30% off, and the maximum weight of logistics cannot exceed 7 kg, and refunds are supported. It is not difficult to write code in a straightforward manner according to the literal meaning of the requirements:

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);
    }
}
复制代码

The above code can fully realize the business requirements from the function, but the programmer not only needs to satisfy the function, but also needs to think about the maintainability of the code. If a new order type is added, or an order attribute processing logic is added, then we need to add code to the above logic, and if it is not handled carefully, the original logic will be affected.

How to change the way of thinking in a straightforward way? This requires adding vertical and horizontal dimensions to the problem analysis. I choose to use the analysis matrix method, where the vertical represents the strategy and the horizontal represents the scene:

 

(1) vertical isolation

The vertical dimension represents strategies. Different strategies should be isolated logically and business-wise. This example includes preferential strategies, logistics strategies, and refund strategies. Strategies are abstractions that can be extended by different order types. The strategy mode is very suitable for this scenario. . This article analyzes the discount strategy in detail, and the logistics strategy and refund strategy are the same.

// 优惠策略
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<String, DiscountStrategy> 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) Horizontal arrangement

The horizontal dimension represents a scenario. An order type can be considered as a business scenario in a broad sense, in which independent strategies are connected in series, and the template method design pattern is suitable for this scenario.

The template method pattern generally uses an abstract class to define the algorithm skeleton, and at the same time defines some abstract methods. These abstract methods are delayed until the implementation of the subclass, so that the subclass not only abides by the algorithm skeleton contract, but also implements its own algorithm. It not only guarantees the specification but also takes into account the flexibility. This is to build the framework with abstraction and expand the details with implementation.

// 创建订单服务
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 Article Summary

This article introduces the design type field, design extension field, design business binary field, design type input parameter, design loose input parameter, design interface version number, and vertical and horizontal design seven specific solutions to increase system scalability. I hope this article will be helpful to everyone help.

Guess you like

Origin blog.csdn.net/qq_41221596/article/details/129347383