看完這篇,code review 誰敢噴你代碼寫的爛?懟回去!
我將常用的軟件設計模式,做了彙總,目錄如下:
(考慮到內容篇幅較大,爲了便於大家閱讀,將軟件設計模式系列(共 23 個)拆分成四篇文章,每篇文章講解六個設計模式,採用不同的顏色區分,便於快速消化記憶)
本文,主要講解模板模式
、策略模式
、狀態模式
、觀察者模式
、訪問者模式
、備忘錄模式
1、模板模式
定義:
定義一個操作中的算法的骨架,將一些步驟延遲到子類中。模板模式使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。
優點:1、封裝不變部分,擴展可變部分。2、提取公共代碼,便於維護。3、行爲由父類控制,子類實現。缺點:每一個不同的實現都需要一個子類來實現,導致類的個數增加,維護成本高。
核心思路:
-
AbstractTemplate:定義一個完整的框架,方法的調用順序已經確定,但會定義一些抽象的方法留給子類去實現
-
AHandler:具體的業務子類,實現
AbstractTemplate
中定義的抽象方法,從而形成一個完整的流程邏輯
代碼示例:
public TradeFlowActionResult execute(TradeFlowActionParam param, Map context) throws ServiceException {
try { // 業務邏輯校驗
this.validateBusinessLogic(param, context);
} catch (ServiceException ex) {
sendGoodsLog.info("SendGoodsAction->validateBusinessLogic got exception. param is " + param, ex);
throw ex;
} catch (RuntimeException ex) {
sendGoodsLog.info("SendGoodsAction->validateBusinessLogic got runtime exception. param is " + param, ex);
throw ex;
}
try {
// 賣家發貨業務邏輯
this.sendGoods(param, context);
} catch (ServiceException ex) {
sendGoodsLog.info("SendGoodsAction->sendGoods got exception. param is " + param, ex);
throw ex;
} catch (RuntimeException ex) {
sendGoodsLog.info("SendGoodsAction->sendGoods got runtime exception. param is " + param, ex);
throw ex;
}
try {
// 補充業務(結果不影響核心業務)
this.addition(param, context);
} catch (ServiceException ex) {
sendGoodsLog.info("SendGoodsAction->addition got exception. param is " + param, ex);
throw ex;
} catch (RuntimeException ex) {
sendGoodsLog.info("SendGoodsAction->addition got runtime exception. param is " + param, ex);
throw ex;
}
// 處理結果
return null;
}
上面提到的三個抽象方法(業務邏輯校驗、賣家發貨業務邏輯、補充業務)都是在子類中實現的。即控制了主流程結構,又不失靈活性,可以讓使用者在其基礎上定製開發。如果有新的業務玩法進來,原來的流程滿足不了需求,我們可以基於模板類編寫新的子類。
適用場景:
-
希望控制算法的主流程,不能隨意變更框架,但又想保留子類業務的個性擴展。
-
去除重複代碼。保留父類通用的代碼邏輯,讓子類不再需要重複處理公用邏輯,只關注特定邏輯,起到去除子類中重複代碼的目的。
-
案例很多,比如 Jenkins 的拉取代碼、編譯、打包、發佈、部署的流程作爲一個通用的流程,不同系統(java、python、nodejs 等)可以根據自身的需求開發,定製自己的持續發佈流程。
2、策略模式
定義:
定義一系列算法,並將每種算法分別放入獨立的類中,以使算法的對象能夠相互替換。
由客戶端自己決定在什麼樣的情況下使用哪些具體的策略。
核心思路:
-
上下文信息類(Context):使用不同的策略環境,根據自身的條件選擇不同的策略實現類來完成所需要的操作。他持有一個策略實例的引用。
-
抽象策略類(Strategy):抽象策略,定義每個策略都要實現的方法
-
具體策略類(A Realize):負責實現抽象策略中定義的策略方法。
代碼示例:
/**
* @author 微信公衆號:微觀技術
*/
public interface PromotionStrategy {
// 活動類型
String promotionType();
// 活動優惠
int recommand(String productId);
}
public class FullSendPromotion implements PromotionStrategy {
@Override
public String promotionType() {
return "FullSend";
}
@Override
public int recommand(String productId) {
System.out.println("參加滿送活動");
return 0;
}
}
public class FullReducePromotion implements PromotionStrategy {
@Override
public String promotionType() {
return "FullReduce";
}
@Override
public int recommand(String productId) {
System.out.println("參加滿減活動");
return 0;
}
}
@author 微信公衆號:微觀技術
public class Context {
private static List<PromotionStrategy> promotionStrategyList = new ArrayList<>();
static {
promotionStrategyList.add(new FullReducePromotion());
promotionStrategyList.add(new FullSendPromotion());
}
public void recommand(String promotionType, String productId) {
PromotionStrategy promotionStrategy = null;
// 找到對應的策略類
for (PromotionStrategy temp : promotionStrategyList) {
if (temp.promotionType().equals(promotionType)) {
promotionStrategy = temp;
}
}
// 策略子類調用
promotionStrategy.recommand(productId);
}
首先,定義活動策略的接口類PromotionStrategy
,定義接口方法,每一種具體的策略算法都要實現該接口,FullSendPromotion
和FullReducePromotion
。
Context
負責存儲和使用策略,彙集了所有的策略子類。並根據傳入的參數,匹配到具體的策略子類,然後調用recommand
方法,處理具體的業務。
使用策略的好處:
-
提升代碼的可維護性。不同策略類隔離,互不影響。每一次新增策略時都通過新增類來進行隔離,避免了 if-else 超大的複雜類
-
動態快速地替換更多的算法。由於調度策略與算法實現分離,且接口規範固定,我們可以靈活的調整選擇不同的策略子類。
適用場景:
-
壓縮文件,提供了 gzip、zip 、rar 等格式,由客戶端自己選擇哪一種壓縮策略。不同策略可以相互替換。
-
營銷活動,根據策略路由選擇不同的活動玩法,不同的營銷活動隔離,滿足開閉原則。
-
選擇權交給了客戶端,適合那些經常調整策略的 to C 業務,靈活性高。
3、狀態模式
定義:
一種行爲設計模式,讓你能在一個對象的內部狀態變化時改變其行爲,使其看上去就像改變了自身所屬的類一樣。
通過定義一系列狀態的變化來控制行爲的變化。以電商爲例,用戶的訂單會經歷以下這些狀態:已下單、已付款、已發貨、派送中、待取件、已簽收、交易成功、交易關閉等狀態。
核心思路:
-
上下文信息類 (OrderContext):存儲
當前狀態
的類,對外提供更新狀態的方法。 -
抽象狀態類(OrderState):可以是一個接口或抽象類,用於聲明狀態更新時執行哪些操作
-
具體狀態類(MakeOrderState、PayOrderState、ReceiveGoodOrderState):實現抽象狀態類定義的方法,根據具體的場景來指定對應狀態改變後的代碼實現邏輯。
代碼示例:
/**
* @author 微信公衆號:微觀技術
* 訂單狀態,接口定義(擴展實現若干不同狀態的子類)
*/
public interface OrderState {
void handle(OrderContext context);
}
// 下單
public class MakeOrderState implements OrderState {
public static MakeOrderState instance = new MakeOrderState();
@Override
public void handle(OrderContext context) {
System.out.println("1、創建訂單");
context.setCurrentOrderState(PayOrderState.instance);
}
}
// 付款、確認收貨,實現類相似,這裏省略。。。。
// 訂單上下文
public class OrderContext {
private OrderState currentOrderState;
public OrderContext(OrderState currentOrderState) {
if (currentOrderState == null) {
this.currentOrderState = new MakeOrderState();
} else {
this.currentOrderState = currentOrderState;
}
}
public void setCurrentOrderState(OrderState currentOrderState) {
this.currentOrderState = currentOrderState;
}
public void execute() {
currentOrderState.handle(this);
}
}
運行結果:
1、創建訂單
2、支付寶付款
3、確認收到貨物
狀態模式設計的核心點在於找到合適的抽象狀態以及狀態之間的轉移關係,通過改變狀態來達到改變行爲的目的。
適用場景:
-
業務需要根據狀態的變化,進行不同的操作。比如:電商下單的全流程
-
不希望有大量的
if-else
代碼堆在一起,希望不同的狀態處理邏輯隔離,遵守開閉原則
4、觀察者模式
定義:
也稱 發佈 - 訂閱模式,是一種通知機制,當一個對象改變狀態時,它的所有依賴項都會自動得到通知和更新。讓發送通知的一方(被觀察者)和接收通知的一方(觀察者,支持多個)能彼此分離,互不影響,該模式在軟件開發中非常流行。
像我們常見的Kafka
、RocketMQ
等消息中間件都是採用這種架構模式,還有Spring
的 ApplicationEvent
異步事件驅動,有很好的低耦合特性。
類似其他叫法:
-
發佈者 --- 訂閱者
-
生產者 --- 消費者
-
事件發佈 --- 事件監聽
核心思路:
-
發佈者(Publisher):定義一系列接口,用來管理和觸發訂閱者
-
具體發佈者(PublisherImpl):具體實現類,實現
Publisher
接口定義的方法 -
訂閱者(Observer):觀察者接口,當發佈者發佈消息或事件時,會通知到訂閱者進行處理。
-
具體訂閱者(WeixinObserver、EmailObserver):
Observer
的子類,用來處理具體的業務邏輯
代碼示例:
/**
* @author 微信公衆號:微觀技術
* <p>
* 被觀察者
*/
public interface Publisher {
void add(Observer observer);
void remove(Observer observer);
void notify(Object object);
}
public class PublisherImpl implements Publisher {
private List<Observer> observerList = new ArrayList<>();
@Override
public void add(Observer observer) {
observerList.add(observer);
}
@Override
public void remove(Observer observer) {
observerList.remove(observer);
}
@Override
public void notify(Object object) {
System.out.println("創建訂單,併發送通知事件");
observerList.stream().forEach(t -> t.handle());
}
}
// 觀察者接口
public interface Observer {
public void handle();
}
// 觀察者子類
public class EmailObserver implements Observer {
@Override
public void handle() {
System.out.println("發送郵件通知!");
}
}
觀察者模式的核心精髓:被觀察者定義了一個通知列表,收集了所有的觀察者對象,當被觀察者
需要發出通知時,就會通知這個列表的所有觀察者
。
適用場景:
-
當一個對象狀態的改變需要改變其他對象時。比如:訂單支付成功後,需要通知扣減賬戶餘額
-
一個對象發生改變時只想要發送通知,而不需要知道接收者是誰。比如:微博 feed 流,粉絲能拉到最新微博
-
代碼的擴展性強,如果需要新增一個
觀察者
業務處理,只需新增一個子類觀察者
,並注入到被觀察者
的通知列表即可,代碼的耦合性非常低。
5、訪問者模式
定義:
訪問者模式是一種將操作與對象結構分離的軟件設計模式,允許在運行時將一個或多個操作應用於一組對象。
核心思路:
-
行爲接口(RouterVisitor):定義對每個 Element 訪問的行爲,方法參數就是被訪問的元素,它的方法個數理論上與元素的個數是一樣的。
-
行爲接口實現類(LinuxRouterVisitor、WindowRouterVisitor):它需要給出對每一個元素類訪問時所產生的具體行爲。
-
元素接口(RouterElement):定義一個可以獲取訪問操作的接口,使客戶端對象能夠 “訪問” 的入口點。
-
元素接口實現類(JhjRouterElement、LyqRouterElement):將訪問者
RouterVisitor
傳遞給此對象作爲參數。
代碼示例:
/**
* @author 微信公衆號:微觀技術
* 定義行爲動作
*/
public interface RouterVisitor {
// 交換機,發送數據
void sendData(JhjRouterElement jhjRouterElement);
// 路由器,發送數據
void sendData(LyqRouterElement lyqRouterElement);
}
public class LinuxRouterVisitor implements RouterVisitor {
@Override
public void sendData(JhjRouterElement jhjRouterElement) {
System.out.println("Linux 環境下,交換機發送數據");
}
@Override
public void sendData(LyqRouterElement lyqRouterElement) {
System.out.println("Linux 環境下,路由器發送數據");
}
}
public class WindowRouterVisitor implements RouterVisitor {
// 省略。。。
}
/**
* @author 微信公衆號:微觀技術
* <p>
* 路由元素
*/
public interface RouterElement {
// 發送數據
void sendData(RouterVisitor routerVisitor);
}
public class JhjRouterElement implements RouterElement {
@Override
public void sendData(RouterVisitor routerVisitor) {
System.out.println("交換機開始工作。。。");
routerVisitor.sendData(this);
}
}
public class LyqRouterElement implements RouterElement {
// 省略。。。
}
適用場景:
-
動態綁定不同的對象和對象操作
-
通過行爲與對象結構的分離,實現對象的職責分離,提高代碼複用性
6、備忘錄模式
定義:
也叫快照模式,用來存儲另外一個對象內部狀態的快照,便於以後可以恢復。
核心思路:
-
原始對象(Originator):除了創建自身所需要的屬性和業務邏輯外,還通過提供方法
bak()
和restore(memento)
來保存和恢復對象副本。 -
備忘錄(Memento):用於保存原始對象的所有屬性狀態,以便在未來進行恢復操作。
代碼示例:
/**
* @author 微信公衆號:微觀技術
* 原始對象
*/
@Data
public class Originator {
private Long id;
private String productName;
private String picture;
// 創建快照
public Memento bak() {
return new Memento(id, productName, picture);
}
//恢復
public void restore(Memento m) {
this.id = m.getId();
this.productName = m.getProductName();
this.picture = m.getPicture();
}
}
// 快照
@Data
@AllArgsConstructor
public class Memento {
private Long id;
private String productName;
private String picture;
}
適用場景:
-
在線編輯器,不小心關閉瀏覽器,重新打開頁面,可以從草稿箱恢復之前內容
-
不希望外界直接訪問對象的內部狀態,比如:物流包裹
-
另外像操作系統自動備份,數據庫的
SAVEPOINT
寫在最後
設計模式很多人都學習過,但項目實戰時總是暈暈乎乎,原因在於沒有了解其核心是什麼,底層邏輯是什麼,《設計模式:可複用面向對象的基礎》有講過,
在設計中思考什麼應該變化,並封裝會發生變化的概念。
軟件架構的精髓:找到變化,封裝變化。
業務千變萬化,沒有固定的編碼答案,千萬不要硬套設計模式。無論選擇哪一種設計模式,儘量要能滿足SOLID
原則,自我 review 是否滿足業務的持續擴展性。有句話說的好,“不論白貓黑貓,能抓老鼠就是好貓。”
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/epk2QDWZswz0VOsz9ACeug