在軟件工程中,設計模式(Design Pattern)是對軟件設計中廣泛存在(反覆出現)的各類問題,所提出的解決方案。根據模式的目的來劃分的話,GoF(Gang of Four)設計模式能夠分爲如下 3 種類型:node
一、建立型模式:用來描述 「如何建立對象」,它的主要特色是 「將對象的建立和使用分離」。包括單例、原型、工廠方法、抽象工廠和建造者 5 種模式。react
二、結構型模式:用來描述如何將類或對象按照某種佈局組成更大的結構。包括代理、適配器、橋接、裝飾、外觀、享元和組合 7 種模式。ios
三、行爲型模式:用來識別對象之間的經常使用交流模式以及如何分配職責。包括模板方法、策略、命令、職責鏈、狀態、觀察者、中介者、迭代器、訪問者、備忘錄和解釋器 11 種模式。git
接下來阿寶哥將結合一些生活中的場景並經過精美的配圖,來向你們介紹 9 種經常使用的設計模式。github
建造者模式(Builder Pattern)將一個複雜對象分解成多個相對簡單的部分,而後根據不一樣須要分別建立它們,最後構建成該複雜對象。算法
一輛小汽車 🚗 一般由 發動機、底盤、車身和電氣設備 四大部分組成。汽車電氣設備的內部構造很複雜,簡單起見,咱們只考慮三個部分:引擎、底盤和車身。sql
在現實生活中,小汽車也是由不一樣的零部件組裝而成,好比上圖中咱們把小汽車分紅引擎、底盤和車身三大部分。下面咱們來看一下如何使用建造者模式來造車子。typescript
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 個部分都組裝完成後,調用 build
方法就能夠開始造車。axios
const car = new CarBuilder()
.addEngine('v12')
.addBody('鎂合金')
.addChassis('複合材料')
.build();
複製代碼
在現實生活中,工廠是負責生產產品的,好比牛奶、麪包或禮物等,這些產品知足了咱們平常的生理需求。設計模式
在衆多設計模式當中,有一種被稱爲工廠模式的設計模式,它提供了建立對象的最佳方式。工廠模式能夠分爲:簡單工廠模式、工廠方法模式和抽象工廠模式。
簡單工廠模式又叫 靜態方法模式,由於工廠類中定義了一個靜態方法用於建立對象。簡單工廠讓使用者不用知道具體的參數就能夠建立出所需的 」產品「 類,即便用者能夠直接消費產品而不須要知道產品的具體生產細節。
在上圖中,阿寶哥模擬了用戶購車的流程,小王和小秦分別向 BMW 工廠訂購了 BMW730 和 BMW840 型號的車型,接着工廠會先判斷用戶選擇的車型,而後按照對應的模型進行生產並在生產完成後交付給用戶。
下面咱們來看一下如何使用簡單工廠來描述 BMW 工廠生產指定型號車子的過程。
abstract class BMW {
abstract run(): void;
}
class BMW730 extends BMW {
run(): void {
console.log("BMW730 發動咯");
}
}
class BMW840 extends BMW {
run(): void {
console.log("BMW840 發動咯");
}
}
class BMWFactory {
public static produceBMW(model: "730" | "840"): BMW {
if (model === "730") {
return new BMW730();
} else {
return new BMW840();
}
}
}
複製代碼
在以上代碼中,咱們定義一個 BMWFactory
類,該類提供了一個靜態的 produceBMW()
方法,用於根據不一樣的模型參數來建立不一樣型號的車子。
const bmw730 = BMWFactory.produceBMW("730");
const bmw840 = BMWFactory.produceBMW("840");
bmw730.run();
bmw840.run();
複製代碼
工廠方法模式(Factory Method Pattern)又稱爲工廠模式,也叫多態工廠(Polymorphic Factory)模式,它屬於類建立型模式。
在工廠方法模式中,工廠父類負責定義建立產品對象的公共接口,而工廠子類則負責生成具體的產品對象, 這樣作的目的是將產品類的實例化操做延遲到工廠子類中完成,即經過工廠子類來肯定究竟應該實例化哪個具體產品類。
在上圖中,阿寶哥模擬了用戶購車的流程,小王和小秦分別向 BMW 730 和 BMW 840 工廠訂購了 BMW730 和 BMW840 型號的車子,接着工廠按照對應的模型進行生產並在生產完成後交付給用戶。
一樣,咱們來看一下如何使用工廠方法來描述 BMW 工廠生產指定型號車子的過程。
abstract class BMWFactory {
abstract produceBMW(): BMW;
}
class BMW730Factory extends BMWFactory {
produceBMW(): BMW {
return new BMW730();
}
}
class BMW840Factory extends BMWFactory {
produceBMW(): BMW {
return new BMW840();
}
}
複製代碼
在以上代碼中,咱們分別建立了 BMW730Factory
和 BMW840Factory
兩個工廠類,而後使用這兩個類的實例來生產不一樣型號的車子。
const bmw730Factory = new BMW730Factory();
const bmw840Factory = new BMW840Factory();
const bmw730 = bmw730Factory.produceBMW();
const bmw840 = bmw840Factory.produceBMW();
bmw730.run();
bmw840.run();
複製代碼
繼續閱讀:Typescript 設計模式之工廠方法
抽象工廠模式(Abstract Factory Pattern),提供一個建立一系列相關或相互依賴對象的接口,而無須指定它們具體的類。
在工廠方法模式中具體工廠負責生產具體的產品,每個具體工廠對應一種具體產品,工廠方法也具備惟一性,通常狀況下,一個具體工廠中只有一個工廠方法或者一組重載的工廠方法。 可是有時候咱們須要一個工廠能夠提供多個產品對象,而不是單一的產品對象。
在上圖中,阿寶哥模擬了用戶購車的流程,小王向 BMW 工廠訂購了 BMW730,工廠按照 730 對應的模型進行生產並在生產完成後交付給小王。而小秦向同一個 BMW 工廠訂購了 BMW840,工廠按照 840 對應的模型進行生產並在生產完成後交付給小秦。
下面咱們來看一下如何使用抽象工廠來描述上述的購車過程。
abstract class BMWFactory {
abstract produce730BMW(): BMW730;
abstract produce840BMW(): BMW840;
}
class ConcreteBMWFactory extends BMWFactory {
produce730BMW(): BMW730 {
return new BMW730();
}
produce840BMW(): BMW840 {
return new BMW840();
}
}
複製代碼
const bmwFactory = new ConcreteBMWFactory();
const bmw730 = bmwFactory.produce730BMW();
const bmw840 = bmwFactory.produce840BMW();
bmw730.run();
bmw840.run();
複製代碼
繼續閱讀:建立對象的最佳方式是什麼?
單例模式(Singleton Pattern)是一種經常使用的模式,有一些對象咱們每每只須要一個,好比全局緩存、瀏覽器中的 window 對象等。單例模式用於保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。
在上圖中,阿寶哥模擬了借車的流程,小王臨時有急事找阿寶哥借車子,阿寶哥家的車子恰好沒用,就借給小王了。當天,小秦也須要用車子,也找阿寶哥借車,由於阿寶哥家裏只有一輛車子,因此就沒有車可借了。
對於車子來講,它雖然給生活帶來了很大的便利,但養車也須要一筆不小的費用(車位費、油費和保養費等),因此阿寶哥家裏只有一輛車子。在開發軟件系統時,若是遇到建立對象時耗時過多或耗資源過多,但又常常用到的對象,咱們就能夠考慮使用單例模式。
下面咱們來看一下如何使用 TypeScript 來實現單例模式。
class Singleton {
// 定義私有的靜態屬性,來保存對象實例
private static singleton: Singleton;
private constructor() {}
// 提供一個靜態的方法來獲取對象實例
public static getInstance(): Singleton {
if (!Singleton.singleton) {
Singleton.singleton = new Singleton();
}
return Singleton.singleton;
}
}
複製代碼
let instance1 = Singleton.getInstance();
let instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true
複製代碼
繼續閱讀:TypeScript 設計模式之單例模式
在實際生活中,也存在適配器的使用場景,好比:港式插頭轉換器、電源適配器和 USB 轉接口。而在軟件工程中,適配器模式的做用是解決兩個軟件實體間的接口不兼容的問題。 使用適配器模式以後,本來因爲接口不兼容而不能工做的兩個軟件實體就能夠一塊兒工做。
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
適配器來解決兼容性問題。
(async () => {
const aliLogger = new AliLogger();
const cloudLoggerAdapter = new CloudLoggerAdapter(aliLogger);
const notificationService = new NotificationService(cloudLoggerAdapter);
await notificationService.send('Hello semlinker, To Cloud');
})();
複製代碼
觀察者模式,它定義了一種一對多的關係,讓多個觀察者對象同時監聽某一個主題對象,這個主題對象的狀態發生變化時就會通知全部的觀察者對象,使得它們可以自動更新本身。
在觀察者模式中有兩個主要角色:Subject(主題)和 Observer(觀察者)。
在上圖中,Subject(主題)就是阿寶哥的 TS 專題文章,而觀察者就是小秦和小王。因爲觀察者模式支持簡單的廣播通訊,當消息更新時,會自動通知全部的觀察者。
下面咱們來看一下如何使用 TypeScript 來實現觀察者模式。
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());
}
}
複製代碼
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();
複製代碼
在軟件架構中,發佈/訂閱是一種消息範式,消息的發送者(稱爲發佈者)不會將消息直接發送給特定的接收者(稱爲訂閱者)。而是將發佈的消息分爲不一樣的類別,而後分別發送給不一樣的訂閱者。 一樣的,訂閱者能夠表達對一個或多個類別的興趣,只接收感興趣的消息,無需瞭解哪些發佈者存在。
在發佈訂閱模式中有三個主要角色:Publisher(發佈者)、 Channels(通道)和 Subscriber(訂閱者)。
在上圖中,Publisher(發佈者)是阿寶哥,Channels(通道)中 Topic A 和 Topic B 分別對應於 TS 專題和 Deno 專題,而 Subscriber(訂閱者)就是小秦、小王和小池。
下面咱們來看一下如何使用 TypeScript 來實現發佈訂閱模式。
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;
}
// 爲指定的主題發佈消息
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;
}
});
}
}
複製代碼
const eventEmitter = new EventEmitter();
eventEmitter.subscribe("ts", (msg) => console.log(`收到訂閱的消息:${msg}`) );
eventEmitter.publish("ts", "TypeScript發佈訂閱模式");
eventEmitter.unsubscribe("ts");
eventEmitter.publish("ts", "TypeScript發佈訂閱模式");
複製代碼
繼續閱讀:如何優雅的實現消息通訊?
策略模式(Strategy Pattern)定義了一系列的算法,把它們一個個封裝起來,而且使它們能夠互相替換。策略模式的重心不是如何實現算法,而是如何組織、調用這些算法,從而讓程序結構更靈活、可維護、可擴展。
目前在一些主流的 Web 站點中,都提供了多種不一樣的登陸方式。好比帳號密碼登陸、手機驗證碼登陸和第三方登陸。爲了方便維護不一樣的登陸方式,咱們能夠把不一樣的登陸方式封裝成不一樣的登陸策略。
下面咱們來看一下如何使用策略模式來封裝不一樣的登陸方式。
爲了更好地理解如下代碼,咱們先來看一下對應的 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('還沒有設置認證策略');
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('帳號和密碼認證成功');
}
}
複製代碼
const auth = new Authenticator();
auth.setStrategy(new WechatStrategy());
auth.authenticate('123456');
auth.setStrategy(new LocalStrategy());
auth.authenticate('abao', '123');
複製代碼
職責鏈模式是使多個對象都有機會處理請求,從而避免請求的發送者和接受者之間的耦合關係。在職責鏈模式裏,不少對象由每個對象對其下家的引用而鏈接起來造成一條鏈。請求在這個鏈上傳遞,直到鏈上的某一個對象決定處理此請求。
在公司中不一樣的崗位擁有不一樣的職責與權限。以上述的請假流程爲例,當阿寶哥請 1 天假時,只要組長審批就能夠了,不須要流轉到主管和總監。若是職責鏈上的某個環節沒法處理當前的請求,若含有下個環節,則會把請求轉交給下個環節來處理。
在平常的軟件開發過程當中,對於職責鏈來講,一種常見的應用場景是中間件,下面咱們來看一下如何利用職責鏈來處理請求。
爲了更好地理解如下代碼,咱們先來看一下對應的 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]);
}
}
}
複製代碼
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);
});
複製代碼
模板方法模式由兩部分結構組成:抽象父類和具體的實現子類。一般在抽象父類中封裝了子類的算法框架,也包括實現一些公共方法以及封裝子類中全部方法的執行順序。子類經過繼承這個抽象類,也繼承了整個算法結構,而且能夠選擇重寫父類的方法。
在上圖中,阿寶哥經過使用不一樣的解析器來分別解析 CSV 和 Markup 文件。雖然解析的是不一樣的類型的文件,但文件的處理流程是同樣的。這裏主要包含讀取文件、解析文件和打印數據三個步驟。針對這個場景,咱們就能夠引入模板方法來封裝以上三個步驟的處理順序。
下面咱們來看一下如何使用模板方法來實現上述的解析流程。
爲了更好地理解如下代碼,咱們先來看一下對應的 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);
}
}
複製代碼
const csvPath = './data.csv';
const mdPath = './design-pattern.md';
new CSVParser().parse(csvPath);
new MarkupParser().parse(mdPath);
複製代碼