likes
comments
collection

复杂、繁杂、庞杂:图解七种代码耦合类型

作者站长头像
站长
· 阅读数 12

欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时欢迎大家加我个人微信「java_front」一起交流学习

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 {

    @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);
    }
}

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

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

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

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

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

复杂、繁杂、庞杂:图解七种代码耦合类型

2.7.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<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.7.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);
    }
}

2.7.3 纵横思维

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

复杂、繁杂、庞杂:图解七种代码耦合类型

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

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

3 文章总结

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

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

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

欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时欢迎大家加我个人微信「java_front」一起交流学习