《設計模式之美》是極客時間上的一個代碼學習系列,在學習之后特在此做記錄和總結。
設計模式要干的事情就是解耦,也就是利用更好的代碼結構將一大坨代碼拆分成職責更單一的小類,讓其滿足高內聚低耦合等特性。
每個設計模式都應該由兩部分組成:第一部分是應用場景,即這個模式可以解決哪類問題;第二部分是解決方案,即這個模式的設計思路和具體的代碼實現。不過,代碼實現並不是模式必須包含的。如果你單純地只關注解決方案這一部分,甚至只關注代碼實現,就會產生大部分模式看起來都很相似的錯覺。
一、創建型
創建型模式主要解決對象的創建問題,封裝復雜的創建過程,解耦對象的創建代碼和使用代碼。
單例模式用來創建全局唯一的對象。工廠模式用來創建不同但是相關類型的對象(繼承同一父類或者接口的一組子類),由給定的參數來決定創建哪種類型的對象。建造者模式是用來創建復雜對象,可以通過設置不同的可選參數,“定制化”地創建不同的對象。原型模式針對創建成本比較大的對象,利用對已有對象進行復制的方式進行創建,以達到節省創建時間的目的。
1)單例模式
單例設計模式(Singleton Design Pattern)是指一個類只允許創建一個對象(或者實例),那這個類就是一個單例類。
public class IdGenerator { private static IdGenerator instance; private IdGenerator() {} public static IdGenerator getInstance() { if (instance == null) { synchronized(IdGenerator.class) { // 此處為類級別的鎖 if (instance == null) { instance = new IdGenerator(); } } } return instance; } }
(1)實戰案例一:處理資源訪問沖突
將 Logger 設計成一個單例類,程序中只允許創建一個 Logger 對象,所有的線程共享使用的這一個 Logger 對象,共享一個 FileWriter 對象,而 FileWriter 本身是對象級別線程安全的,也就避免了多線程情況下寫日志會互相覆蓋的問題。
(2)實戰案例二:表示全局唯一類
從業務概念上,如果有些數據在系統中只應保存一份,那就比較適合設計為單例類。比如,配置信息類、唯一遞增 ID 號碼生成器。
實現:要實現一個單例,需要關注的點無外乎下面幾個:
(1)構造函數需要是 private 訪問權限的,這樣才能避免外部通過 new 創建實例;
(2)考慮對象創建時的線程安全問題;
(3)考慮是否支持延遲加載;
(4)考慮 getInstance() 性能是否高(是否加鎖)。
問題:有些人認為單例是一種反模式(anti-pattern),並不推薦使用。
(1)單例對 OOP 特性的支持不友好,對於其中的抽象、繼承、多態都支持得不好。
(2)單例會隱藏類之間的依賴關系,通過構造函數、參數傳遞等方式聲明的類之間的依賴關系很容易分辨,但是,單例類不需要顯示創建、不需要依賴參數傳遞。
(3)單例對代碼的擴展性不友好,單例類只能有一個對象實例。如果未來某一天,需要在代碼中創建兩個實例或多個實例,那就要對代碼有比較大的改動。
(4)單例對代碼的可測試性不友好,如果單例類依賴比較重的外部資源,比如 DB,由於單例類這種硬編碼式的使用方式,導致無法實現 mock 替換。
(5)單例不支持有參數的構造函數,比如創建一個連接池的單例對象,沒法通過參數來指定連接池的大小。
為了保證全局唯一,除了使用單例,還可以用靜態方法來實現。這也是項目開發中經常用到的一種實現思路。
2)工廠模式
(1)簡單工廠(Simple Factory)
大部分工廠類都是以“Factory”這個單詞結尾的,工廠類中創建對象的方法一般都是 create 開頭。
public class RuleConfigParserFactory { public static IRuleConfigParser createParser(String configFormat) { IRuleConfigParser parser = null; if ("json".equalsIgnoreCase(configFormat)) { parser = new JsonRuleConfigParser(); } else if ("xml".equalsIgnoreCase(configFormat)) { parser = new XmlRuleConfigParser(); } else if ("yaml".equalsIgnoreCase(configFormat)) { parser = new YamlRuleConfigParser(); } else if ("properties".equalsIgnoreCase(configFormat)) { parser = new PropertiesRuleConfigParser(); } return parser; } }
(2)工廠方法(Factory Method)
定義了一個創建對象的接口,但由子類決定要實例化的類是哪一個。工廠方法讓類把實例化推遲到子類。
如果非得要將 if 分支邏輯去掉,那么比較經典處理方法就是利用多態。工廠方法模式比起簡單工廠模式更加符合開閉原則。
public interface IRuleConfigParserFactory { IRuleConfigParser createParser(); } public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory { @Override public IRuleConfigParser createParser() { return new JsonRuleConfigParser(); } } public class XmlRuleConfigParserFactory implements IRuleConfigParserFactory { @Override public IRuleConfigParser createParser() { return new XmlRuleConfigParser(); } }
在工廠類的使用上,工廠類對象的創建邏輯又耦合進了 load() 函數中,跟最初的代碼版本非常相似。
可以為工廠類再創建一個簡單工廠,也就是工廠的工廠,用來創建工廠類對象。
當對象的創建邏輯比較復雜,不只是簡單的 new 一下就可以,而是要組合其他類對象,做各種初始化操作的時候,推薦使用工廠方法模式,將復雜的創建邏輯拆分到多個工廠類中,讓每個工廠類都不至於過於復雜。
而使用簡單工廠模式,將所有的創建邏輯都放到一個工廠類中,會導致這個工廠類變得很復雜。
3)建造者模式
Builder 模式,即建造者模式、構建者模式或生成器模式。要解決下面這些問題,就需要建造者模式上場了。
(1)如果可配置項逐漸增多,變成了 8 個、10 個,那么在使用構造函數的時候,就容易搞錯各參數的順序,傳遞進錯誤的參數值,導致非常隱蔽的 bug。
(2)如果必填的配置項有很多,把這些必填配置項都放到構造函數中設置,那構造函數就又會出現參數列表很長的問題。
(3)如果配置項之間有約束條件,那么校驗邏輯就無處安放了。
(4)如果希望對象在創建好之后,就不能再修改內部的屬性值,那么就不能暴露 set() 方法。
可以把校驗邏輯放置到 Builder 類中,先創建建造者,並且通過 set() 方法設置建造者的變量值,然后再使用 build() 方法真正創建對象之前,做集中的校驗,校驗通過之后才會創建對象。
除此之外,把 ResourcePoolConfig 的構造函數改為 private 私有權限。這樣就只能通過建造者來創建 ResourcePoolConfig 類對象。並且,ResourcePoolConfig 沒有提供任何 set() 方法,這樣創建出來的對象就是不可變對象了。
public class ResourcePoolConfig { private String name; private int maxTotal; private ResourcePoolConfig(Builder builder) { this.name = builder.name; this.maxTotal = builder.maxTotal; } //...省略getter方法... //將Builder類設計成了ResourcePoolConfig的內部類。 //也可以將Builder類設計成獨立的非內部類ResourcePoolConfigBuilder。 public static class Builder { private static final int DEFAULT_MAX_TOTAL = 8; private String name; private int maxTotal = DEFAULT_MAX_TOTAL; public ResourcePoolConfig build() { // 校驗邏輯放到這里來做,包括必填項校驗、依賴關系校驗、約束條件校驗等 if (StringUtils.isBlank(name)) { throw new IllegalArgumentException("..."); } return new ResourcePoolConfig(this); } public Builder setName(String name) { if (StringUtils.isBlank(name)) { throw new IllegalArgumentException("..."); } this.name = name; return this; } public Builder setMaxTotal(int maxTotal) { if (maxTotal <= 0) { throw new IllegalArgumentException("..."); } this.maxTotal = maxTotal; return this; } } } // 這段代碼會拋出IllegalArgumentException,因為minIdle>maxIdle ResourcePoolConfig config = new ResourcePoolConfig.Builder() .setName("dbconnectionpool") .setMaxTotal(16) .build();
工廠模式是用來創建不同但是相關類型的對象(繼承同一父類或者接口的一組子類),由給定的參數來決定創建哪種類型的對象。建造者模式是用來創建一種類型的復雜對象,通過設置不同的可選參數,“定制化”地創建不同的對象。
簡單地說,工廠模式是根據不同的條件生成不同類的對象,建造者模式是根據不同參數生成一個類的不同對象。
4)原型模式
如果對象的創建成本比較大,而同一個類的不同對象之間差別不大(大部分字段都相同),在這種情況下,可以利用對已有對象(原型)進行復制(或者叫拷貝)的方式來創建新對象,以達到節省創建時間的目的。這種基於原型來創建對象的方式就叫作原型設計模式(Prototype Design Pattern),簡稱原型模式。
如果對象中的數據需要經過復雜的計算才能得到(比如排序、計算哈希值),或者需要從 RPC、網絡、數據庫、文件系統等非常慢速的 IO 中讀取,這種情況下,就可以利用原型模式,從其他已有對象中直接拷貝得到,而不用每次在創建新對象的時候,都重復執行這些耗時的操作。
原型模式的實現方式:深拷貝(Deep Copy)和淺拷貝(Shallow Copy)。
淺拷貝只會復制圖中的索引(散列表),不會復制數據(SearchWord 對象)本身。相反,深拷貝不僅僅會復制索引,還會復制數據本身。淺拷貝得到的對象(newKeywords)跟原始對象(currentKeywords)共享數據(SearchWord 對象),而深拷貝得到的是一份完完全全獨立的對象。
實現深拷貝的兩種方法:
(1)第一種方法:遞歸拷貝對象、對象的引用對象以及引用對象的引用對象……直到要拷貝的對象只包含基本數據類型數據,沒有引用對象為止。
(2)第二種方法:先將對象序列化,然后再反序列化成新的對象。
二、結構型
結構型模式主要總結了一些類或對象組合在一起的經典結構,這些經典的結構可以解決特定應用場景的問題。
1)代理模式
代理模式(Proxy Design Pattern)是指在不改變原始類(或叫被代理類)代碼的情況下,通過引入代理類來給原始類附加功能。
為了將框架代碼和業務代碼解耦,代理模式就派上用場了。
UserController 類只負責業務功能。代理類 UserControllerProxy 負責在業務代碼執行前后附加其他邏輯代碼,並通過委托的方式調用原始類來執行業務代碼。
public interface IUserController { UserVo login(String telephone, String password); UserVo register(String telephone, String password); } public class UserController implements IUserController { } public class UserControllerProxy implements IUserController { private MetricsCollector metricsCollector; private UserController userController; public UserControllerProxy(UserController userController) { this.userController = userController; this.metricsCollector = new MetricsCollector(); } @Override public UserVo login(String telephone, String password) { long startTimestamp = System.currentTimeMillis(); // 委托 UserVo userVo = userController.login(telephone, password); long endTimeStamp = System.currentTimeMillis(); long responseTime = endTimeStamp - startTimestamp; RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp); metricsCollector.recordRequest(requestInfo); return userVo; } }
為了讓代碼改動盡量少,在剛剛的代理模式的代碼實現中,代理類和原始類需要實現相同的接口。而對於外部類的擴展,一般都是采用繼承的方式。
public class UserControllerProxy extends UserController { }
所謂動態代理(Dynamic Proxy),就是不事先為每個原始類編寫代理類,而是在運行的時候,動態地創建原始類對應的代理類,然后在系統中用代理類替換掉原始類。
應用場景:
(1)在業務系統中開發一些非功能性需求,比如:監控、統計、鑒權、限流、事務、冪等、日志。
(2)RPC 框架也可以看作一種代理模式,通過遠程代理,將網絡通信、數據編解碼等細節隱藏起來。在 AOP 切面中完成接口緩存的功能。
2)橋接模式
橋接模式(Bridge Design Pattern)也叫橋梁模式,對於這個模式有兩種不同的理解方式。
(1)將抽象和實現解耦,讓它們可以獨立變化。
(2)一個類存在兩個(或多個)獨立變化的維度,通過組合的方式,讓這兩個(或多個)維度可以獨立進行擴展。
針對 Notification 的代碼,將不同渠道的發送邏輯剝離出來,形成獨立的消息發送類(MsgSender 相關類)。其中,Notification 類相當於抽象,MsgSender 類相當於實現,兩者可以獨立開發,通過組合關系(也就是橋梁)任意組合在一起。所謂任意組合的意思就是,不同緊急程度的消息和發送渠道之間的對應關系,不是在代碼中固定寫死的,可以動態地去指定(比如,通過讀取配置來獲取對應關系)。
public interface MsgSender { void send(String message); } public class TelephoneMsgSender implements MsgSender { private List<String> telephones; public TelephoneMsgSender(List<String> telephones) { this.telephones = telephones; } @Override public void send(String message) { //... } } public class EmailMsgSender implements MsgSender { // 與TelephoneMsgSender代碼結構類似,所以省略... } public abstract class Notification { protected MsgSender msgSender; public Notification(MsgSender msgSender) { this.msgSender = msgSender; } public abstract void notify(String message); } public class SevereNotification extends Notification { public SevereNotification(MsgSender msgSender) { super(msgSender); } @Override public void notify(String message) { msgSender.send(message); } } public class UrgencyNotification extends Notification { // 與SevereNotification代碼結構類似,所以省略... } public class NormalNotification extends Notification { // 與SevereNotification代碼結構類似,所以省略... }
3)裝飾器模式
裝飾器模式(Decorator Design Pattern)相對於簡單的組合關系,有兩個比較特殊的地方。
(1)裝飾器類和原始類繼承同樣的父類,這樣可以對原始類“嵌套”多個裝飾器類。
(2)裝飾器類是對功能的增強,這也是裝飾器模式應用場景的一個重要特點。
代理類附加的是跟原始類無關的功能,而在裝飾器模式中,裝飾器類附加的是跟原始類相關的增強功能。
代理模式偏重業務無關,高度抽象和穩定性較高的場景。裝飾器模式偏重業務相關,定制化訴求高,改動較頻繁的場景。
4)適配器模式
適配器模式(Adapter Design Pattern)可將不兼容的接口轉換為可兼容的接口,讓原本由於接口不兼容而不能一起工作的類可以一起工作。
適配器模式有兩種實現方式:類適配器和對象適配器。其中,類適配器使用繼承關系來實現,對象適配器使用組合關系來實現。下面是使用的前提條件。
(1)如果 Adaptee 接口並不多,那兩種實現方式都可以。
(2)如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定義大部分都相同,那推薦使用類適配器,因為 Adaptor 復用父類 Adaptee 的接口,比起對象適配器的實現方式,Adaptor 的代碼量要少一些。
(3)如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定義大部分都不相同,那推薦使用對象適配器,因為組合結構相對於繼承更加靈活。
應用場景:
適配器模式可以看作一種“補償模式”,用來補救設計上的缺陷。應用這種模式算是“無奈之舉”。
(1)封裝有缺陷的接口設計。對外部系統提供的接口進行二次封裝,抽象出更好的接口設計。
(2)統一多個類的接口設計。將所有系統的接口適配為統一的接口定義。
(3)替換依賴的外部系統。
(4)兼容老版本接口。
(5)適配不同格式的數據。
代理、橋接、裝飾器和適配器都可以稱為 Wrapper 模式,也就是通過 Wrapper 類二次封裝原始類。
(1)代理模式:代理模式在不改變原始類接口的條件下,為原始類定義一個代理類,主要目的是控制訪問,而非加強功能,這是它跟裝飾器模式最大的不同。
(2)橋接模式:橋接模式的目的是將接口部分和實現部分分離,從而讓它們可以較為容易、也相對獨立地加以改變。
(3)裝飾器模式:裝飾者模式在不改變原始類接口的情況下,對原始類功能進行增強,並且支持多個裝飾器的嵌套使用。
(4)適配器模式:適配器模式是一種事后的補救策略。適配器提供跟原始類不同的接口,而代理模式、裝飾器模式提供的都是跟原始類相同的接口。
5)門面模式
門面模式(Facade Design Pattern)為子系統提供一組統一的接口,定義一組高層接口讓子系統更易用。子系統(subsystem)既可以是一個完整的系統,也可以是更細粒度的類或者模塊。
App 客戶端的響應速度比較慢,排查之后發現,是因為過多的接口調用過多的網絡通信。針對這種情況,就可以利用門面模式,讓后端服務器提供一個包裹 a、b、d 三個接口調用的接口 x。App 客戶端調用一次接口 x,來獲取到所有想要的數據,將網絡通信的次數從 3 次減少到 1 次,也就提高了 App 的響應速度。
應用場景:
(1)解決易用性問題,比如,Linux 系統調用函數、Shell 命令。
(2)解決性能問題,如果門面接口特別多,並且很多都是跨多個子系統的,可將門面接口放到一個新的子系統中。
(3)解決分布式事務問題,比如在一個事務中,執行創建用戶和創建錢包這兩個 SQL 操作。
與適配器模式的區別:
(1)適配器模式是做接口轉換,解決的是原接口和目標接口不匹配的問題。在代碼結構上主要是繼承加組合。
(2)門面模式做接口整合,解決的是多接口調用帶來的問題。在代碼結構上主要是封裝。
6)組合模式
組合模式(Composite Design Pattern)跟面向對象設計中的“組合關系(通過組合來組裝兩個類)”,完全是兩碼事。它主要是用來處理樹形結構數據,其中數據可理解為一組對象集合。
組合模式是將一組對象組織成樹形結構,以表示一種“部分 - 整體”的層次結構。組合讓客戶端(指代碼的使用者)可以統一單個對象和組合對象的處理邏輯。
對照着例子,重新定義:
將一組對象(文件和目錄)組織成樹形結構,以表示一種‘部分 - 整體’的層次結構(目錄與子目錄的嵌套結構)。組合模式讓客戶端可以統一單個對象(文件)和組合對象(目錄)的處理邏輯(遞歸遍歷)。
實際上,組合模式的設計思路,與其說是一種設計模式,倒不如說是對業務場景的一種數據結構和算法的抽象。其中,數據可以表示成樹,業務需求可以通過在樹上的遞歸遍歷算法來實現。
7)享元模式
所謂享元,顧名思義就是被共享的單元。享元模式(Flyweight Design Pattern)的意圖是復用對象,節省內存,前提是享元對象是不可變對象。
當一個系統中存在大量重復對象的時候,如果這些重復的對象是不可變對象,就可以利用享元模式將對象設計成享元,在內存中只保留一份實例,供多處代碼引用。對於相似對象,也可以將它相同的部分(字段)提取出來,設計成享元。
“不可變對象”指的是,一旦通過構造函數初始化完成之后,它的狀態(對象的成員變量或者屬性)就不會再被修改了。所以,不可變對象不能暴露任何 set() 等修改內部狀態的方法。之所以要求享元是不可變對象,那是因為它會被多處代碼共享使用,避免一處代碼對享元進行了修改,影響到其他使用它的代碼。
所有的 ChessBoard 對象共享這 30 個 ChessPieceUnit 對象(因為象棋中只有 30 個棋子)。在使用享元模式之前,記錄 1 萬個棋局,要創建 30 萬(30*1 萬)個棋子的 ChessPieceUnit 對象。利用享元模式,只需要創建 30 個享元對象供所有棋局共享使用即可,大大節省了內存。
// 享元類 public class ChessPieceUnit { private int id; private String text; private Color color; public ChessPieceUnit(int id, String text, Color color) { this.id = id; this.text = text; this.color = color; } public static enum Color { RED, BLACK } } public class ChessPieceUnitFactory { private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>(); static { pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK)); pieces.put(2, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK)); //...省略擺放其他棋子的代碼... } public static ChessPieceUnit getChessPiece(int chessPieceId) { return pieces.get(chessPieceId); } } public class ChessPiece { //棋子 private ChessPieceUnit chessPieceUnit; private int positionX; private int positionY; public ChessPiece(ChessPieceUnit unit, int positionX, int positionY) { this.chessPieceUnit = unit; this.positionX = positionX; this.positionY = positionY; } } public class ChessBoard { //棋局 private Map<Integer, ChessPiece> chessPieces = new HashMap<>(); public ChessBoard() { init(); } private void init() { chessPieces.put(1, new ChessPiece( ChessPieceUnitFactory.getChessPiece(1), 0,0)); chessPieces.put(1, new ChessPiece( ChessPieceUnitFactory.getChessPiece(2), 1,0)); //...省略擺放其他棋子的代碼... } public void move(int chessPieceId, int toPositionX, int toPositionY) { //...省略... } }
實際上,它的代碼實現非常簡單,主要是通過工廠模式,在工廠類中,通過一個 Map 來緩存已經創建過的享元對象,來達到復用的目的。
在單例模式中,一個類只能創建一個對象,而在享元模式中,一個類可以創建多個對象,每個對象被多處代碼引用共享。
三、行為型
創建型設計模式主要解決“對象的創建”問題,結構型設計模式主要解決“類或對象的組合或組裝”問題,那行為型設計模式主要解決的就是“類或對象之間的交互”問題。
設計模式要干的事情就是解耦。創建型模式是將創建和使用代碼解耦,結構型模式是將不同功能代碼解耦,行為型模式是將不同的行為代碼解耦。
1)觀察者模式
觀察者模式(Observer Design Pattern)也叫發布訂閱模式(Publish-Subscribe Design Pattern),在對象之間定義一個一對多的依賴,當一個對象狀態改變的時候,所有依賴的對象都會自動收到通知。
一般情況下,被依賴的對象叫作被觀察者(Observable),依賴的對象叫作觀察者(Observer)。觀察者模式就是將觀察者和被觀察者代碼解耦。
public interface Subject { void registerObserver(Observer observer); void removeObserver(Observer observer); void notifyObservers(Message message); } public interface Observer { void update(Message message); }
基於消息隊列的實現方式,被觀察者完全不感知觀察者,同理,觀察者也完全不感知被觀察者。被觀察者只管發送消息到消息隊列,觀察者只管從消息隊列中讀取消息來執行相應的邏輯。
2)模板模式
模板模式(Template Method Design Pattern)全稱是模板方法模式,可在一個方法中定義一個算法骨架,並將某些步驟推遲到子類中實現。模板模式可以讓子類在不改變算法整體結構的情況下,重新定義算法中的某些步驟。
這里的“算法”,可以理解為廣義上的“業務邏輯”,並不特指數據結構和算法中的“算法”。
public abstract class AbstractClass { public final void templateMethod() { //... method1(); //... method2(); //... } protected abstract void method1(); protected abstract void method2(); } public class ConcreteClass1 extends AbstractClass { @Override protected void method1() {} @Override protected void method2() {} } public class ConcreteClass2 extends AbstractClass { @Override protected void method1() {} @Override protected void method2() {} }
模板方法定義為 final,可以避免被子類重寫。需要子類重寫的方法定義為 abstract,可以強迫子類去實現。
(1)作用一:復用
模板模式把一個算法中不變的流程抽象到父類的模板方法 templateMethod() 中,將可變的部分 method1()、method2() 留給子類 ContreteClass1 和 ContreteClass2 來實現。
(2)作用二:擴展
這里所說的擴展,並不是指代碼的擴展性,而是指框架的擴展性,有點類似之前講到的控制反轉。基於這個作用,模板模式常用在框架的開發中,讓框架用戶可以在不修改框架源碼的情況下,定制化框架的功能。
3)策略模式
策略模式(Strategy Design Pattern)定義一族算法類,將每個算法分別封裝起來,讓它們可以互相替換。策略模式可以使算法的變化獨立於使用它們的客戶端(使用算法的代碼)。
策略模式解耦的是策略的定義、創建、使用這三部分。讓每個部分都不至於過於復雜、代碼量過多。
(1)策略類的定義比較簡單,包含一個策略接口和一組實現這個接口的策略類。
(2)通過類型(type)來判斷創建哪個策略。可以把根據 type 創建策略的邏輯抽離出來,放到工廠類中。
(3)運行時動態確定使用哪種策略,即在程序運行期間,根據配置、用戶輸入、計算結果等這些不確定因素,動態決定使用哪種策略。
利用策略模式避免分支判斷。將不同類型訂單的打折策略設計成策略類,並由工廠類來負責創建策略對象。
// 策略的定義 public interface DiscountStrategy { double calDiscount(Order order); } // 省略NormalDiscountStrategy、GrouponDiscountStrategy、PromotionDiscountStrategy類代碼... // 策略的創建 public class DiscountStrategyFactory { private static final Map<OrderType, DiscountStrategy> strategies = new HashMap<>(); static { strategies.put(OrderType.NORMAL, new NormalDiscountStrategy()); strategies.put(OrderType.GROUPON, new GrouponDiscountStrategy()); strategies.put(OrderType.PROMOTION, new PromotionDiscountStrategy()); } public static DiscountStrategy getDiscountStrategy(OrderType type) { return strategies.get(type); } } // 策略的使用 public class OrderService { public double discount(Order order) { OrderType type = order.getType(); DiscountStrategy discountStrategy = DiscountStrategyFactory.getDiscountStrategy(type); return discountStrategy.calDiscount(order); } }
策略模式側重“策略”或“算法”這個特定的應用場景,用來解決根據運行時狀態從一組策略中選擇不同策略的問題,而工廠模式側重封裝對象的創建過程,這里的對象沒有任何業務場景的限定,可以是策略,但也可以是其他東西。
4)職責鏈模式
職責鏈模式(Chain Of Responsibility Design Pattern)是將請求的發送和接收解耦,讓多個接收對象都有機會處理這個請求。將這些接收對象串成一條鏈,並沿着這條鏈傳遞這個請求,直到鏈上的某個接收對象能夠處理它為止。
在職責鏈模式中,多個處理器(接收對象)依次處理同一個請求。一個請求先經過 A 處理器處理,然后再把請求傳遞給 B 處理器,B 處理器處理完后再傳遞給 C 處理器,以此類推,形成一個鏈條。鏈條上的每個處理器各自承擔各自的處理職責,所以叫作職責鏈模式。
public abstract class Handler { //模板模式 protected Handler successor = null; public void setSuccessor(Handler successor) { this.successor = successor; } public final void handle() { boolean handled = doHandle(); if (successor != null && !handled) { successor.handle(); } } protected abstract boolean doHandle(); } public class HandlerA extends Handler { @Override protected boolean doHandle() { boolean handled = false; //... return handled; } } public class HandlerB extends Handler { @Override protected boolean doHandle() { boolean handled = false; //... return handled; } } public class HandlerChain { private Handler head = null; private Handler tail = null; public void addHandler(Handler handler) { handler.setSuccessor(null); if (head == null) { head = handler; tail = handler; return; } tail.setSuccessor(handler); tail = handler; } public void handle() { if (head != null) { head.handle(); } } } public class Application { //使用舉例 public static void main(String[] args) { HandlerChain chain = new HandlerChain(); chain.addHandler(new HandlerA()); chain.addHandler(new HandlerB()); chain.handle(); } }
職責鏈模式還有一種變體,那就是請求會被所有的處理器都處理一遍,不存在中途終止的情況。這種變體也有兩種實現方式:用鏈表存儲處理器和用數組存儲處理器。
為什么非要使用職責鏈模式呢?這是不是過度設計呢?
(1)應對代碼的復雜性,用職責鏈模式把各個敏感詞過濾函數繼續拆分出來,設計成獨立的類,進一步簡化了 SensitiveWordFilter 類,讓 SensitiveWordFilter 類的代碼不會過多,過復雜。
(2)滿足開閉原則,當要擴展新的過濾算法時,只需要新添加一個 Filter 類,並且通過 addFilter() 函數將它添加到 FilterChain 中即可,其他代碼完全不需要修改。
5)狀態模式
狀態模式一般用來實現狀態機,而狀態機常用在游戲、工作流引擎等系統開發中。
有限狀態機(Finite State Machine,FSM),簡稱為狀態機。狀態機有 3 個組成部分:狀態(State)、事件(Event)、動作(Action)。其中,事件也稱為轉移條件(Transition Condition)。事件觸發狀態的轉移及動作的執行。不過,動作不是必須的,也可能只轉移狀態,不執行任何動作。
(1)狀態機實現方式一:分支邏輯法
參照狀態轉移圖,將每一個狀態轉移,原模原樣地直譯成代碼。這樣編寫的代碼會包含大量的 if-else 或 switch-case 分支判斷邏輯,甚至是嵌套的分支判斷邏輯,所以這種方法暫且命名為分支邏輯法。
(2)狀態機實現方式二:查表法
把這兩個二維數組存儲在配置文件中,當需要修改狀態機時,甚至可以不修改任何代碼,只需要修改配置文件就可以了。
(3)狀態機實現方式三:狀態模式
如果要執行的動作是一系列復雜的邏輯操作(比如加減積分、寫數據庫,還有可能發送消息通知等等),那么查表法就不合適了。
狀態模式通過將事件觸發的狀態轉移和動作執行,拆分到不同的狀態類中,來避免分支判斷邏輯。
public interface IMario { //所有狀態類的接口 void obtainMushRoom(); void obtainCape(); } public class SmallMario implements IMario { private MarioStateMachine stateMachine; public SmallMario(MarioStateMachine stateMachine) { this.stateMachine = stateMachine; } @Override public void obtainMushRoom() { stateMachine.setCurrentState(new SuperMario(stateMachine)); stateMachine.setScore(stateMachine.getScore() + 100); } @Override public void obtainCape() { stateMachine.setCurrentState(new CapeMario(stateMachine)); stateMachine.setScore(stateMachine.getScore() + 200); } } public class SuperMario implements IMario { private MarioStateMachine stateMachine; public SuperMario(MarioStateMachine stateMachine) { this.stateMachine = stateMachine; } @Override public void obtainMushRoom() { // do nothing... } @Override public void obtainCape() { stateMachine.setCurrentState(new CapeMario(stateMachine)); stateMachine.setScore(stateMachine.getScore() + 200); } } public class MarioStateMachine { private int score; private IMario currentState; //不再使用枚舉來表示狀態 public MarioStateMachine() { this.score = 0; this.currentState = new SmallMario(this); } public void obtainMushRoom() { this.currentState.obtainMushRoom(); } public void obtainCape() { this.currentState.obtainCape(); } public void setScore(int score) { this.score = score; } public void setCurrentState(IMario currentState) { this.currentState = currentState; } }
6)迭代器模式
迭代器模式(Iterator Design Pattern)也叫游標模式(Cursor Design Pattern)用來遍歷集合對象。
這里說的“集合對象”也可以叫“容器”“聚合對象”,實際上就是包含一組對象的對象,比如數組、鏈表、樹、圖、跳表。
迭代器模式將集合對象的遍歷操作從集合類中拆分出來,放到迭代器類中,讓兩者的職責更加單一。
一個完整的迭代器模式一般會涉及容器和容器迭代器兩部分內容。為了達到基於接口而非實現編程的目的,容器又包含容器接口、容器實現類,迭代器又包含迭代器接口、迭代器實現類。
public interface Iterator<E> { boolean hasNext(); void next(); E currentItem(); }
總結下來就三句話:迭代器中需要定義 hasNext()、currentItem()、next() 三個最基本的方法。待遍歷的容器對象通過依賴注入傳遞到迭代器類中。容器通過 iterator() 方法來創建迭代器。
為什么還要用迭代器來遍歷容器呢?為什么還要給容器設計對應的迭代器呢?
(1)復雜的數據結構(比如樹、圖)來說,有各種復雜的遍歷方式。
(2)將游標指向的當前位置等信息,存儲在迭代器類中,每個迭代器獨享游標信息。
(3)容器和迭代器都提供了抽象的接口,方便在開發時基於接口而非具體的實現編程。
在通過迭代器來遍歷集合元素的同時,增加或者刪除集合中的元素,有可能會導致某個元素被重復遍歷或遍歷不到。
7)訪問者模式
訪問者者模式(Visitor Design Pattern)允許一個或者多個操作應用到一組對象上,解耦操作和對象本身。
訪問者模式針對的是一組類型不同的對象(PdfFile、PPTFile、WordFile)。不過,盡管這組對象的類型是不同的,但是,它們繼承相同的父類(ResourceFile)或者實現相同的接口。
在不同的應用場景下,需要對這組對象進行一系列不相關的業務操作(抽取文本、壓縮等),但為了避免不斷添加功能導致類(PdfFile、PPTFile、WordFile)不斷膨脹,職責越來越不單一,以及避免頻繁地添加功能導致的頻繁代碼修改,使用訪問者模式,將對象與操作解耦,將這些業務操作抽離出來,定義在獨立細分的訪問者類(Extractor、Compressor)中。
對於訪問者模式,學習的主要難點在代碼實現。而代碼實現比較復雜的主要原因是,函數重載在大部分面向對象編程語言中是靜態綁定的。也就是說,調用類的哪個重載函數,是在編譯期間,由參數的聲明類型決定的,而非運行時,根據參數的實際類型決定的。
8)備忘錄模式
備忘錄模式(Memento Design Pattern)也叫快照(Snapshot)模式,在不違背封裝原則的前提下,捕獲一個對象的內部狀態,並在該對象之外保存這個狀態,以便之后恢復對象為先前的狀態。
備忘錄模式主要是用來防丟失、撤銷、恢復等。
其一,定義一個獨立的類(Snapshot 類)來表示快照,而不是復用 InputText 類。這個類只暴露 get() 方法,沒有 set() 等任何修改內部狀態的方法。
其二,在 InputText 類中,把 setText() 方法重命名為 restoreSnapshot() 方法,用意更加明確,只用來恢復對象。
public class InputText { private StringBuilder text = new StringBuilder(); public String getText() { return text.toString(); } public void append(String input) { text.append(input); } public Snapshot createSnapshot() { return new Snapshot(text.toString()); } public void restoreSnapshot(Snapshot snapshot) { this.text.replace(0, this.text.length(), snapshot.getText()); } } public class Snapshot { private String text; public Snapshot(String text) { this.text = text; } public String getText() { return this.text; } } public class SnapshotHolder { private Stack<Snapshot> snapshots = new Stack<>(); public Snapshot popSnapshot() { return snapshots.pop(); } public void pushSnapshot(Snapshot snapshot) { snapshots.push(snapshot); } }
9)命令模式
命令模式(Command Design Pattern)將請求(命令)封裝為一個對象,這樣可以使用不同的請求參數化其他對象(將不同請求依賴注入到其他對象),並且能夠支持請求(命令)的排隊執行、記錄日志、撤銷等(附加控制)功能。
在大部分編程語言中,函數沒法作為參數傳遞給其他函數,也沒法賦值給變量。借助命令模式,可以將函數封裝成對象。設計一個包含這個函數的類,實例化一個對象傳來傳去,這樣就可以實現把函數像對象一樣使用。
在策略模式中,不同的策略具有相同的目的、不同的實現、互相之間可以替換。比如,BubbleSort、SelectionSort 都是為了實現排序的,只不過一個是用冒泡排序算法來實現的,另一個是用選擇排序算法來實現的。而在命令模式中,不同的命令具有不同的目的,對應不同的處理邏輯,並且互相之間不可替換。
命令模式的主要作用和應用場景,是用來控制命令的執行,比如,異步、延遲、排隊執行命令、撤銷重做命令、存儲命令、給命令記錄日志等。
10)解釋器模式
解釋器模式(Interpreter Design Pattern)只在一些特定的領域會被用到,比如編譯器、規則引擎、正則表達式。它能為某個語言定義它的語法(或者叫文法)表示,並定義一個解釋器用來處理這個語法。
11)中介模式
中介模式(Mediator Design Pattern)定義了一個單獨的(中介)對象,來封裝一組對象之間的交互。將這組對象之間的交互委派給與中介對象交互,來避免對象之間的直接交互。
中介模式的設計思想跟中間層很像,通過引入中介這個中間層,將一組對象之間的交互關系(或者說依賴關系)從多對多(網狀關系)轉換為一對多(星狀關系)。
假設有一個比較復雜的對話框,對話框中有很多控件,比如按鈕、文本框、下拉框等。當對某個控件進行操作的時候,其他控件會做出相應的反應,比如,在下拉框中選擇“注冊”,注冊相關的控件就會顯示在對話框中。
public interface Mediator { void handleEvent(Component component, String event); } public class LandingPageDialog implements Mediator { private Button loginButton; private Button regButton; private Selection selection; private Input usernameInput; private Input passwordInput; private Input repeatedPswdInput; private Text hintText; @Override public void handleEvent(Component component, String event) { if (component.equals(loginButton)) { String username = usernameInput.text(); String password = passwordInput.text(); //校驗數據... //做業務處理... } else if (component.equals(regButton)) { //獲取usernameInput、passwordInput、repeatedPswdInput數據... //校驗數據... //做業務處理... } else if (component.equals(selection)) { String selectedItem = selection.select(); if (selectedItem.equals("login")) { usernameInput.show(); passwordInput.show(); repeatedPswdInput.hide(); hintText.hide(); //...省略其他代碼 } else if (selectedItem.equals("register")) { //.... } } } }
好處是簡化了控件之間的交互,壞處是中介類有可能會變成大而復雜的“上帝類”(God Class)。所以,在使用中介模式的時候,要根據實際的情況,平衡對象之間交互的復雜度和中介類本身的復雜度。
中介模式和觀察者模式的區別在哪里呢?
(1)在觀察者模式中,盡管一個參與者既可以是觀察者,同時也可以是被觀察者,但是大部分情況下,交互關系往往都是單向的,一個參與者要么是觀察者,要么是被觀察者,不會兼具兩種身份。
(2)而中介模式正好相反。參與者之間的交互關系錯綜復雜,既可以是消息的發送者、也可以同時是消息的接收者。