萬字詳解常用設計模式

2023-06-20 06:00:59

本文是博主在工作中對常用設計模式的使用經驗總結歸納而來分享給大家。

設計模式一共有23種,本文講解涉及如下:

  1. 責任鏈模式
  2. 模板方法模式
  3. 釋出訂閱模式
  4. 策略模式

三大分類

業界一般將設計模式分為三大類:

  • 建立型模式:對類的範例化過程進行了抽象,能夠將軟體模組中物件的建立和物件的使用分離。有五種建立型模式,分別是工廠方法模式、抽象工廠模式、單例模式、建造者模式和原型模式。
  • 結構型模式:關注於物件的組成以及物件之間的依賴關係,描述如何將類或者物件結合在一起形成更大的結構,就像搭積木,可以通過簡單積木的組合形成複雜的、功能更為強大的結構。有七種結構型模式,分別是介面卡模式、裝飾者模式、代理模式、外觀模式、橋接模式、組合模式和享元模式。
  • 行為型模式:關注於物件的行為問題,是對在不同的物件之間劃分責任和演演算法的抽象化;不僅僅關注類和物件的結構,而且重點關注它們之間的相互作用。有十一種行為型模式,分別是策略模式、模板方法模式、觀察者模式、迭代器模式、責任鏈模式、命令模式、備忘錄模式、狀態模式、存取者模式、中介者模式和直譯器模式。

六大原則

設計模式遵循了六大原則,也稱為SOLID原則:

  • 單一職責原則(Single Responsibitity Principle):一個類應該只有一個發生變化的原因。不要存在多於一個導致類變更的原因,也就是說每個類應該實現單一的職責,否則就應該把類拆分。
  • 開閉原則(Open Close Principle):一個軟體實體,如類、模組和函數應該對擴充套件開放,對修改關閉。在程式需要進行拓展的時候,不能去修改原有的程式碼,而是要擴充套件原有程式碼,實現一個熱插拔的效果。
  • 里氏替換原則(Liskov Substitution Principle):所有參照基礎類別的地方必須能透明地使用其子類的物件。任何基礎類別可以出現的地方,子類一定可以出現。里氏替換原則是繼承複用的基石,只有當衍生類可以替換基礎類別,軟體單位的功能不受到影響時,基礎類別才能真正被複用,而衍生類也能夠在基礎類別的基礎上增加新的行為。
  • 介面隔離原則(Interface Segregation Principle):使用者端不應該依賴它不需要的介面。類間的依賴關係應該建立在最小的介面上。每個介面中不存在子類用不到卻必須實現的方法,如果不然,就要將介面拆分。使用多個隔離的介面,比使用單個介面要好。
  • 依賴倒置原則(Dependence Inversion Principle):上層模組不應該依賴底層模組,它們都應該依賴於抽象。抽象不應該依賴於細節,細節應該依賴於抽象。面向介面程式設計,依賴於抽象而不依賴於具體。
  • 迪米特法則(Law Of Demter):只與你的直接朋友交談,不跟「陌生人」說話。一個類對自己依賴的類知道的越少越好。無論被依賴的類多麼複雜,都應該將邏輯封裝在方法的內部,通過public方法提供給外部。這樣當被依賴的類變化時,才能最小的影響該類。

設計模式的好處

  • 可以重用設計,減少程式碼的重複,提高程式碼的可維護性。
  • 可以為設計提供共同的詞彙,方便程式設計師間的交流和理解。
  • 可以實現開閉原則,增加新的功能或者修改舊的功能不影響原有的結構。
  • 可以讓重構系統變得容易,確保開發正確的程式碼,並降低出錯的可能。
  • 可以支援變化,為重寫其他應用程式提供很好的系統架構。
  • 後期可以節省大量時間,提高開發效率。

設計模式(Design Pattern)是一套被反覆使用、多數人知曉的、經過分類編目的、程式碼設計經驗的總結。使用設計模式是為了可重用程式碼、讓程式碼更容易被他人理解、保證程式碼可靠性。

1. 責任鏈模式

概述

責任鏈模式(Chain of Responsibility Pattern)是一種行為型設計模式,它通過將請求的傳送者和接收者解耦,使多個物件都有機會處理請求。在這個模式中,請求沿著一個處理鏈依次傳遞,直到有一個物件能夠處理它為止。

責任鏈模式的核心思想是將請求的傳送者和接收者解耦,使得多個物件都有機會處理請求。在責任鏈模式中,請求會沿著一個處理鏈依次傳遞,每個處理者都有機會處理請求,如果一個處理者不能處理請求,則將請求傳遞給下一個處理者,直到有一個處理者能夠處理它。

責任鏈模式包含以下幾個角色:

  • 抽象處理者(Handler):定義了處理請求的介面,通常包含一個指向下一個處理者的參照,用於將請求傳遞給下一個處理者。
  • 具體處理者(ConcreteHandler):實現了處理請求的介面,具體處理者可以決定是否處理請求,如果不能處理,則將請求傳遞給下一個處理者。
  • 使用者端(Client):建立處理者物件並組成責任鏈的結構,負責將請求傳送給第一個處理者。

優缺點

優點:

  • 責任鏈模式可以實現請求的傳送者和接收者之間的解耦。傳送者只需要將請求傳送給第一個處理者,無需關心具體是哪個處理者來處理。這樣,系統的靈活性大大增強,可以隨時增加或修改處理者的順序。
  • 責任鏈模式能夠避免請求的傳送者和接收者之間的緊耦合。每個處理者只需要關心自己負責的請求型別,無需關心其他請求。這樣,系統的可維護性也得到了提升。
  • 責任鏈模式可以靈活地動態新增或刪除處理者。我們可以根據實際情況來調整責任鏈的結構,以滿足不同的業務需求。

缺點:

  • 複雜度會明顯提升,如果責任鏈過長或者處理者之間的關係複雜,可能還會導致效能下降和偵錯困難。

應用場景

責任鏈模式在許多不同的應用場景中都有廣泛的應用。下面列舉了一些常見的應用場景:

  • 請求處理鏈:當一個請求需要經過多個處理步驟或處理者進行處理時,可以使用責任鏈模式。每個處理者負責一部分邏輯,處理完後可以選擇將請求傳遞給下一個處理者,從而形成一個處理鏈。
  • 紀錄檔記錄:在紀錄檔系統中,可以使用責任鏈模式來記錄紀錄檔。不同的處理者可以負責不同級別的紀錄檔記錄,例如,一個處理者負責記錄錯誤紀錄檔,另一個處理者負責記錄偵錯紀錄檔,然後按照鏈式結構傳遞紀錄檔。
  • 身份驗證和許可權檢查:在身份驗證和許可權檢查系統中,可以使用責任鏈模式來驗證使用者的身份和許可權。每個處理者可以檢查特定的條件,例如使用者名稱和密碼的正確性、賬戶是否鎖定等。如果一個處理者無法通過驗證,可以將請求傳遞給下一個處理者。
  • 資料過濾和轉換:在資料處理過程中,可以使用責任鏈模式來進行資料過濾和轉換。每個處理者可以根據特定的條件過濾資料或對資料進行轉換,然後將處理後的資料傳遞給下一個處理者。
  • 錯誤處理和例外處理:在錯誤處理和例外處理系統中,可以使用責任鏈模式來處理錯誤和異常。不同的處理者可以處理不同型別的錯誤或異常,並根據需要將錯誤或異常傳遞給下一個處理者進行進一步處理或記錄。

Java 程式碼範例

Java 中實現責任鏈模式有多種方式,包括基於介面、基於抽象類、基於註解等。下面將詳細介紹基於介面的常見實現方式。

基於介面的實現方式是通過定義一個處理請求的介面,每個處理者實現這個介面,並在自己的實現中決定是否處理請求和傳遞請求給下一個處理者。

首先,我們定義一個處理請求的介面 Handler 以及請求入參 Request

public interface Handler {
    void handleRequest(Request request);
}

public class Request {
    private String type;
    // 省略getter、setter
}

然後,我們建立3個具體的處理者類實現這個介面,在具體處理者類的實現中,首先判斷自己是否能夠處理請求,如果能夠處理,則進行處理;否則將請求傳遞給下一個處理者。程式碼如下:

public class ConcreteHandlerA implements Handler {
    private Handler successor;

    public void setSuccessor(Handler successor) {
        this.successor = successor;
    }

    public void handleRequest(Request request) {
        if (request.getType().equals("A")) {
            // 處理請求的邏輯
        } else if (successor != null) {
            successor.handleRequest(request);
        }
    }
}

public class ConcreteHandlerB implements Handler {
    private Handler successor;

    public void setSuccessor(Handler successor) {
        this.successor = successor;
    }

    public void handleRequest(Request request) {
        if (request.getType().equals("B")) {
            // 處理請求的邏輯
        } else if (successor != null) {
            successor.handleRequest(request);
        }
    }
}

public class ConcreteHandlerC implements Handler {
    private Handler successor;

    public void setSuccessor(Handler successor) {
        this.successor = successor;
    }

    public void handleRequest(Request request) {
        if (request.getType().equals("C")) {
            // 處理請求的邏輯
        } else if (successor != null) {
            successor.handleRequest(request);
        }
    }
}

接下來,我們建立一個使用者端類 Client,用於建立處理者物件並組成責任鏈的結構:

public class Client {
    public static void main(String[] args) {
        Handler handlerA = new ConcreteHandlerA();
        Handler handlerB = new ConcreteHandlerB();
        Handler handlerC = new ConcreteHandlerC();

        handlerA.setSuccessor(handlerB);
        handlerB.setSuccessor(handlerC);

        // 建立請求並行送給第一個處理者
        Request request = new Request("A");
        handlerA.handleRequest(request);
    }
}

在使用者端類中,我們建立了具體的處理者物件,並通過 setSuccessor() 方法將它們組成一個責任鏈的結構。然後,建立一個請求物件,並將請求傳送給第一個處理者。

基於介面的實現方式簡單直觀,每個處理者只需要實現一個介面即可。但是它的缺點是如果責任鏈較長,需要建立多個處理者物件,增加了系統的複雜性和資源消耗。下面基於 Spring 框架實現一個高階版的責任鏈模式。

Spring 程式碼範例

在實際開發中,一個請求會在多個處理器之間流轉,每個處理器都可以處理請求。

假設我們有一個 Spring 框架開發的訂單處理系統,訂單需要依次經過訂單檢查、庫存處理、支付處理。如果某個處理環節無法處理訂單,將會終止處理並返回錯誤資訊,只有每個處理器都完成了請求處理,這個訂單才演演算法下單成功。

首先,我們定義一個訂單類 Order

@Data
@AllArgsConstructor
public class orderNo {
    private String orderNumber;
    private String paymentMethod;
    private boolean stockAvailability;
    private String shippingAddress;
}

然後,我們定義一個抽象訂單處理者類 OrderHandler

public abstract class OrderHandler {
    public abstract void handleOrder(Order order);
}

接下來,我們建立具體的訂單處理者類繼承自抽象訂單處理者類,實現相應的方法,並註冊到 Spring 中,

@Component
public class CheckOrderHandler extends OrderHandler {
    public void handleOrder(Order order) {
        if (StringUtils.isBlank(order.getOrderNo())) {
            throw new RuntimeException("訂單編號不能為空");
        }
        if (order.getPrice().compareTo(BigDecimal.ONE) <= 0) {
            throw new RuntimeException("訂單金額不能小於等於0");
        }
        if (StringUtils.isBlank(order.getShippingAddress())) {
            throw new RuntimeException("收貨地址不能為空");
        }
        System.out.println("訂單引數檢驗通過");
    }
}

@Component
public class StockHandler extends OrderHandler {
    public void handleOrder(Order order) {
        if (!order.isStockAvailability()) {
            throw new RuntimeException("訂單庫存不足");
        }
        System.out.println("庫存扣減成功");
    }
}

@Component
public class AliPaymentHandler extends OrderHandler {
    public void handleOrder(Order order) {
        if (!order.getPaymentMethod().equals("支付寶")) {
            throw new RuntimeException("不支援支付寶以外的支付方式");
        }
        System.out.println("支付寶預下單成功");
    }
}

在具體訂單處理者類的實現中,CheckOrderHandler 負責做訂單引數檢查、StockHandler 負責做庫存扣減、AliPaymentHandler 負責做預下單,每個處理者的邏輯都是相互獨立各不不干擾。


最後,我們建立一個訂單生產鏈條 BuildOrderChain ,用於組成責任鏈的鏈條處理結構:

@Component
public class BuildOrderChain {

    @Autowired
    private AliPaymentHandler aliPaymentHandler;

    @Autowired
    private CheckOrderHandler checkOrderHandler;

    @Autowired
    private StockHandler stockHandler;

    List<OrderHandler> list = new ArrayList<>();

    @PostConstruct
    public void init() {
        // 1. 檢查訂單引數
        list.add(checkOrderHandler);
        // 2. 扣減庫存
        list.add(stockHandler);
        // 3. 支付寶預下單
        list.add(aliPaymentHandler);
    }

    public void doFilter(Order order) {
        for (OrderHandler orderHandler : this.list) {
            orderHandler.handleOrder(order);
        }
    }
}

訂單生產鏈條 BuildOrderChain 類中,我們通過 @PostConstruct 註解下的 init() 初始化方法,將具體的訂單處理者按程式碼順序組成一個責任鏈的結構。然後通過 doFilter(order) 方法遍歷處理者集合依次處理。

執行程式碼:

@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class OrderChainTest {
    @Autowired
    private BuildOrderChain buildOrderChain;

    @Test
    public void test() {
        Order order = new Order("123456", "支付寶",
                      true, "長沙", new BigDecimal("100"));
        buildOrderChain.doFilter(order);
    }

}

-------------------------------
訂單引數檢驗通過
庫存扣減成功
支付寶預下單成功

可以看到訂單依次經過校驗處理器、庫存處理器和支付處理器進行處理,直到最後完成整個訂單的處理。

在舉個例子,假如我們的訂單針對的是虛擬不限庫存商品,我們不需要進行庫存扣減,那我們可以直接新建 VirtualGoodsOrderChain 虛擬商品訂單生產鏈條類,程式碼如下,

@Component
public class VirtualGoodsOrderChain {
    @Autowired
    private AliPaymentHandler aliPaymentHandler;

    @Autowired
    private CheckOrderHandler checkOrderHandler;

    List<OrderHandler> list = new ArrayList<>();

    @PostConstruct
    public void init() {
        // 1. 檢查訂單引數
        list.add(checkOrderHandler);
        // 2 支付寶預下單
        list.add(aliPaymentHandler);
    }

    public void doFilter(Order order) {
        for (OrderHandler orderHandler : this.list) {
            orderHandler.handleOrder(order);
        }
    }
}

執行程式碼:

@Test
public void virtualOrderTest() {
    Order order = new Order("123456", "支付寶", true, "長沙", new BigDecimal("100"));
    virtualGoodsOrderChain.doFilter(order);
}

-------------------------------------------
訂單引數檢驗通過
支付寶預下單成功

總的來說,責任鏈模式適用於存在多個處理步驟、每個處理步驟具有獨立邏輯或條件、需要靈活組合和擴充套件的場景。通過責任鏈模式,可以將複雜的處理邏輯拆分為多個獨立的處理步驟,並且可以動態地組合和調整處理步驟的順序,從而提高系統的靈活性和可維護性。希望本文能夠幫助讀者理解和應用責任鏈模式,提升軟體設計和開發的能力。

2. 模板方法模式

概述

模板方法模式是一種行為型設計模式,它定義一個操作(模板方法)的基本組合與控制流程,將一些步驟(抽象方法)推遲到子類中,在使用時呼叫不同的子類,就可以達到不改變一個操作的基本流程情況下,即可修改其中的某些特定步驟。這種設計方式將特定步驟的具體實現與操作流程分離開來,實現了程式碼的複用和擴充套件,從而提高程式碼質量和可維護性。

模板方法模式包含以下:

  • 抽象類:負責定義模板方法、基本方法、抽象方法。
  • 模板方法:在抽象類中定義的流程操作集合,裡面有一系列流程操作和條件控制,包含基本方法和抽象方法。
  • 基本方法:在抽象類中已經實現了的方法。
  • 抽象方法:在抽象類中還沒有實現的方法。
  • 具體子類:實現抽象類中所定義的抽象方法,也就是實現特定步驟。

優缺點

  1. 封裝不變部分,擴充套件可變部分。模板方法模式將可變的部分封裝在抽象方法中,不變的部分封裝在基本方法中。這使得子類可以根據需求對可變部分進行擴充套件,而不變部分仍然保持不變。
  2. 避免重複程式碼,抽象類中包含的基本方法可以避免子類重複實現相同的程式碼邏輯。
  3. 更好的擴充套件性,由於具體實現由子類來完成,因此可以方便地擴充套件新的功能或變更實現方式,同時不影響模板方法本身。

模板方法模式的缺點:

  1. 類多,由於每個演演算法都需要一個抽象類和具體子類來實現,因此在操作流程比較多時可能導致類的數量急劇增加,從而導致程式碼的複雜性提高。
  2. 關聯性高,模板方法與子類實現的抽象方法緊密相關,如果該模板方法需要修改,可能會涉及到多個子類的修改。

應用場景

  1. 開發框架,通常框架會定義一些通用的模板,子類可以根據自身的特定需求來細化模板的實現細節,比如 Spring 中的 JdbcTemplate、RestTemplate、RabbitTemplate、KafkaTemplate 等。
  2. 業務邏輯,我們可以針對業務流程做一些拆解,將特定步驟改為子類實現。比如傳送驗證碼的流程,在傳送驗證碼時需要選擇不同廠商來傳送驗證碼,但是我們傳送的驗證碼前的檢查、驗證碼生成、儲存驗證碼邏輯都是一樣的。

Java 程式碼範例

如上,我們用一個簡單的傳送簡訊程式碼來做模板方法模式的範例:

定義一個傳送簡訊模板

/**
 * 傳送簡訊模板
 */
public abstract class SmsTemplate {

    /**
     * 傳送方法
     *
     * @param mobile 手機號
     */
    public void send(String mobile) throws Exception {
        System.out.println("檢查使用者一分鐘內是否傳送過簡訊,
                    mobile:" + mobile);
        if (checkUserReceiveInOneMinute(mobile)) {
            throw new Exception("請等待1分鐘後重試");
        }
        String code = genCode();
        if (manufacturer(mobile, code)) {
            System.out.println("簡訊廠商傳送簡訊成功,
                    mobile:" + mobile + ",code=" + code);
            save2redis(mobile, code);
        }
    }

    /**
     * 模板方法,由不同的廠商來實現傳送簡訊到手機上
     * @return
     */
    abstract boolean manufacturer(String mobile, String code);

    /**
     * 檢查1分鐘內該手機號是否接收過驗證碼,1分鐘內接收過就不能在傳送驗證碼
     * @param mobile
     * @return
     */
    public boolean checkUserReceiveInOneMinute(String mobile) {
        return ...;
    }


    /**
     * 生成6位驗證碼
     * @return
     */
    public String genCode() {
        return "123456";
    }

    /**
     * 將手機號+驗證碼存進redis中,給登入介面做校驗用
     * @param mobile
     * @param code
     */
    public void save2redis(String mobile, String code) {
        ...
    }
}

新增兩個不同廠商實現的子類

/**
 * 阿里雲簡訊傳送
 */
public class AliyunSmsSend extends SmsTemplate{
    @Override
    boolean manufacturer(String mobile, String code) {
        System.out.println("讀取阿里雲簡訊設定");
        System.out.println("建立阿里雲傳送簡訊使用者端");
        System.out.println("阿里雲傳送簡訊成功");
        return true;
    }
}

/**
 * 騰訊雲簡訊傳送
 */
public class TencentSmsSend extends SmsTemplate {
    @Override
    boolean manufacturer(String mobile, String code) {
        System.out.println("讀取騰訊雲簡訊設定");
        System.out.println("建立騰訊雲傳送簡訊使用者端");
        System.out.println("騰訊雲傳送簡訊成功");
        return true;
    }
}

在 Java 程式中進行呼叫

public class Main {
    public static void main(String[] args) throws Exception {
        SmsTemplate smsTemplate1 = new AliyunSmsSend();
        smsTemplate1.send("13333333333");
        System.out.println("---------------------------");
        SmsTemplate smsTemplate2 = new TencentSmsSend();
        smsTemplate2.send("13333333333");
    }
}

輸出如下:

檢查使用者一分鐘內是否傳送過簡訊,mobile:13333333333
讀取阿里雲簡訊設定
建立阿里雲傳送簡訊使用者端
阿里雲傳送簡訊成功
簡訊廠商傳送簡訊成功,mobile:13333333333,code=123456
---------------------------
檢查使用者一分鐘內是否傳送過簡訊,mobile:13333333333
讀取騰訊雲簡訊設定
建立騰訊雲傳送簡訊使用者端
騰訊雲傳送簡訊成功
簡訊廠商傳送簡訊成功,mobile:13333333333,code=123456

我們來看看模板方法模式的組成:

  • 抽象類 SmsTemplate 中定義了傳送簡訊的基本流程操作
    1. 傳送前檢查使用者1分鐘內是否接收過簡訊,不變部分。
    2. 生成驗證碼,不變部分。
    3. 發遠驗證碼到使用者手機,這個抽象方法由不同子類實現,可變部分
    4. 傳送成功則儲存到 redis 中,不變部分。
  • 具體子類 AliyunSmsSend、TencentSmsSend 繼承抽象類,實現抽象方法 manufacturer(String mobile, String code),定義流程中的可變部分。
  • 呼叫模板方法 send(mobile) ,在模板方法中完成了基本流程組合與條件控制。

Spring 程式碼範例

Spring 中實現模板方法模式,是非常簡單的,我們只需要對上述的 Java 程式碼範例的 AliyunSmsSend 類稍作改造,加上 @Component 註解就行,

/**
 * 阿里雲簡訊傳送
 */
@Component
public class AliyunSmsSend extends SmsTemplate{
    @Override
    boolean manufacturer(String mobile, String code) {
        IUserService userService = SpringUtil.getBean(IUserService.class);
        System.out.println("讀取阿里雲簡訊設定");
        System.out.println("建立阿里雲傳送簡訊使用者端");
        System.out.println("阿里雲傳送簡訊成功");
        return true;
    }
}

如果在 AliyunSmsSend 類中需要注入其他 bean,通過 cn.hutool.extra.spring.SpringUtil.getBean(...) 方法獲取對應 bean 就行。

使用 Lambda 表示式

在Java8 中,還可以使用函數表示式來替換抽象方法,程式碼如下,

/**
 * 傳送簡訊模板
 */
public class SmsTemplateLambda {
    /**
     * 傳送簡訊
     * @param mobile 手機號
     * @param biFunction
     * @throws Exception
     */
    public void send(String mobile,
        BiFunction<String, String, Boolean> biFunction) throws Exception {
        System.out.println("檢查使用者一分鐘內是否傳送過簡訊,mobile:" + mobile);
        if (checkUserReceiveInOneMinute(mobile)) {
            throw new Exception("請等待1分鐘後重試");
        }
        String code = genCode();
        if (biFunction.apply(mobile, code)) {
            System.out.println("簡訊廠商傳送簡訊成功,mobile:" 
                + mobile + ",code=" + code);
            save2redis(mobile, code);
        }
    }
    ...
}

通過 BiFunction 函數,將不同廠商傳送簡訊到使用者手機的程式碼在 send(mobile) 方法中分離處理。


呼叫方法如下:

    public static void main(String[] args) throws Exception {
        SmsTemplateLambda smsTemplateLambda = new SmsTemplateLambda();
        smsTemplateLambda.send("1333333333", (s, s2) -> {
            System.out.println("讀取阿里雲簡訊設定");
            System.out.println("建立阿里雲傳送簡訊使用者端");
            System.out.println("阿里雲傳送簡訊成功");
            return true;
        });

        smsTemplateLambda.send("1333333333", (s, s2) -> {
            System.out.println("讀取騰訊雲簡訊設定");
            System.out.println("建立騰訊雲傳送簡訊使用者端");
            System.out.println("騰訊雲傳送簡訊成功");
            return true;
        });
    }

可以看到,我們可以只在呼叫 SmsTemplateLambda 類的 send(mobile) 方法時,才實現不同廠商傳送簡訊到手機的具體邏輯。好處就是每增加一個模板方法時,不用增加具體的子類實現,減少類的建立與降低子類的實現成本。

模板方法模式通過定義一個流程基本操作也就是模板方法,將具體的實現步驟推遲到子類中,使得子類可以靈活地實現可變的行為,這是模板方法模式的核心思想與價值所在。

3. 訂閱釋出模式

概述

訂閱釋出模式(Publish-Subscribe Pattern)是一種行之有效的解耦框架與業務邏輯的方式,也是一種常見的觀察者設計模式,它被廣泛應用於事件驅動架構中。

觀察者模式的各角色定義如下。

  • Subject(目標主題):被觀察的目標主題的介面抽象,維護觀察者物件列表,並定義註冊方法register()(訂閱)與通知方法notify()(釋出)。對應本章例程中的商店類Shop。
  • ConcreteSubject(主題實現):被觀察的目標主題的具體實現類,持有一個屬性狀態State,可以有多種實現。對應本章例程中的商店類Shop。
  • Observer(觀察者):觀察者的介面抽象,定義響應方法update()。對應本章例程中的買家類Buyer。
  • ConcreteObserver(觀察者實現):觀察者的具體實現類,可以有任意多個子類實現。實現了響應方法update(),收到通知後進行自己獨特的處理。對應本章例程中的手機買家類PhoneFans、海淘買家類HandChopper

優缺點

優點:

  1. 解耦:釋出者和訂閱者不直接依賴,通過訊息代理間接通訊,實現解耦。
  2. 擴充套件性好:釋出者和訂閱者可以隨時增加或刪除,訊息類別也可以增加,程式的擴充套件性很好。
  3. 鬆散耦合:釋出者無需瞭解所有的訂閱者,只管釋出訊息;訂閱者也無需瞭解所有的釋出者,只關注自己感興趣的訊息。
  4. 增加新的訊息與訂閱者很方便:訊息中心統一管理訊息與訂閱者的對應關係,增加新訊息時,無需修改已有模組。

缺點:

  1. 複雜性提高:需要引入訊息代理和訊息分類這兩個新的元件,系統結構變得複雜。
  2. 效能開銷:使用訊息代理中轉訊息,會產生額外的效能開銷,如網路互動等。
  3. 依賴中介軟體:訊息代理的可用性會影響系統的可用性,引入了新的依賴點。
  4. 難以跟蹤資料流向:訊息在各個元件之間轉發,如果系統比較複雜,訊息流向會變得不太清晰。
  5. 不支援同步操作:釋出訂閱模式以非同步訊息通知為基礎,不適用於同步操作場景。

應用場景

  • 構建實時訊息系統:比如普通的即時聊天,群聊等功能。釋出者可以將訊息傳送到指定的頻道或者主題,訂閱者可以根據自己的興趣或者身份來訂閱不同的頻道或者主題,並及時收到訊息。
  • 實現事件驅動的系統:比如前端框架中的事件監聽和觸發,或者後端框架中的中介軟體機制。釋出者可以將事件作為訊息傳送出去,訂閱者可以根據自己的業務邏輯來訂閱不同的事件,並在事件發生時執行相應的操作。
  • 實現分散式系統中的訊息佇列:比如使用 Redis、RabbitMQ、Kafka 等中介軟體來實現生產者和消費者之間的通訊。釋出者可以將任務或者資料作為訊息傳送到佇列中,訂閱者可以從佇列中獲取訊息並進行處理。
  • 實現微信公眾號等推播服務:比如使用者可以關注不同的公眾號或者主題,並在有新內容時收到推播通知。釋出者可以將內容作為訊息傳送到指定的公眾號或者主題,訂閱者可以根據自己的喜好來訂閱不同的公眾號或者主題,並在有新內容時收到推播通知。

Java 程式碼範例

  1. 建立訂閱者介面,用於接受訊息通知。
interface Subscriber {
    void update(String message);
}
  1. 建立釋出者,用於釋出訊息。實現了增加、刪除和釋出的功能,並且維護了一個訂閱列表,
class Publisher {
    private Map<String, List<Subscriber>> subscribers = new HashMap<>();

    public void subscribe(String topic, Subscriber subscriber) {
        List<Subscriber> subscriberList = subscribers.get(topic);
        if (subscriberList == null) {
            subscriberList = new ArrayList<>();
            subscribers.put(topic, subscriberList);
        }
        subscriberList.add(subscriber);
    }

    public void unsubscribe(String topic, Subscriber subscriber) {
        List<Subscriber> subscriberList = subscribers.get(topic);
        if (subscriberList != null) {
            subscriberList.remove(subscriber);
        }
    }

    public void publish(String topic, String message) {
        List<Subscriber> subscriberList = subscribers.get(topic);
        if (subscriberList != null) {
            for (Subscriber subscriber : subscriberList) {
                subscriber.update(message);
            }
        }
    }
}
  1. 我們還實現了兩個不同的 Subscriber 實現,一個是 EmailSubscriber,另一個是 SMSSubscriber,用於接受釋出者的訊息並將其分別傳送到郵箱和手機上。
class EmailSubscriber implements Subscriber {
    private String email;

    public EmailSubscriber(String email) {
        this.email = email;
    }

    public void update(String message) {
        System.out.println("Send email to " + email + ": " + message);
    }
}

class SMSSubscriber implements Subscriber {
    private String phoneNumber;

    public SMSSubscriber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    public void update(String message) {
        System.out.println("Send SMS to " + phoneNumber + ": " + message);
    }
}

  1. 在 Main 類中,我們建立了一個 Publisher 物件,並新增了兩個 EmailSubscriber 和兩個 SMSSubscriber,分別訂閱了 news 主題的更新。我們先給這個主題傳送一條訊息,然後取消 news 主題的其中一個訂閱者,最後我們再次給 news 主題傳送一條訊息。
public class Main {
    public static void main(String[] args) {
        Publisher publisher = new Publisher();

        Subscriber emailSubscriber1 = new EmailSubscriber("[email protected]");
        Subscriber smsSubscriber1 = new SMSSubscriber("1234567890");

        publisher.subscribe("news", emailSubscriber1);
        publisher.subscribe("news", smsSubscriber1);

        publisher.publish("news", "釋出新訊息1");
        publisher.unsubscribe("news", smsSubscriber1);
        publisher.publish("news", "釋出新訊息2");
    }
}

列印輸出如下:

Send email to [email protected]: 釋出新訊息1
Send SMS to 1234567890: 釋出新訊息1
Send email to [email protected]: 釋出新訊息2

Spring 程式碼範例

Spring的訂閱釋出模式是通過釋出事件、事件監聽器和事件釋出器3個部分來完成的

這裡我們通過 newbee-mall-pro 專案中已經實現訂閱釋出模式的下單流程給大家講解,專案地址:https://github.com/wayn111/newbee-mall-pro

  1. 自定義訂單釋出事件,繼承 ApplicationEvent
public class OrderEvent extends ApplicationEvent {
  void onApplicationEvent(Object event) {
    ...
  }
}
  1. 定義訂單監聽器,實現 ApplicationListener
@Component
public class OrderListener implements ApplicationListener<OrderEvent> {
    @Override
    public void onApplicationEvent(OrderEvent event) {
    // 生成訂單、刪除購物車、扣減庫存
    ...
    }
}
  1. 下單流程,通過事件釋出器 applicationEventPublisher 釋出訂單事件,然後再訂單監聽器中處理訂單儲存邏輯。
@Resource
private ApplicationEventPublisher applicationEventPublisher;

private void saveOrder(MallUserVO mallUserVO, Long couponUserId, List<ShopCatVO> shopcatVOList, String orderNo) {
    // 訂單檢查
    ...
    // 生成訂單號
    String orderNo = NumberUtil.genOrderNo();
    // 釋出訂單事件,在事件監聽中處理下單邏輯
    applicationEventPublisher.publishEvent(new OrderEvent(orderNo, mallUserVO, couponUserId, shopcatVOList));
    // 所有操作成功後,將訂單號返回
    return orderNo;
    ...
}

通過事件監聽機制,我們將下單邏輯拆分成如下步驟:

  1. 訂單檢查
  2. 生成訂單號
  3. 釋出訂單事件,在事件監聽中處理訂單儲存邏輯
  4. 所有操作成功後,將訂單號返回

每個步驟都是各自獨立

如上的程式碼已經實現了訂閱釋出模式,成功解耦了下單邏輯。建議大家在日常開發中多加思考哪些業務流程可以適用,例如微服務專案中訂單支付成功後需要通知使用者、商品、活動等多個服務時,可以考慮使用訂閱釋出模式。解耦釋出者和訂閱者,釋出者只管釋出訊息,不需要知道有哪些訂閱者,也不需要知道訂閱者的具體實現。訂閱者只需要關注自己感興趣的訊息即可。這種鬆耦合的設計使得系統更容易擴充套件和維護。

4. 策略模式

概述

策略模式(Strategy Pattern)是一種行為型設計模式,它定義了一組同型別的演演算法,在不同的類中封裝起來,每種演演算法可以根據當前場景相互替換,從而使演演算法的變化獨立於使用它們的使用者端(即演演算法的呼叫者)。

策略模式的各角色定義如下。

  • Strategy(策略介面):定義通用的策略規範標準,包含在系統環境中並宣告策略介面標準。對應本章例程中的USB介面USB。
  • ConcreteStrategyA、ConcreteStrategyB、ConcreteStrategyC……(策略實現):實現了策略介面的策略實現類,可以有多種不同的策略實現,但都得符合策略介面定義的規範。對應本章例程中的USB鍵盤類Keyboard、USB滑鼠類Mouse、USB攝像頭類Camera。
  • Context(上下文):包含策略介面的系統環境,對外提供更換策略實現的方法setStrategy()以及執行策略的方法executeStrategy(),其本身並不關心執行的是哪種策略實現。對應本章例程中的計算機主機類Computer。

優缺點

優點:

  • 可以避免使用多重條件語句,提高程式碼的可讀性和可維護性。
  • 可以提供多種可重用的演演算法族,減少程式碼的重複。
  • 可以實現開閉原則,增加新的演演算法或者修改舊的演演算法不影響原有的結構。
  • 可以靈活地切換不同的演演算法,增加系統的靈活性。

缺點:

  • 使用者端必須知道所有的策略類,並自行決定使用哪一個策略類,這增加了使用者端的複雜度。
  • 策略模式會產生很多的策略類,增加系統的類數量。

應用場景

策略模式適用於以下場景:

  • 一個系統有許多類,它們之間的區別僅在於它們的行為,那麼使用策略模式可以動態地讓一個物件在許多行為中選擇一種行為。
  • 一個系統需要動態地在幾種演演算法中選擇一種。
  • 如果一個物件有很多的行為,如果不用恰當的模式,這些行為就只好使用多重的條件選擇語句來實現。

Java程式碼範例

假設我們有一個計算器程式,它可以根據使用者輸入的不同運運算元(+、-、*、/)來執行不同的算術運算。我們可以使用策略模式來實現這個功能,具體步驟如下:

1. 定義一個策略介面

我們首先定義一個策略介面 Strategy ,它宣告了一個 doOperation 方法,用於執行具體的運算。

// 策略介面
public interface Strategy {
    // 執行運算
    public int doOperation(int num1, int num2);
}

2. 實現具體的策略類

然後我們實現四個具體的策略類,分別是 AddStrategy 、SubtractStrategy 、MultiplyStrategy 和 DivideStrategy ,它們都實現了 Strategy 介面,並重寫了 doOperation 方法。

// 加法策略
public class AddStrategy implements Strategy {
    @Override
    public int doOperation(int num1, int num2) {
        return num1 + num2;
    }
}

// 減法策略
public class SubtractStrategy implements Strategy {
    @Override
    public int doOperation(int num1, int num2) {
        return num1 - num2;
    }
}

// 乘法策略
public class MultiplyStrategy implements Strategy {
    @Override
    public int doOperation(int num1, int num2) {
        return num1 * num2;
    }
}

// 除法策略
public class DivideStrategy implements Strategy {
    @Override
    public int doOperation(int num1, int num2) {
        if (num2 == 0) {
            throw new IllegalArgumentException("除數不能為0");
        }
        return num1 / num2;
    }
}

3. 定義一個上下文類

接下來我們定義一個上下文類 Context ,它持有一個 Strategy 的參照,並提供了一個構造方法和一個 executeStrategy 方法。構造方法用於傳入具體的策略物件,executeStrategy 方法用於呼叫策略物件的 doOperation 方法。

// 上下文類
public class Context {
    private Strategy strategy;

    public Context(Strategy strategy) {
        this.strategy = strategy;
    }

    public int executeStrategy(int num1, int num2) {
        return strategy.doOperation(num1, num2);
    }
}

4. 測試策略模式

最後我們編寫一個測試類,用於建立不同的策略物件和上下文物件,並根據使用者的輸入來執行不同的演演算法。

// 測試類
public class StrategyTest {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("請輸入第一個數:");
        int num1 = scanner.nextInt();
        System.out.println("請輸入運運算元(+、-、*、/):");
        String operator = scanner.next();
        System.out.println("請輸入第二個數:");
        int num2 = scanner.nextInt();
        scanner.close();

        // 根據運運算元建立不同的策略物件
        Strategy strategy;
        switch (operator) {
            case "+" -> strategy = new AddStrategy();
            case "-" -> strategy = new SubtractStrategy();
            case "*" -> strategy = new MultiplyStrategy();
            case "/" -> strategy = new DivideStrategy();
            default -> {
                System.out.println("無效的運運算元");
                return;
            }
        }

        // 建立上下文物件,並傳入策略物件
        Context context = new Context(strategy);
        // 呼叫上下文物件的方法,執行策略物件的演演算法
        int result = context.executeStrategy(num1, num2);
        // 輸出結果
        System.out.println(num1 + " " + operator + " " + num2 + " = " + result);
    }
}

執行結果:

請輸入第一個數:
1
請輸入運運算元(+、-、*、/):
+
請輸入第二個數:
1
1 + 1 = 2

Spring 程式碼範例

在 Spring 框架中,也有很多地方使用了策略模式,比如 BeanFactory 的實現類,它們都實現了一個 BeanFactory 介面,但是具體的範例化和管理 Bean 的方式不同。比如 XmlBeanFactory 是從 XML 檔案中讀取 Bean 的定義,而 AnnotationConfigApplicationContext 是從註解中讀取 Bean 的定義。

我們可以使用 Spring 的依賴注入功能,來實現策略模式,具體步驟如下:

1. 定義一個策略介面

我們還是使用上面的計算器程式作為例子,首先定義一個策略介面 Strategy ,它宣告了一個 doOperation 方法,用於執行具體的運算。

// 策略介面
public interface Strategy {
    // 執行運算
    public int doOperation(int num1, int num2);
}

2. 實現具體的策略類

然後我們實現四個具體的策略類,分別是 AddStrategy 、SubtractStrategy 、MultiplyStrategy 和 DivideStrategy ,它們都實現了 Strategy 介面,並重寫了 doOperation 方法。同時,我們給每個策略類新增一個 @Component 註解,表示它們是 Spring 容器管理的元件。

// 加法策略
@Component
public class AddStrategy implements Strategy {
    @Override
    public int doOperation(int num1, int num2) {
        return num1 + num2;
    }
}

// 減法策略
@Component
public class SubtractStrategy implements Strategy {
    @Override
    public int doOperation(int num1, int num2) {
        return num1 - num2;
    }
}

// 乘法策略
@Component
public class MultiplyStrategy implements Strategy {
    @Override
    public int doOperation(int num1, int num2) {
        return num1 * num2;
    }
}

// 除法策略
@Component
public class DivideStrategy implements Strategy {
    @Override
    public int doOperation(int num1, int num2) {
        if (num2 == 0) {
            throw new IllegalArgumentException("除數不能為0");
        }
        return num1;
    }
}

好的,我繼續寫。

3. 定義一個上下文類

接下來我們定義一個上下文類 Context ,它持有一個 Strategy 的參照,並提供了一個構造方法和一個 executeStrategy 方法。構造方法用於傳入具體的策略物件,executeStrategy 方法用於呼叫策略物件的 doOperation 方法。同時,我們給上下文類新增一個 @Component 註解,表示它也是 Spring 容器管理的元件。

// 上下文類
@Component
public class Context {
    private Strategy strategy;

    public Context(Strategy strategy) {
        this.strategy = strategy;
    }

    public int executeStrategy(int num1, int num2) {
        return strategy.doOperation(num1, num2);
    }
}

4. 使用 @Autowired 注入策略物件

為了讓 Spring 容器能夠自動注入不同的策略物件,我們需要使用 @Autowired 註解來標註 Context 類的構造方法,並使用 @Qualifier 註解來指定具體的策略類。這樣,我們就可以根據不同的運運算元來建立不同的上下文物件,而不需要手動建立策略物件。

// 上下文類
@Component
public class Context {

    @Autowired
    private List<Strategy> list;

    public Strategy getBean(Class tClass) throws Exception {
        for (Strategy strategy : list) {
            if (strategy.getClass() == tClass) {
                return strategy;
            }
        }
        throw new Exception("獲取策略失敗");
    }
}

5. 測試策略模式

最後我們編寫一個測試類,用於從 Spring 容器中獲取 Context 物件,並根據使用者的輸入來執行不同的演演算法。

// 測試類
@SpringBootTest
@RunWith(SpringRunner.class)
public class StrategyTest {

    // 從Spring容器中獲取Context物件
    @Autowired
    private Context context;

    @Test
    public void test() throws Exception {
        System.out.println("請輸入第一個數:2");
        int num1 = 2;
        System.out.println("請輸入運運算元(+、-、*、/):*");
        String operator = "*";
        System.out.println("請輸入第二個數:3");
        int num2 = 3;

        // 根據運運算元建立不同的上下文物件
        Strategy strategy;
        switch (operator) {
            case "+" -> strategy = context.getBean(AddStrategy.class);
            case "-" -> strategy = context.getBean(SubtractStrategy.class);
            case "*" -> strategy = context.getBean(MultiplyStrategy.class);
            case "/" -> strategy = context.getBean(DivideStrategy.class);
            default -> {
                System.out.println("無效的運運算元");
                return;
            }
        }

        // 呼叫上下文物件的方法,執行策略物件的演演算法
        int result = strategy.doOperation(num1, num2);

        // 輸出結果
        System.out.println(num1 + " " + operator + " " + num2 + " = " + result);
    }
}

執行結果:

請輸入第一個數:2
請輸入運運算元(+、-、*、/):*
請輸入第二個數:3
2 * 3 = 6

總的來說策略模式是一種常用的行為型設計模式,它可以將不同的演演算法封裝在不同的類中,並讓它們可以相互替換。策略模式可以避免使用多重條件語句,提高程式碼的可讀性和可維護性,同時也可以實現開閉原則,增加系統的靈活性。但是策略模式也會增加使用者端和系統的複雜度,因此需要根據具體的情況來權衡利弊。

總結

至此本文所講的四種常用設計模式就全部介紹完了,希望能對大家有所幫助。

關注公眾號【waynblog】每週分享技術乾貨、開源專案、實戰經驗、高效開發工具等,你的關注將是我的更新動力!