1 复杂、繁杂、庞杂

在开发工作中我们经常会听到:这个业务很复杂,这个系统很复杂,这个逻辑很复杂,只要是处理遇到困难的场景,似乎都可以使用复杂这个词进行描述。

但是我认为困难之所以困难,原因还是有所不同的,不能用复杂这个词笼而统之,有加以区分的必要。大体上我认为可以分为复杂、繁杂、庞杂三个类型。

复杂和繁杂二者均包含分支多和逻辑多的含义,但是不同之处在于,复杂场景是可以理出头绪的,如果设计得当,是可以设计出很优雅的系统的。但是繁杂场景是难以理出头绪的,为了兼容只能打各种补丁,最终积重难返只能系统重构。

还有一种类型可以称之为庞杂,当数量达到一定规模时,复杂和繁杂都可以演化为庞杂。虽然同样是庞杂,但是也有复杂庞杂和繁杂庞杂的区别。本文只要讨论清楚复杂和庞杂,只要加上数量维度就是庞杂。

我们在开发中可以写复杂的代码,要尽量避免繁杂的代码,其中代码耦合就是一种典型的繁杂场景,模块间高度耦合的代码导致最终根本无法维护,本文我们讨论七种代码耦合类型。

2 代码耦合类型

七种代码耦合类型根据耦合程度由高到低排序分别是:内容耦合、公共耦合、外部耦合、控制耦合、标记耦合、数据耦合和非直接耦合。

2.1 内容耦合

一个模块可以直接访问另一个模块的内部数据被称为内容耦合,这是耦合性最强的类型,这也是我们需要尽量避免的。

假设模块A是订单模块,模块B是支付模块,如果支付模块可以直接访问订单数据表,那么至少会带来以下问题。

第一个问题是存在重复的数据访问层代码,支付和订单模块都要写订单数据访问代码。 第二个问题是如果订单业务变动,需要变更订单数据字段,如果支付模块没有跟着及时 变更,那么可能会造成业务错误。

第三个问题是如果订单业务变动,需要分库分表拆分数据,如果支付模块没有跟着及时变更,例如没有使用shardingKey进行查询或者旧库表停写,那么可能会造成支付模块严重错误。

第四个问题是业务入口没有收敛,访问入口到处散落,如果想要业务变更则需要多处修改,非常不利于维护。

2.2 公共耦合

多个模块都访问同一个公共数据环境被称为公共耦合,公共数据环境例如全局数据结构、共享通信区和内存公共覆盖区。

例如在项目中使用Apollo动态配置,配置项A内容是一段JSON,订单模块和支付模块均读取并解析这段数据结构进行业务处理。

public class ApolloConfig {@Value("${apollo.json.config}")private String jsonConfig;}public class JsonConfig {public int type;public boolean switchOpen;}public class OrderServiceImpl {public void createOrder() {String jsonConfig = apolloConfig.getJsonConfig();JsonConfig config = JSONUtils.toBean(jsonConfig, JsonConfig.class);if(config.getType() == TypeEnum.ORDER.getCode() && config.isSwitchOpen()) {createBizOrder();}}}public class PayServiceImpl {public void createPayOrder() {String jsonConfig = apolloConfig.getJsonConfig();JsonConfig config = JSONUtils.toBean(jsonConfig, JsonConfig.class);if(config.getType() == TypeEnum.PAY.getCode() && config.isSwitchOpen()) {createBizPayOrder();}}}

2.3 外部耦合

多个模块访问同一个全局简单变量(非全局数据结构)并且不是通过参数表传递此全局变量信息被称为外部耦合。

例如在项目中使用Apollo动态配置,配置项A内容是一个简单变量,订单模块和支付模块均读取这个简单变量进行业务处理。

public class ApolloConfig {@Value("${apollo.type.config}")private int typeConfig;}public class OrderServiceImpl {public void createOrder() {if(apolloConfig.getTypeConfig() == TypeEnum.ORDER.getCode()) {createBizOrder();}}}public class PayServiceImpl {public void createPayOrder() {if(apolloConfig.getTypeConfig() == TypeEnum.PAY.getCode()) {createBizPayOrder();}}}

2.4 控制耦合

模块之间传递信息中包含用于控制模块内部的信息被称为控制耦合。控制耦合可能会导致模块之间控制逻辑相互交织,逻辑之间相互影响,非常不利于代码维护。

控制耦合代码实例如下,我们可以看到模块B代码逻辑重度依赖模块A类型,假设A类型发生了变化很可能就会影响B逻辑:

public class ModuleA {private int type;}public class A {private B b = new B();public void methondA(int type) {ModuleA moduleA = new ModuleA(type);b.methondB(moduleA);}}public class B {public void methondB(ModuleA moduleA) {if(moduleA.getType() == 1) {action1();} else if(moduleA.getType() == 2) {action2();}}}

2.5 标记耦合

多个模块通过参数表传递数据结构信息被称为标记耦合,可以类比JAVA语言引用传递。

2.6 数据耦合

多个模块通过参数表传递简单数据信息被称为标记耦合,可以类比JAVA语言值传递。

2.7 非直接耦合

多个模块之间没有直接联系,通过主模块的控制和调用实现联系被称为非直接耦合,这也是一种理想的耦合方式。

我们重点谈一谈非直接耦合。复杂业务之所以复杂,一个重要原因是涉及角色或者类型较多,很难平铺直叙地进行设计。如果非要进行平铺设计,必然会出现大量if else代码块。

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

public class OrderServiceImpl implements OrderService {@Resourceprivate OrderMapper orderMapper;@Overridepublic 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);}}

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

为了避免牵一发而动全身这种情况,设计模式中的开闭原则要求我们面向新增开放,面向修改关闭,我认为这是设计模式中最重要的一条原则。

需求变化通过扩展,而不是通过修改已有代码实现,这样就保证代码稳定性。扩展也不是随意扩展,因为事先定义了算法,扩展也是根据算法扩展,用抽象构建框架,用实现扩展细节。标准意义的二十三种设计模式说到底最终都是在遵循开闭原则。

如何改变平铺直叙的思考方式?我们需要增加分析维度。其中最常见的是增加横向和纵向两个维度,总体而言横向扩展的是思考广度,纵向扩展的是思考深度,对应到系统设计而言可以总结为:纵向做隔离,横向做编排。

这时我们可以为问题分析加上纵向和横向两个维度,选择使用分析矩阵方法,其中纵向表示策略,横向表示场景:

2.7.1 纵向做隔离

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

// 优惠策略public interface DiscountStrategy {public void discount(OrderBO orderBO);}// A类型优惠策略@Componentpublic class TypeADiscountStrategy implements DiscountStrategy {@Overridepublic void discount(OrderBO orderBO) {orderBO.setPrice(orderBO.getPrice() * 0.9);}}// B类型优惠策略@Componentpublic class TypeBDiscountStrategy implements DiscountStrategy {@Overridepublic void discount(OrderBO orderBO) {orderBO.setPrice(orderBO.getPrice() * 0.8);}}// C类型优惠策略@Componentpublic class TypeCDiscountStrategy implements DiscountStrategy {@Overridepublic void discount(OrderBO orderBO) {orderBO.setPrice(orderBO.getPrice() * 0.7);}}// 优惠策略工厂@Componentpublic class DiscountStrategyFactory implements InitializingBean {private Map strategyMap = new HashMap();@Resourceprivate TypeADiscountStrategy typeADiscountStrategy;@Resourceprivate TypeBDiscountStrategy typeBDiscountStrategy;@Resourceprivate TypeCDiscountStrategy typeCDiscountStrategy;public DiscountStrategy getStrategy(String type) {return strategyMap.get(type);}@Overridepublic 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);}}// 优惠策略执行@Componentpublic 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.7.2 横向做编排

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

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

// 创建订单服务public interface CreateOrderService {public void createOrder(OrderBO orderBO);}// 抽象创建订单流程public abstract class AbstractCreateOrderFlow {@Resourceprivate 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);}// 实现创建订单流程@Servicepublic class CreateOrderFlow extends AbstractCreateOrderFlow {@Resourceprivate DiscountStrategyExecutor discountStrategyExecutor;@Resourceprivate ExpressStrategyExecutor expressStrategyExecutor;@Resourceprivate RefundStrategyExecutor refundStrategyExecutor;@Overridepublic void discount(OrderBO orderBO) {discountStrategyExecutor.discount(orderBO);}@Overridepublic void weighing(OrderBO orderBO) {expressStrategyExecutor.weighing(orderBO);}@Overridepublic void supportRefund(OrderBO orderBO) {refundStrategyExecutor.supportRefund(orderBO);}}

2.7.3 纵横思维

上述实例业务和代码并不复杂,其实复杂业务场景也不过是简单场景的叠加、组合和交织,无外乎也是通过纵向做隔离、横向做编排寻求答案。

纵向维度抽象出能力池这个概念,能力池中包含许多能力,不同的能力按照不同业务维度聚合,例如优惠能力池,物流能力池,退款能力池。我们可以看到两种程度的隔离性,能力池之间相互隔离,能力之间也相互隔离。

横向维度将能力从能力池选出来,按照业务需求串联在一起,形成不同业务流程。因为能力可以任意组合,所以体现了很强的灵活性。除此之外,不同能力既可以串行执行,如果不同能力之间没有依赖关系,也可以如同流程Y一样并行执行,提升执行效率。

3 文章总结

第一本文区分了复杂、繁杂、庞杂这一组概念,复杂和繁杂虽然都比较难处理,但是复杂是可以理出头绪的,而繁杂最终会积重难返。我们应该尽量避免繁杂的代码。复杂和繁杂加上数量维度就成为庞杂。

第二本文介绍了七种代码耦合类型,根据耦合程度由高到低排序分别是:内容耦合、公共耦合、外部耦合、控制耦合、标记耦合、数据耦合和非直接耦合。我们应该尽量写耦合度低的代码。

第三本文由一个复杂订单场景实例出发,重点介绍了非直接耦合类型,可以看到即使是复杂场景,通过合理的设计也可以优雅实现,希望本文对大家有所帮助。