多圖詳解:七種具體方法增強代碼可擴展性
JAVA前線?
歡迎大家關(guān)注公眾號「JAVA前線」查看更多精彩分享,主要內(nèi)容包括源碼分析、實際應(yīng)用、架構(gòu)思維、職場分享、產(chǎn)品思考等等,同時也非常歡迎大家加我微信「java_front」一起交流學(xué)習(xí)
1 六大原則
在設(shè)計模式中有六大設(shè)計原則:
單一職責(zé)原則:一次做一件事
里式替換原則:子類擴展父類
依賴倒置原則:面向接口編程
接口隔離原則:高內(nèi)聚低耦合
迪米特法則:最少知道原則
開閉原則:關(guān)閉修改,開放新增
我認(rèn)為在這六個原則中開閉原則最為重要,開閉原則是可擴展性的重要基石。
第一個原因是需求變化時應(yīng)該通過新增而不是修改已有代碼實現(xiàn),這樣保證了代碼穩(wěn)定性,避免牽一發(fā)而動全身。
第二個原因是可以事先定義代碼框架,擴展也是根據(jù)框架擴展,體現(xiàn)了用抽象構(gòu)建框架,用實現(xiàn)擴展細(xì)節(jié),保證了穩(wěn)定性也保證了靈活性。
第三個原因是其它五個原則雖然側(cè)重點各有不同,但是都可以包含于開閉原則。
第四個原因是標(biāo)準(zhǔn)二十三種設(shè)計模式最終都是在遵循開閉原則。
既然開閉原則如此重要,我們應(yīng)該怎么在系統(tǒng)設(shè)計時遵循開閉原則呢?
2 數(shù)據(jù)庫維度
2.1 設(shè)計類型字段
我們可以在數(shù)據(jù)表設(shè)計時新增兩列:biz_type、biz_sub_type,這兩個列即使目前用不上,可以先設(shè)置一個默認(rèn)值。
biz_type可以指業(yè)務(wù)類型,業(yè)務(wù)渠道、租戶等等,歸根結(jié)底是用來隔離大業(yè)務(wù)類型,假設(shè)后續(xù)業(yè)務(wù)新增了一個大業(yè)務(wù)類型,那么可以通過這個字段隔離。
biz_sub_type可以在大業(yè)務(wù)類型中細(xì)分小業(yè)務(wù),使得業(yè)務(wù)更加細(xì)化,靈活性更好。
2.2 設(shè)計擴展字段
我們可以在數(shù)據(jù)表設(shè)計時新增三列:extend1、extend2、extend3,可以先設(shè)置為空。擴展字段存儲JSON類型信息,存放一些擴展信息、附加信息、松散信息或者是之前未預(yù)估到的信息。
之所以設(shè)置三個擴展字段是為了增加隔離性,例如extend1存放訂單擴展信息,extend2存放商品擴展信息,extend3存放營銷擴展信息。
2.3 設(shè)計業(yè)務(wù)二進(jìn)制字段
2.3.1 需求背景
我們可以在數(shù)據(jù)表設(shè)計時新增業(yè)務(wù)二進(jìn)制字段,這個字段可以很大程度上擴展業(yè)務(wù)表意能力。假設(shè)在系統(tǒng)中用戶一共有三種角色:普通用戶、管理員、超級管理員,現(xiàn)在需要設(shè)計一張用戶角色表記錄這類信息。不難設(shè)計出如下方案:
| id | name | super | admin | normal |
|---|---|---|---|---|
| 101 | 用戶一 | 1 | 0 | 0 |
| 102 | 用戶二 | 0 | 1 | 0 |
| 103 | 用戶三 | 0 | 0 | 1 |
| 104 | 用戶四 | 1 | 1 | 1 |
觀察上表不難得出,用戶一具有超級管理員角色,用戶二具有管理員角色,用戶三具有普通用戶角色,用戶四同時具有三種角色。如果此時新增加一種角色呢?那么新增一個字段即可。
2.3.2 發(fā)現(xiàn)問題
按照上述一個字段表示一種角色進(jìn)行表設(shè)計功能上是沒有問題的,優(yōu)點是容易理解結(jié)構(gòu)清晰,但是我們想一想有沒有什么問題?筆者遇到過如下問題:在復(fù)雜業(yè)務(wù)環(huán)境一份數(shù)據(jù)可能會使用在不同的場景,例如上述數(shù)據(jù)存儲在MySQL數(shù)據(jù)庫,這一份數(shù)據(jù)還會被用在如下場景:
檢索數(shù)據(jù)需要同步一份到ES
業(yè)務(wù)方使用此表通過Flink計算業(yè)務(wù)指標(biāo)
業(yè)務(wù)方訂閱此表Binlog消息進(jìn)行業(yè)務(wù)處理
如果表結(jié)構(gòu)發(fā)生變化,數(shù)據(jù)源之間就要重新進(jìn)行對接,業(yè)務(wù)方也要進(jìn)行代碼修改,這樣開發(fā)成本比較非常高。有沒有辦法避免此類問題?
2.3.3 解決方案
我們可以使用位圖法,這樣同一個字段可以表示多個業(yè)務(wù)含義。首先設(shè)計如下數(shù)據(jù)表,userFlag字段暫時不填。
| id | name | user_flag |
|---|---|---|
| 101 | 用戶一 | 暫時不填 |
| 102 | 用戶二 | 暫時不填 |
| 103 | 用戶三 | 暫時不填 |
| 104 | 用戶四 | 暫時不填 |
設(shè)計位圖每一個bit表示一種角色:

使用位圖法表示如下數(shù)據(jù)表:
| id | name | super | admin | normal |
|---|---|---|---|---|
| 101 | 用戶一 | 1 | 0 | 0 |
| 102 | 用戶二 | 0 | 1 | 0 |
| 103 | 用戶三 | 0 | 0 | 1 |
| 104 | 用戶四 | 1 | 1 | 1 |
用戶一位圖如下其十進(jìn)制數(shù)值等于4:

用戶二位圖如下其十進(jìn)制數(shù)值等于2:

用戶三位圖如下其十進(jìn)制數(shù)值等于1:

用戶四位圖如下其十進(jìn)制數(shù)值等于7:

根據(jù)上述分析可以填寫數(shù)據(jù)表第三列:
| id | name | user_flag |
|---|---|---|
| 101 | 用戶一 | 4 |
| 102 | 用戶二 | 2 |
| 103 | 用戶三 | 1 |
| 104 | 用戶四 | 7 |
2.3.4 代碼實例
定義枚舉時不要直接定義為1、2、4這類數(shù)字,而應(yīng)該采用位移方式進(jìn)行定義,這樣使用者可以明白設(shè)計者的意圖。
/**
?*?用戶角色枚舉
?*
?*?@author?微信公眾號「JAVA前線」
?*
?*/
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;
????}
}
假設(shè)用戶已經(jīng)具有普通用戶角色,我們需要為其增加管理員角色,這就是新增角色,與之對應(yīng)還有刪除角色和查詢角色,這些操作需要用到為位運算,詳見代碼注釋。
/**
?*?用戶角色枚舉
?*
?*?@author?微信公眾號「JAVA前線」
?*
?*/
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));
????}
}
假設(shè)在運營后臺查詢界面中,需要查詢具有普通用戶角色的用戶數(shù)據(jù),可以使用如下SQL語句:
select?*?from?user_role?where?(user_flag?&?1)?=?user_flag;
select?*?from?user_role?where?(user_flag?&?b'0001')?=?user_flag;
我們也可以使用MyBatis語句:
<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 接口維度
3.1 設(shè)計類型入?yún)?/span>
接口輸入?yún)?shù)一定要設(shè)計類型入?yún)?,可以與數(shù)據(jù)庫biz_type、biz_sub_type相對應(yīng),也可以自定義類型最終翻譯與數(shù)據(jù)庫類型相對應(yīng)。
如果一開始不用區(qū)分類型,可以先設(shè)置為默認(rèn)值,但是其核心思想是一定要可以通過類型對輸入?yún)?shù)進(jìn)行區(qū)分。
public?class?OrderDTO?{
????private?Integer?bizType;
????private?Integer?bizSubType;
????private?Long?amount;
????private?String?goodsId;
}
public?Response?createOrder(OrderDTO?order) ;
3.2 設(shè)計松散入?yún)?/span>
接口輸入?yún)?shù)可以設(shè)計Map類型松散參數(shù),松散參數(shù)缺點是表意能力弱,優(yōu)點是靈活性強,可以以較低成本新增參數(shù)。
public?class?OrderDTO?{
????private?Integer?bizType;
????private?Integer?bizSubType;
????private?Long?amount;
????private?String?goodsId;
????private?Map?params;
}
public?Response?createOrder(OrderDTO?order) ;
3.3 設(shè)計接口版本號
對外接口需要設(shè)計版本號,對于同一個接口允許外部業(yè)務(wù)逐漸切到新版本,新老版本接口會共存一段時間,通過版本號區(qū)分。
/order/1.0/createOrder
/order/1.1/createOrder
3.4 縱橫做設(shè)計
我們分析一個下單場景:當(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);
????}
}
上述代碼從功能上完全可以實現(xiàn)業(yè)務(wù)需求,但是程序員不僅要滿足功能,還需要思考代碼的可維護(hù)性。如果新增一種訂單類型,或者新增一個訂單屬性處理邏輯,那么我們就要在上述邏輯中新增代碼,如果處理不慎就會影響原有邏輯。
如何改變平鋪直敘的思考方式?這就要為問題分析加上縱向和橫向兩個維度,我選擇使用分析矩陣方法,其中縱向表示策略,橫向表示場景:

3.4.1 縱向做隔離
縱向維度表示策略,不同策略在邏輯上和業(yè)務(wù)上應(yīng)該是隔離的,本實例包括優(yōu)惠策略、物流策略和退款策略,策略作為抽象,不同訂單類型去擴展這個抽象,策略模式非常適合這種場景。本文詳細(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);
????}
}
3.4.2 橫向做編排
橫向維度表示場景,一種訂單類型在廣義上可以認(rèn)為是一種業(yè)務(wù)場景,在場景中將獨立的策略進(jìn)行串聯(lián),模板方法設(shè)計模式適用于這種場景。
模板方法模式一般使用抽象類定義算法骨架,同時定義一些抽象方法,這些抽象方法延遲到子類實現(xiàn),這樣子類不僅遵守了算法骨架約定,也實現(xiàn)了自己的算法。既保證了規(guī)約也兼顧靈活性,這就是用抽象構(gòu)建框架,用實現(xiàn)擴展細(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ù)校驗
????????if?(null?==?orderBO)?{
????????????throw?new?RuntimeException("參數(shù)異常");
????????}
????????if?(OrderTypeEnum.isNotValid(orderBO.getType()))?{
????????????throw?new?RuntimeException("參數(shù)異常");
????????}
????????//?計算優(yōu)惠
????????discount(orderBO);
????????//?計算重量
????????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);
}
//?實現(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);
????}
}
4 文章總結(jié)
本文介紹了設(shè)計類型字段、設(shè)計擴展字段、設(shè)計業(yè)務(wù)二進(jìn)制字段、設(shè)計類型入?yún)ⅰ⒃O(shè)計松散入?yún)?、設(shè)計接口版本號、縱橫做設(shè)計這七種增加系統(tǒng)可擴展性方法,希望本文對大家有所幫助。
JAVA前線?
歡迎大家關(guān)注公眾號「JAVA前線」查看更多精彩分享,主要內(nèi)容包括源碼分析、實際應(yīng)用、架構(gòu)思維、職場分享、產(chǎn)品思考等等,同時也非常歡迎大家加我微信「java_front」一起交流學(xué)習(xí)
