复杂、繁杂、庞杂:图解七种代码耦合类型
共 9600字,需浏览 20分钟
·
2021-12-15 22:16
JAVA前线
欢迎大家关注公众号「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 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前线」查看更多精彩分享,主要内容包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时也非常欢迎大家加我微信「java_front」一起交流学习