復(fù)雜、繁雜、龐雜:圖解七種代碼耦合類型
JAVA前線?
歡迎大家關(guān)注公眾號「JAVA前線」查看更多精彩分享,主要內(nèi)容包括源碼分析、實(shí)際應(yīng)用、架構(gòu)思維、職場分享、產(chǎn)品思考等等,同時也非常歡迎大家加我微信「java_front」一起交流學(xué)習(xí)
1 復(fù)雜、繁雜、龐雜
在開發(fā)工作中我們經(jīng)常會聽到:這個業(yè)務(wù)很復(fù)雜,這個系統(tǒng)很復(fù)雜,這個邏輯很復(fù)雜,只要是處理遇到困難的場景,似乎都可以使用復(fù)雜這個詞進(jìn)行描述。
但是我認(rèn)為困難之所以困難,原因還是有所不同的,不能用復(fù)雜這個詞籠而統(tǒng)之,有加以區(qū)分的必要。大體上我認(rèn)為可以分為復(fù)雜、繁雜、龐雜三個類型。
復(fù)雜和繁雜二者均包含分支多和邏輯多的含義,但是不同之處在于,復(fù)雜場景是可以理出頭緒的,如果設(shè)計(jì)得當(dāng),是可以設(shè)計(jì)出很優(yōu)雅的系統(tǒng)的。但是繁雜場景是難以理出頭緒的,為了兼容只能打各種補(bǔ)丁,最終積重難返只能系統(tǒng)重構(gòu)。
還有一種類型可以稱之為龐雜,當(dāng)數(shù)量達(dá)到一定規(guī)模時,復(fù)雜和繁雜都可以演化為龐雜。雖然同樣是龐雜,但是也有復(fù)雜龐雜和繁雜龐雜的區(qū)別。本文只要討論清楚復(fù)雜和龐雜,只要加上數(shù)量維度就是龐雜。
我們在開發(fā)中可以寫復(fù)雜的代碼,要盡量避免繁雜的代碼,其中代碼耦合就是一種典型的繁雜場景,模塊間高度耦合的代碼導(dǎo)致最終根本無法維護(hù),本文我們討論七種代碼耦合類型。
2 代碼耦合類型
七種代碼耦合類型根據(jù)耦合程度由高到低排序分別是:內(nèi)容耦合、公共耦合、外部耦合、控制耦合、標(biāo)記耦合、數(shù)據(jù)耦合和非直接耦合。

2.1 內(nèi)容耦合
一個模塊可以直接訪問另一個模塊的內(nèi)部數(shù)據(jù)被稱為內(nèi)容耦合,這是耦合性最強(qiáng)的類型,這也是我們需要盡量避免的。

假設(shè)模塊A是訂單模塊,模塊B是支付模塊,如果支付模塊可以直接訪問訂單數(shù)據(jù)表,那么至少會帶來以下問題。
第一個問題是存在重復(fù)的數(shù)據(jù)訪問層代碼,支付和訂單模塊都要寫訂單數(shù)據(jù)訪問代碼。 第二個問題是如果訂單業(yè)務(wù)變動,需要變更訂單數(shù)據(jù)字段,如果支付模塊沒有跟著及時 變更,那么可能會造成業(yè)務(wù)錯誤。
第三個問題是如果訂單業(yè)務(wù)變動,需要分庫分表拆分?jǐn)?shù)據(jù),如果支付模塊沒有跟著及時變更,例如沒有使用shardingKey進(jìn)行查詢或者舊庫表停寫,那么可能會造成支付模塊嚴(yán)重錯誤。
第四個問題是業(yè)務(wù)入口沒有收斂,訪問入口到處散落,如果想要業(yè)務(wù)變更則需要多處修改,非常不利于維護(hù)。
2.2 公共耦合
多個模塊都訪問同一個公共數(shù)據(jù)環(huán)境被稱為公共耦合,公共數(shù)據(jù)環(huán)境例如全局?jǐn)?shù)據(jù)結(jié)構(gòu)、共享通信區(qū)和內(nèi)存公共覆蓋區(qū)。

例如在項(xiàng)目中使用Apollo動態(tài)配置,配置項(xiàng)A內(nèi)容是一段JSON,訂單模塊和支付模塊均讀取并解析這段數(shù)據(jù)結(jié)構(gòu)進(jìn)行業(yè)務(wù)處理。
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 外部耦合
多個模塊訪問同一個全局簡單變量(非全局?jǐn)?shù)據(jù)結(jié)構(gòu))并且不是通過參數(shù)表傳遞此全局變量信息被稱為外部耦合。

例如在項(xiàng)目中使用Apollo動態(tài)配置,配置項(xiàng)A內(nèi)容是一個簡單變量,訂單模塊和支付模塊均讀取這個簡單變量進(jìn)行業(yè)務(wù)處理。
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 控制耦合
模塊之間傳遞信息中包含用于控制模塊內(nèi)部的信息被稱為控制耦合??刂岂詈峡赡軙?dǎo)致模塊之間控制邏輯相互交織,邏輯之間相互影響,非常不利于代碼維護(hù)。

控制耦合代碼實(shí)例如下,我們可以看到模塊B代碼邏輯重度依賴模塊A類型,假設(shè)A類型發(fā)生了變化很可能就會影響B(tài)邏輯:
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 標(biāo)記耦合
多個模塊通過參數(shù)表傳遞數(shù)據(jù)結(jié)構(gòu)信息被稱為標(biāo)記耦合,可以類比JAVA語言引用傳遞。

2.6 數(shù)據(jù)耦合
多個模塊通過參數(shù)表傳遞簡單數(shù)據(jù)信息被稱為標(biāo)記耦合,可以類比JAVA語言值傳遞。

2.7 非直接耦合
多個模塊之間沒有直接聯(lián)系,通過主模塊的控制和調(diào)用實(shí)現(xiàn)聯(lián)系被稱為非直接耦合,這也是一種理想的耦合方式。

我們重點(diǎn)談一談非直接耦合。復(fù)雜業(yè)務(wù)之所以復(fù)雜,一個重要原因是涉及角色或者類型較多,很難平鋪直敘地進(jìn)行設(shè)計(jì)。如果非要進(jìn)行平鋪設(shè)計(jì),必然會出現(xiàn)大量if else代碼塊。
我們首先分析一個下單場景。當(dāng)前有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("參數(shù)異常");
????????}
????????if?(OrderTypeEnum.isNotValid(orderBO.getType()))?{
????????????throw?new?RuntimeException("參數(shù)異常");
????????}
????????//?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);
????????}
????????//?保存數(shù)據(jù)
????????OrderDO?orderDO?=?new?OrderDO();
????????BeanUtils.copyProperties(orderBO,?orderDO);
????????orderMapper.insert(orderDO);
????}
}
上述代碼從功能上完全可以實(shí)現(xiàn)業(yè)務(wù)需求,但是程序員不僅要滿足功能,還需要思考代碼的可維護(hù)性。如果新增一種訂單類型,或者新增一個訂單屬性處理邏輯,那么我們就要在上述邏輯中新增代碼,如果處理不慎就會影響原有邏輯。
為了避免牽一發(fā)而動全身這種情況,設(shè)計(jì)模式中的開閉原則要求我們面向新增開放,面向修改關(guān)閉,我認(rèn)為這是設(shè)計(jì)模式中最重要的一條原則。
需求變化通過擴(kuò)展,而不是通過修改已有代碼實(shí)現(xiàn),這樣就保證代碼穩(wěn)定性。擴(kuò)展也不是隨意擴(kuò)展,因?yàn)槭孪榷x了算法,擴(kuò)展也是根據(jù)算法擴(kuò)展,用抽象構(gòu)建框架,用實(shí)現(xiàn)擴(kuò)展細(xì)節(jié)。標(biāo)準(zhǔn)意義的二十三種設(shè)計(jì)模式說到底最終都是在遵循開閉原則。
如何改變平鋪直敘的思考方式?我們需要增加分析維度。其中最常見的是增加橫向和縱向兩個維度,總體而言橫向擴(kuò)展的是思考廣度,縱向擴(kuò)展的是思考深度,對應(yīng)到系統(tǒng)設(shè)計(jì)而言可以總結(jié)為:縱向做隔離,橫向做編排。
這時我們可以為問題分析加上縱向和橫向兩個維度,選擇使用分析矩陣方法,其中縱向表示策略,橫向表示場景:

2.7.1 縱向做隔離
縱向維度表示策略,不同策略在邏輯上和業(yè)務(wù)上應(yīng)該是隔離的,本實(shí)例包括優(yōu)惠策略、物流策略和退款策略,策略作為抽象,不同訂單類型去擴(kuò)展這個抽象,策略模式非常適合這種場景。本文詳細(xì)分析優(yōu)惠策略,物流策略和退款策略同理。
//?優(yōu)惠策略
public?interface?DiscountStrategy?{
????public?void?discount(OrderBO?orderBO);
}
//?A類型優(yōu)惠策略
@Component
public?class?TypeADiscountStrategy?implements?DiscountStrategy?{
????@Override
????public?void?discount(OrderBO?orderBO)?{
????????orderBO.setPrice(orderBO.getPrice()?*?0.9);
????}
}
//?B類型優(yōu)惠策略
@Component
public?class?TypeBDiscountStrategy?implements?DiscountStrategy?{
????@Override
????public?void?discount(OrderBO?orderBO)?{
????????orderBO.setPrice(orderBO.getPrice()?*?0.8);
????}
}
//?C類型優(yōu)惠策略
@Component
public?class?TypeCDiscountStrategy?implements?DiscountStrategy?{
????@Override
????public?void?discount(OrderBO?orderBO)?{
????????orderBO.setPrice(orderBO.getPrice()?*?0.7);
????}
}
//?優(yōu)惠策略工廠
@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);
????}
}
//?優(yōu)惠策略執(zhí)行
@Component
public?class?DiscountStrategyExecutor?{
????private?DiscountStrategyFactory?discountStrategyFactory;
????public?void?discount(OrderBO?orderBO)?{
????????DiscountStrategy?discountStrategy?=?discountStrategyFactory.getStrategy(orderBO.getType());
????????if?(null?==?discountStrategy)?{
????????????throw?new?RuntimeException("無優(yōu)惠策略");
????????}
????????discountStrategy.discount(orderBO);
????}
}
2.7.2 橫向做編排
橫向維度表示場景,一種訂單類型在廣義上可以認(rèn)為是一種業(yè)務(wù)場景,在場景中將獨(dú)立的策略進(jìn)行串聯(lián),模板方法設(shè)計(jì)模式適用于這種場景。
模板方法模式一般使用抽象類定義算法骨架,同時定義一些抽象方法,這些抽象方法延遲到子類實(shí)現(xiàn),這樣子類不僅遵守了算法骨架約定,也實(shí)現(xiàn)了自己的算法。既保證了規(guī)約也兼顧靈活性,這就是用抽象構(gòu)建框架,用實(shí)現(xiàn)擴(kuò)展細(xì)節(jié)。
//?創(chuàng)建訂單服務(wù)
public?interface?CreateOrderService?{
????public?void?createOrder(OrderBO?orderBO);
}
//?抽象創(chuàng)建訂單流程
public?abstract?class?AbstractCreateOrderFlow?{
????@Resource
????private?OrderMapper?orderMapper;
????public?void?createOrder(OrderBO?orderBO)?{
????????//?參數(shù)校驗(yàn)
????????if?(null?==?orderBO)?{
????????????throw?new?RuntimeException("參數(shù)異常");
????????}
????????if?(OrderTypeEnum.isNotValid(orderBO.getType()))?{
????????????throw?new?RuntimeException("參數(shù)異常");
????????}
????????//?計(jì)算優(yōu)惠
????????discount(orderBO);
????????//?計(jì)算重量
????????weighing(orderBO);
????????//?退款支持
????????supportRefund(orderBO);
????????//?保存數(shù)據(jù)
????????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);
}
//?實(shí)現(xiàn)創(chuàng)建訂單流程
@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 縱橫思維
上述實(shí)例業(yè)務(wù)和代碼并不復(fù)雜,其實(shí)復(fù)雜業(yè)務(wù)場景也不過是簡單場景的疊加、組合和交織,無外乎也是通過縱向做隔離、橫向做編排尋求答案。

縱向維度抽象出能力池這個概念,能力池中包含許多能力,不同的能力按照不同業(yè)務(wù)維度聚合,例如優(yōu)惠能力池,物流能力池,退款能力池。我們可以看到兩種程度的隔離性,能力池之間相互隔離,能力之間也相互隔離。
橫向維度將能力從能力池選出來,按照業(yè)務(wù)需求串聯(lián)在一起,形成不同業(yè)務(wù)流程。因?yàn)槟芰梢匀我饨M合,所以體現(xiàn)了很強(qiáng)的靈活性。除此之外,不同能力既可以串行執(zhí)行,如果不同能力之間沒有依賴關(guān)系,也可以如同流程Y一樣并行執(zhí)行,提升執(zhí)行效率。
3 文章總結(jié)
第一本文區(qū)分了復(fù)雜、繁雜、龐雜這一組概念,復(fù)雜和繁雜雖然都比較難處理,但是復(fù)雜是可以理出頭緒的,而繁雜最終會積重難返。我們應(yīng)該盡量避免繁雜的代碼。復(fù)雜和繁雜加上數(shù)量維度就成為龐雜。
第二本文介紹了七種代碼耦合類型,根據(jù)耦合程度由高到低排序分別是:內(nèi)容耦合、公共耦合、外部耦合、控制耦合、標(biāo)記耦合、數(shù)據(jù)耦合和非直接耦合。我們應(yīng)該盡量寫耦合度低的代碼。
第三本文由一個復(fù)雜訂單場景實(shí)例出發(fā),重點(diǎn)介紹了非直接耦合類型,可以看到即使是復(fù)雜場景,通過合理的設(shè)計(jì)也可以優(yōu)雅實(shí)現(xiàn),希望本文對大家有所幫助。
JAVA前線?
歡迎大家關(guān)注公眾號「JAVA前線」查看更多精彩分享,主要內(nèi)容包括源碼分析、實(shí)際應(yīng)用、架構(gòu)思維、職場分享、產(chǎn)品思考等等,同時也非常歡迎大家加我微信「java_front」一起交流學(xué)習(xí)
