圖解常見的九種設(shè)計模式
在軟件工程中,設(shè)計模式(Design Pattern)是對軟件設(shè)計中普遍存在(反復出現(xiàn))的各種問題,所提出的解決方案。根據(jù)模式的目的來劃分的話,GoF(Gang of Four)設(shè)計模式可以分為以下 3 種類型:

1、創(chuàng)建型模式:用來描述 “如何創(chuàng)建對象”,它的主要特點是 “將對象的創(chuàng)建和使用分離”。包括單例、原型、工廠方法、抽象工廠和建造者 5 種模式。
2、結(jié)構(gòu)型模式:用來描述如何將類或?qū)ο蟀凑漳撤N布局組成更大的結(jié)構(gòu)。包括代理、適配器、橋接、裝飾、外觀、享元和組合 7 種模式。
3、行為型模式:用來識別對象之間的常用交流模式以及如何分配職責。包括模板方法、策略、命令、職責鏈、狀態(tài)、觀察者、中介者、迭代器、訪問者、備忘錄和解釋器 11 種模式。
接下來阿寶哥將結(jié)合一些生活中的場景并通過精美的配圖,來向大家介紹 9 種常用的設(shè)計模式。
一、建造者模式
建造者模式(Builder Pattern)將一個復雜對象分解成多個相對簡單的部分,然后根據(jù)不同需要分別創(chuàng)建它們,最后構(gòu)建成該復雜對象。
一輛小汽車 ? 通常由 發(fā)動機、底盤、車身和電氣設(shè)備 四大部分組成。汽車電氣設(shè)備的內(nèi)部構(gòu)造很復雜,簡單起見,我們只考慮三個部分:引擎、底盤和車身。

在現(xiàn)實生活中,小汽車也是由不同的零部件組裝而成,比如上圖中我們把小汽車分成引擎、底盤和車身三大部分。下面我們來看一下如何使用建造者模式來造車子。
1.1 實現(xiàn)代碼
class?Car?{
??constructor(
????public?engine:?string,
????public?chassis:?string,?
????public?body:?string
??)?{}
}
class?CarBuilder?{
??engine!:?string;?//?引擎
??chassis!:?string;?//?底盤
??body!:?string;?//?車身
??addChassis(chassis:?string)?{
????this.chassis?=?chassis;
????return?this;
??}
??addEngine(engine:?string)?{
????this.engine?=?engine;
????return?this;
??}
??addBody(body:?string)?{
????this.body?=?body;
????return?this;
??}
??build()?{
????return?new?Car(this.engine,?this.chassis,?this.body);
??}
}
在以上代碼中,我們定義一個 CarBuilder 類,并提供了 addChassis、addEngine 和 addBody 3 個方法用于組裝車子的不同部位,當車子的 3 個部分都組裝完成后,調(diào)用 build 方法就可以開始造車。
1.2 使用示例
const?car?=?new?CarBuilder()
??.addEngine('v12')
??.addBody('鎂合金')
??.addChassis('復合材料')
??.build();
1.3 應用場景及案例
需要生成的產(chǎn)品對象有復雜的內(nèi)部結(jié)構(gòu),這些產(chǎn)品對象通常包含多個成員屬性。 需要生成的產(chǎn)品對象的屬性相互依賴,需要指定其生成順序。 隔離復雜對象的創(chuàng)建和使用,并使得相同的創(chuàng)建過程可以創(chuàng)建不同的產(chǎn)品。 Github - node-sql-query:https://github.com/dresende/node-sql-query
二、工廠模式
在現(xiàn)實生活中,工廠是負責生產(chǎn)產(chǎn)品的,比如牛奶、面包或禮物等,這些產(chǎn)品滿足了我們?nèi)粘5纳硇枨蟆?/p>
在眾多設(shè)計模式當中,有一種被稱為工廠模式的設(shè)計模式,它提供了創(chuàng)建對象的最佳方式。工廠模式可以分為:簡單工廠模式、工廠方法模式和抽象工廠模式。
2.1 簡單工廠
簡單工廠模式又叫 靜態(tài)方法模式,因為工廠類中定義了一個靜態(tài)方法用于創(chuàng)建對象。簡單工廠讓使用者不用知道具體的參數(shù)就可以創(chuàng)建出所需的 ”產(chǎn)品“ 類,即使用者可以直接消費產(chǎn)品而不需要知道產(chǎn)品的具體生產(chǎn)細節(jié)。

在上圖中,阿寶哥模擬了用戶購車的流程,小王和小秦分別向 BMW 工廠訂購了 BMW730 和 BMW840 型號的車型,接著工廠會先判斷用戶選擇的車型,然后按照對應的模型進行生產(chǎn)并在生產(chǎn)完成后交付給用戶。
下面我們來看一下如何使用簡單工廠來描述 BMW 工廠生產(chǎn)指定型號車子的過程。
2.1.1 實現(xiàn)代碼
abstract?class?BMW?{
??abstract?run():?void;
}
class?BMW730?extends?BMW?{
??run():?void?{
????console.log("BMW730?發(fā)動咯");
??}
}
class?BMW840?extends?BMW?{
??run():?void?{
????console.log("BMW840?發(fā)動咯");
??}
}
class?BMWFactory?{
??public?static?produceBMW(model:?"730"?|?"840"):?BMW?{
????if?(model?===?"730")?{
??????return?new?BMW730();
????}?else?{
??????return?new?BMW840();
????}
??}
}
在以上代碼中,我們定義一個 BMWFactory 類,該類提供了一個靜態(tài)的 produceBMW() 方法,用于根據(jù)不同的模型參數(shù)來創(chuàng)建不同型號的車子。
2.1.2 使用示例
const?bmw730?=?BMWFactory.produceBMW("730");
const?bmw840?=?BMWFactory.produceBMW("840");
bmw730.run();
bmw840.run();
2.1.3 應用場景
工廠類負責創(chuàng)建的對象比較少:由于創(chuàng)建的對象比較少,不會造成工廠方法中業(yè)務邏輯過于復雜。 客戶端只需知道傳入工廠類靜態(tài)方法的參數(shù),而不需要關(guān)心創(chuàng)建對象的細節(jié)。
2.2 工廠方法
工廠方法模式(Factory Method Pattern)又稱為工廠模式,也叫多態(tài)工廠(Polymorphic Factory)模式,它屬于類創(chuàng)建型模式。
在工廠方法模式中,工廠父類負責定義創(chuàng)建產(chǎn)品對象的公共接口,而工廠子類則負責生成具體的產(chǎn)品對象, 這樣做的目的是將產(chǎn)品類的實例化操作延遲到工廠子類中完成,即通過工廠子類來確定究竟應該實例化哪一個具體產(chǎn)品類。

在上圖中,阿寶哥模擬了用戶購車的流程,小王和小秦分別向 BMW 730 和 BMW 840 工廠訂購了 BMW730 和 BMW840 型號的車子,接著工廠按照對應的模型進行生產(chǎn)并在生產(chǎn)完成后交付給用戶。
同樣,我們來看一下如何使用工廠方法來描述 BMW 工廠生產(chǎn)指定型號車子的過程。
2.2.1 實現(xiàn)代碼
abstract?class?BMWFactory?{
??abstract?produceBMW():?BMW;
}
class?BMW730Factory?extends?BMWFactory?{
??produceBMW():?BMW?{
????return?new?BMW730();
??}
}
class?BMW840Factory?extends?BMWFactory?{
??produceBMW():?BMW?{
????return?new?BMW840();
??}
}
在以上代碼中,我們分別創(chuàng)建了 BMW730Factory 和 BMW840Factory 兩個工廠類,然后使用這兩個類的實例來生產(chǎn)不同型號的車子。
2.2.2 使用示例
const?bmw730Factory?=?new?BMW730Factory();
const?bmw840Factory?=?new?BMW840Factory();
const?bmw730?=?bmw730Factory.produceBMW();
const?bmw840?=?bmw840Factory.produceBMW();
bmw730.run();
bmw840.run();
2.2.3 應用場景
一個類不知道它所需要的對象的類:在工廠方法模式中,客戶端不需要知道具體產(chǎn)品類的類名,只需要知道所對應的工廠即可,具體的產(chǎn)品對象由具體工廠類創(chuàng)建;客戶端需要知道創(chuàng)建具體產(chǎn)品的工廠類。 一個類通過其子類來指定創(chuàng)建哪個對象:在工廠方法模式中,對于抽象工廠類只需要提供一個創(chuàng)建產(chǎn)品的接口,而由其子類來確定具體要創(chuàng)建的對象,利用面向?qū)ο蟮亩鄳B(tài)性和里氏代換原則,在程序運行時,子類對象將覆蓋父類對象,從而使得系統(tǒng)更容易擴展。
2.3 抽象工廠
抽象工廠模式(Abstract Factory Pattern),提供一個創(chuàng)建一系列相關(guān)或相互依賴對象的接口,而無須指定它們具體的類。
在工廠方法模式中具體工廠負責生產(chǎn)具體的產(chǎn)品,每一個具體工廠對應一種具體產(chǎn)品,工廠方法也具有唯一性,一般情況下,一個具體工廠中只有一個工廠方法或者一組重載的工廠方法。但是有時候我們需要一個工廠可以提供多個產(chǎn)品對象,而不是單一的產(chǎn)品對象。

在上圖中,阿寶哥模擬了用戶購車的流程,小王向 BMW 工廠訂購了 BMW730,工廠按照 730 對應的模型進行生產(chǎn)并在生產(chǎn)完成后交付給小王。而小秦向同一個 BMW 工廠訂購了 BMW840,工廠按照 840 對應的模型進行生產(chǎn)并在生產(chǎn)完成后交付給小秦。
下面我們來看一下如何使用抽象工廠來描述上述的購車過程。
2.3.1 實現(xiàn)代碼
abstract?class?BMWFactory?{
??abstract?produce730BMW():?BMW730;
??abstract?produce840BMW():?BMW840;
}
class?ConcreteBMWFactory?extends?BMWFactory?{
??produce730BMW():?BMW730?{
????return?new?BMW730();
??}
??produce840BMW():?BMW840?{
????return?new?BMW840();
??}
}
2.3.2 使用示例
const?bmwFactory?=?new?ConcreteBMWFactory();
const?bmw730?=?bmwFactory.produce730BMW();
const?bmw840?=?bmwFactory.produce840BMW();
bmw730.run();
bmw840.run();
2.3.3 應用場景
一個系統(tǒng)不應當依賴于產(chǎn)品類實例如何被創(chuàng)建、組合和表達的細節(jié),這對于所有類型的工廠模式都是重要的。 系統(tǒng)中有多于一個的產(chǎn)品族,而每次只使用其中某一產(chǎn)品族。 系統(tǒng)提供一個產(chǎn)品類的庫,所有的產(chǎn)品以同樣的接口出現(xiàn),從而使客戶端不依賴于具體實現(xiàn)。
三、單例模式
單例模式(Singleton Pattern)是一種常用的模式,有一些對象我們往往只需要一個,比如全局緩存、瀏覽器中的 window 對象等。單例模式用于保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。

在上圖中,阿寶哥模擬了借車的流程,小王臨時有急事找阿寶哥借車子,阿寶哥家的車子剛好沒用,就借給小王了。當天,小秦也需要用車子,也找阿寶哥借車,因為阿寶哥家里只有一輛車子,所以就沒有車可借了。
對于車子來說,它雖然給生活帶來了很大的便利,但養(yǎng)車也需要一筆不小的費用(車位費、油費和保養(yǎng)費等),所以阿寶哥家里只有一輛車子。
在開發(fā)軟件系統(tǒng)時,如果遇到創(chuàng)建對象時耗時過多或耗資源過多,但又經(jīng)常用到的對象,我們就可以考慮使用單例模式。
下面我們來看一下如何使用 TypeScript 來實現(xiàn)單例模式。
3.1 實現(xiàn)代碼
class?Singleton?{
??//?定義私有的靜態(tài)屬性,來保存對象實例
??private?static?singleton:?Singleton;
??private?constructor()?{}
??//?提供一個靜態(tài)的方法來獲取對象實例
??public?static?getInstance():?Singleton?{
????if?(!Singleton.singleton)?{
??????Singleton.singleton?=?new?Singleton();
????}
????return?Singleton.singleton;
??}
}
3.2 使用示例
let?instance1?=?Singleton.getInstance();
let?instance2?=?Singleton.getInstance();
console.log(instance1?===?instance2);?//?true
3.3 應用場景
需要頻繁實例化然后銷毀的對象。 創(chuàng)建對象時耗時過多或耗資源過多,但又經(jīng)常用到的對象。 系統(tǒng)只需要一個實例對象,如系統(tǒng)要求提供一個唯一的序列號生成器或資源管理器,或者需要考慮資源消耗太大而只允許創(chuàng)建一個對象。
四、適配器模式
在實際生活中,也存在適配器的使用場景,比如:港式插頭轉(zhuǎn)換器、電源適配器和 USB 轉(zhuǎn)接口。而在軟件工程中,適配器模式的作用是解決兩個軟件實體間的接口不兼容的問題。使用適配器模式之后,原本由于接口不兼容而不能工作的兩個軟件實體就可以一起工作。

4.1 實現(xiàn)代碼
interface?Logger?{
??info(message:?string):?Promise<void>;
}
interface?CloudLogger?{
??sendToServer(message:?string,?type:?string):?Promise<void>;
}
class?AliLogger?implements?CloudLogger?{
??public?async?sendToServer(message:?string,?type:?string):?Promise<void>?{
????console.info(message);
????console.info('This?Message?was?saved?with?AliLogger');
??}
}
class?CloudLoggerAdapter?implements?Logger?{
??protected?cloudLogger:?CloudLogger;
??constructor?(cloudLogger:?CloudLogger)?{
????this.cloudLogger?=?cloudLogger;
??}
??public?async?info(message:?string):?Promise<void>?{
????await?this.cloudLogger.sendToServer(message,?'info');
??}
}
class?NotificationService?{
??protected?logger:?Logger;
??
??constructor?(logger:?Logger)?{????
????this.logger?=?logger;
??}
??public?async?send(message:?string):?Promise<void>?{
????await?this.logger.info(`Notification?sended:?${message}`);
??}
}
在以上代碼中,因為 Logger 和 CloudLogger 這兩個接口不匹配,所以我們引入了 CloudLoggerAdapter 適配器來解決兼容性問題。
4.2 使用示例
(async?()?=>?{
??const?aliLogger?=?new?AliLogger();
??const?cloudLoggerAdapter?=?new?CloudLoggerAdapter(aliLogger);
??const?notificationService?=?new?NotificationService(cloudLoggerAdapter);
??await?notificationService.send('Hello?semlinker,?To?Cloud');
})();
4.3 應用場景及案例
以前開發(fā)的系統(tǒng)存在滿足新系統(tǒng)功能需求的類,但其接口同新系統(tǒng)的接口不一致。 使用第三方提供的組件,但組件接口定義和自己要求的接口定義不同。 Github - axios-mock-adapter:https://github.com/ctimmerm/axios-mock-adapter
五、觀察者模式 & 發(fā)布訂閱模式
5.1 觀察者模式
觀察者模式,它定義了一種一對多的關(guān)系,讓多個觀察者對象同時監(jiān)聽某一個主題對象,這個主題對象的狀態(tài)發(fā)生變化時就會通知所有的觀察者對象,使得它們能夠自動更新自己。
在觀察者模式中有兩個主要角色:Subject(主題)和 Observer(觀察者)。

在上圖中,Subject(主題)就是阿寶哥的 TS 專題文章,而觀察者就是小秦和小王。由于觀察者模式支持簡單的廣播通信,當消息更新時,會自動通知所有的觀察者。
下面我們來看一下如何使用 TypeScript 來實現(xiàn)觀察者模式。
5.1.1 實現(xiàn)代碼
interface?Observer?{
??notify:?Function;
}
class?ConcreteObserver?implements?Observer{
??constructor(private?name:?string)?{}
??notify()?{
????console.log(`${this.name}?has?been?notified.`);
??}
}
class?Subject?{?
??private?observers:?Observer[]?=?[];
??public?addObserver(observer:?Observer):?void?{
????console.log(observer,?"is?pushed!");
????this.observers.push(observer);
??}
??public?deleteObserver(observer:?Observer):?void?{
????console.log("remove",?observer);
????const?n:?number?=?this.observers.indexOf(observer);
????n?!=?-1?&&?this.observers.splice(n,?1);
??}
??public?notifyObservers():?void?{
????console.log("notify?all?the?observers",?this.observers);
????this.observers.forEach(observer?=>?observer.notify());
??}
}
5.1.2 使用示例
const?subject:?Subject?=?new?Subject();
const?xiaoQin?=?new?ConcreteObserver("小秦");
const?xiaoWang?=?new?ConcreteObserver("小王");
subject.addObserver(xiaoQin);
subject.addObserver(xiaoWang);
subject.notifyObservers();
subject.deleteObserver(xiaoQin);
subject.notifyObservers();
5.1.3 應用場景及案例
一個對象的行為依賴于另一個對象的狀態(tài)。或者換一種說法,當被觀察對象(目標對象)的狀態(tài)發(fā)生改變時 ,會直接影響到觀察對象的行為。 RxJS Subject:https://github.com/ReactiveX/rxjs/blob/master/src/internal/Subject.ts RxJS Subject 文檔:https://rxjs.dev/guide/subject
5.2 發(fā)布訂閱模式
在軟件架構(gòu)中,發(fā)布/訂閱是一種消息范式,消息的發(fā)送者(稱為發(fā)布者)不會將消息直接發(fā)送給特定的接收者(稱為訂閱者)。而是將發(fā)布的消息分為不同的類別,然后分別發(fā)送給不同的訂閱者。 同樣的,訂閱者可以表達對一個或多個類別的興趣,只接收感興趣的消息,無需了解哪些發(fā)布者存在。
在發(fā)布訂閱模式中有三個主要角色:Publisher(發(fā)布者)、 Channels(通道)和 Subscriber(訂閱者)。

在上圖中,Publisher(發(fā)布者)是阿寶哥,Channels(通道)中 Topic A 和 Topic B 分別對應于 TS 專題和 Deno 專題,而 Subscriber(訂閱者)就是小秦、小王和小池。
下面我們來看一下如何使用 TypeScript 來實現(xiàn)發(fā)布訂閱模式。
5.2.1 實現(xiàn)代碼
type?EventHandler?=?(...args:?any[])?=>?any;
class?EventEmitter?{
??private?c?=?new?Map<string,?EventHandler[]>();
??//?訂閱指定的主題
??subscribe(topic:?string,?...handlers:?EventHandler[])?{
????let?topics?=?this.c.get(topic);
????if?(!topics)?{
??????this.c.set(topic,?topics?=?[]);
????}
????topics.push(...handlers);
??}
??//?取消訂閱指定的主題
??unsubscribe(topic:?string,?handler?:?EventHandler):?boolean?{
????if?(!handler)?{
??????return?this.c.delete(topic);
????}
????const?topics?=?this.c.get(topic);
????if?(!topics)?{
??????return?false;
????}
????
????const?index?=?topics.indexOf(handler);
????if?(index?0)?{
??????return?false;
????}
????topics.splice(index,?1);
????if?(topics.length?===?0)?{
??????this.c.delete(topic);
????}
????return?true;
??}
??//?為指定的主題發(fā)布消息
??publish(topic:?string,?...args:?any[]):?any[]?|?null?{
????const?topics?=?this.c.get(topic);
????if?(!topics)?{
??????return?null;
????}
????return?topics.map(handler?=>?{
??????try?{
????????return?handler(...args);
??????}?catch?(e)?{
????????console.error(e);
????????return?null;
??????}
????});
??}
}
5.2.2 使用示例
const?eventEmitter?=?new?EventEmitter();
eventEmitter.subscribe("ts",?(msg)?=>?console.log(`收到訂閱的消息:${msg}`)?);
eventEmitter.publish("ts",?"TypeScript發(fā)布訂閱模式");
eventEmitter.unsubscribe("ts");
eventEmitter.publish("ts",?"TypeScript發(fā)布訂閱模式");
5.2.3 應用場景
對象間存在一對多關(guān)系,一個對象的狀態(tài)發(fā)生改變會影響其他對象。 作為事件總線,來實現(xiàn)不同組件間或模塊間的通信。 BetterScroll - EventEmitter:https://github.com/ustbhuangyi/better-scroll/blob/dev/packages/shared-utils/src/events.ts EventEmitter 在插件化架構(gòu)的應用:https://mp.weixin.qq.com/s/N4iw3bi0bxJ57J8EAp5ctQ
六、策略模式
策略模式(Strategy Pattern)定義了一系列的算法,把它們一個個封裝起來,并且使它們可以互相替換。策略模式的重心不是如何實現(xiàn)算法,而是如何組織、調(diào)用這些算法,從而讓程序結(jié)構(gòu)更靈活、可維護、可擴展。

目前在一些主流的 Web 站點中,都提供了多種不同的登錄方式。比如賬號密碼登錄、手機驗證碼登錄和第三方登錄。為了方便維護不同的登錄方式,我們可以把不同的登錄方式封裝成不同的登錄策略。
下面我們來看一下如何使用策略模式來封裝不同的登錄方式。
6.1 實現(xiàn)代碼
為了更好地理解以下代碼,我們先來看一下對應的 UML 類圖:

interface?Strategy?{
??authenticate(...args:?any):?any;
}
class?Authenticator?{
??strategy:?any;
??constructor()?{
????this.strategy?=?null;
??}
??setStrategy(strategy:?any)?{
????this.strategy?=?strategy;
??}
??authenticate(...args:?any)?{
????if?(!this.strategy)?{
??????console.log('尚未設(shè)置認證策略');
??????return;
????}
????return?this.strategy.authenticate(...args);
??}
}
class?WechatStrategy?implements?Strategy?{
??authenticate(wechatToken:?string)?{
????if?(wechatToken?!==?'123')?{
??????console.log('無效的微信用戶');
??????return;
????}
????console.log('微信認證成功');
??}
}
class?LocalStrategy?implements?Strategy?{
??authenticate(username:?string,?password:?string)?{
????if?(username?!==?'abao'?&&?password?!==?'123')?{
??????console.log('賬號或密碼錯誤');
??????return;
????}
????console.log('賬號和密碼認證成功');
??}
}
6.2 使用示例
const?auth?=?new?Authenticator();
auth.setStrategy(new?WechatStrategy());
auth.authenticate('123456');
auth.setStrategy(new?LocalStrategy());
auth.authenticate('abao',?'123');
6.3 應用場景及案例
一個系統(tǒng)需要動態(tài)地在幾種算法中選擇一種時,可將每個算法封裝到策略類中。 多個類只區(qū)別在表現(xiàn)行為不同,可以使用策略模式,在運行時動態(tài)選擇具體要執(zhí)行的行為。 一個類定義了多種行為,并且這些行為在這個類的操作中以多個條件語句的形式出現(xiàn),可將每個條件分支移入它們各自的策略類中以代替這些條件語句。 Github - passport-local:https://github.com/jaredhanson/passport-local Github - passport-oauth2:https://github.com/jaredhanson/passport-oauth2 Github - zod:https://github.com/vriad/zod/blob/master/src/types/string.ts
七、職責鏈模式
職責鏈模式是使多個對象都有機會處理請求,從而避免請求的發(fā)送者和接受者之間的耦合關(guān)系。在職責鏈模式里,很多對象由每一個對象對其下家的引用而連接起來形成一條鏈。請求在這個鏈上傳遞,直到鏈上的某一個對象決定處理此請求。

在公司中不同的崗位擁有不同的職責與權(quán)限。以上述的請假流程為例,當阿寶哥請 1 天假時,只要組長審批就可以了,不需要流轉(zhuǎn)到主管和總監(jiān)。如果職責鏈上的某個環(huán)節(jié)無法處理當前的請求,若含有下個環(huán)節(jié),則會把請求轉(zhuǎn)交給下個環(huán)節(jié)來處理。
在日常的軟件開發(fā)過程中,對于職責鏈來說,一種常見的應用場景是中間件,下面我們來看一下如何利用職責鏈來處理請求。
7.1 實現(xiàn)代碼
為了更好地理解以下代碼,我們先來看一下對應的 UML 類圖:

interface?IHandler?{
??addMiddleware(h:?IHandler):?IHandler;
??get(url:?string,?callback:?(data:?any)?=>?void):?void;
}
abstract?class?AbstractHandler?implements?IHandler?{
??next!:?IHandler;
??addMiddleware(h:?IHandler)?{
????this.next?=?h;
????return?this.next;
??}
??get(url:?string,?callback:?(data:?any)?=>?void)?{
????if?(this.next)?{
??????return?this.next.get(url,?callback);
????}
??}
}
//?定義Auth中間件
class?Auth?extends?AbstractHandler?{
??isAuthenticated:?boolean;
??constructor(username:?string,?password:?string)?{
????super();
????this.isAuthenticated?=?false;
????if?(username?===?'abao'?&&?password?===?'123')?{
??????this.isAuthenticated?=?true;
????}
??}
??get(url:?string,?callback:?(data:?any)?=>?void)?{
????if?(this.isAuthenticated)?{
??????return?super.get(url,?callback);
????}?else?{
??????throw?new?Error('Not?Authorized');
????}
??}
}
//?定義Logger中間件
class?Logger?extends?AbstractHandler?{
??get(url:?string,?callback:?(data:?any)?=>?void)?{
????console.log('/GET?Request?to:?',?url);
????return?super.get(url,?callback);
??}
}
class?Route?extends?AbstractHandler?{
??URLMaps:?{[key:?string]:?any};
??constructor()?{
????super();
????this.URLMaps?=?{
??????'/api/todos':?[{?title:?'learn?ts'?},?{?title:?'learn?react'?}],
??????'/api/random':?Math.random(),
????};
??}
??get(url:?string,?callback:?(data:?any)?=>?void)?{
????super.get(url,?callback);
????if?(this.URLMaps.hasOwnProperty(url))?{
??????callback(this.URLMaps[url]);
????}
??}
}
7.2 使用示例
const?route?=?new?Route();
route.addMiddleware(new?Auth('abao',?'123')).addMiddleware(new?Logger());
route.get('/api/todos',?data?=>?{
??console.log(JSON.stringify({?data?},?null,?2));
});
route.get('/api/random',?data?=>?{
??console.log(data);
});
7.3 應用場景
可處理一個請求的對象集合應被動態(tài)指定。 想在不明確指定接收者的情況下,向多個對象中的一個提交一個請求。 有多個對象可以處理一個請求,哪個對象處理該請求運行時自動確定,客戶端只需要把請求提交到鏈上即可。
八、模板方法模式
模板方法模式由兩部分結(jié)構(gòu)組成:抽象父類和具體的實現(xiàn)子類。通常在抽象父類中封裝了子類的算法框架,也包括實現(xiàn)一些公共方法以及封裝子類中所有方法的執(zhí)行順序。子類通過繼承這個抽象類,也繼承了整個算法結(jié)構(gòu),并且可以選擇重寫父類的方法。

在上圖中,阿寶哥通過使用不同的解析器來分別解析 CSV 和 Markup 文件。雖然解析的是不同的類型的文件,但文件的處理流程是一樣的。這里主要包含讀取文件、解析文件和打印數(shù)據(jù)三個步驟。針對這個場景,我們就可以引入模板方法來封裝以上三個步驟的處理順序。
下面我們來看一下如何使用模板方法來實現(xiàn)上述的解析流程。
8.1 實現(xiàn)代碼
為了更好地理解以下代碼,我們先來看一下對應的 UML 類圖:

import?fs?from?'fs';
abstract?class?DataParser?{
??data:?string?=?'';
??out:?any?=?null;
??//?這就是所謂的模板方法
??parse(pathUrl:?string)?{
????this.readFile(pathUrl);
????this.doParsing();
????this.printData();
??}
??readFile(pathUrl:?string)?{
????this.data?=?fs.readFileSync(pathUrl,?'utf8');
??}
??abstract?doParsing():?void;
??
??printData()?{
????console.log(this.out);
??}
}
class?CSVParser?extends?DataParser?{
??doParsing()?{
????this.out?=?this.data.split(',');
??}
}
class?MarkupParser?extends?DataParser?{
??doParsing()?{
????this.out?=?this.data.match(/<\w+>.*<\/\w+>/gim);
??}
}
8.2 使用示例
const?csvPath?=?'./data.csv';
const?mdPath?=?'./design-pattern.md';
new?CSVParser().parse(csvPath);
new?MarkupParser().parse(mdPath);
8.3 應用場景
算法的整體步驟很固定,但其中個別部分易變時,這時候可以使用模板方法模式,將容易變的部分抽象出來,供子類實現(xiàn)。 當需要控制子類的擴展時,模板方法只在特定點調(diào)用鉤子操作,這樣就只允許在這些點進行擴展。
九、參考資源
維基百科 - 設(shè)計模式 Java設(shè)計模式:23種設(shè)計模式全面解析 Design Patterns Everyday
掃碼關(guān)注公眾號,訂閱更多精彩內(nèi)容。

