【翻譯】JavaScript 設計模式

Ivan Chiou
49 min readMar 26, 2020

--

原文: https://medium.com/better-programming/javascript-design-patterns-25f0faaaa15

更新訊息: 更新代理人模式(Proxy Pattern)的範例,該範例使用ES6的Proxy和 Reflect. 把程式碼片段的圖片放置到GitHub gists。

在這一篇文章裡,我們將討論可以使我們寫出更好,更可維護的Javascript程式碼的設計模式。先假設你對Javascript有一些基本的了解,比如像是類別(classes), 物件(objects), 原型繼承(prototypal inheritance), 閉包(closures)等概念。

這篇文章因為這個主題的關係,讀起來有點長需要一點時間。但我試著保持每個章節的獨立性,所以你可以選擇你想閱讀的部分,並忽略掉你不感興趣的或者你已經了解的部分。現在,就讓我們開始!

補充: 所有設計模式解說的實作程式碼都在這裡

簡介

我們透過寫程式來解決問題。這些問題通常都有許多相似之處。而且,當我們試著去解決它們的時候,我們會發現有幾個常見的模式。這就是我們這邊所說的設計模式(design pattern)。

設計模式通常是被使用在軟體工程中的一個名詞,其代表著替軟體設計中常見發生的問題,提供一個可重複使用的解決方案

這些設計模式根本的概念,從非常一開始就已經出現在軟體工程的領域裡,只是沒有被很正式地提出來。由最著名的四人幫,Erich Gamma, Richard Helm, Ralph Johnson, 和John Vlissides所共同撰寫的設計模式:可復用物件導向軟體的基礎(Design Patterns: Elements Of Reusable Object-Oriented Software),大力促成設計模式成為軟體工程中的正統概念。現在,設計模式已成為軟體開發中最主要的部份且已行之有年。

這本書介紹了23種設計模式。

以上為四人幫所提出的23個典型的模式。

設計模式的益處有好幾種解釋。且已經被很多該領域的老鳥試過與驗證過這些解決方案。透過這些設計模式,確實地達到以廣泛接受性地方式解決問題,以及反映出這些定義問題的領先開發者的經驗與見解。當要極大地加快開發流程時,設計模式也使你的程式碼更容易重複使用與增加可讀性。

設計模式的意義不在完成解決方案。而是在提供我們更好的方法與解決問題的方案。

補充: 在這篇文章裡,我們將主要從物件導向的觀點以及在現代Javascript易用性背景之下,闡述設計模式。這也是為什麼一些四人幫所提出的典型設計模式將會被忽略,但一些由Addy Osmani撰寫的書JavaScript設計模式 (Learning JavaScript Design Patterns)中所提出的現代設計模式,將會被包含在其中。這些範例將保持簡單且容易被了解,因此並非這些範例中的每一個設計模式都提供了最佳實作的範例。

設計模式的分類

設計模式經常被分類為三種主要類型。

創建型模式

(Ivan:在建構物件時做變化)

如同名稱所述,這種模式主要是為了處理物件的創建機制。創建型模式基本上是藉由控制物件創建的流程來解決問題。

我們將細部討論以下的設計模式: 建構子模式(Constructor Pattern),工廠模式(Factory Pattern),原型模式(Prototype Pattern),和單例模式(Singleton Pattern)。

結構型模式

(Ivan:改變物件與物件之間的結構,在呼叫某個功能時做變化)

這個模式著重在類別與物件的組合。它們在沒有影響整個系統的狀況下,協助組織或重新組織其中一個或多個部分。換句話說,它們幫助我們在沒有篡改原先功能的狀況下,取得新的功能。我們將詳細討論以下設計模式: 配接器模式(Adapter Pattern), 組合模式(Composite Pattern), 裝飾者模式(Decorator Pattern), 表象模式(Façade Pattern), 享元模式(Flyweight Pattern), 和代理人模式(Proxy Pattern).

行為型模式

(Ivan:改變使用物件的行為,在使用某功能時所呼叫的方法不同)

這個設計模式主要是關於增進不相似的物件之間的溝通。

我們將詳細討論以下設計模式: 責任鏈模式(Chain of Responsibility Pattern), 命令模式(Command Pattern), 疊代器模式(Iterator Pattern), 中介者模式(Mediator Pattern), 觀察者模式(Observer Pattern), 狀態模式(State Pattern), 策略模式(Strategy Pattern), 和模板方法(Template Pattern)

建構子模式(Constructor Pattern)

這個是以類別為基底的創建式設計模式。建構子是一個特殊的function可以用來實例化新物件中所定義的方法與屬性。

這不是來自於典型的設計模式。事實上,這是一個在大部份物件導向語言中比設計模式更常見的基礎語言架構。但是在Javascript,物件可以很自由的不需要建構子的功能或類別的定義。因此,我想最重要一個簡單的開始,就是制定好這些設計模式的基礎建設。

建構子模式是Javascript裡,最普遍使用來創建給定新物件的一種設計模式。

在這個範例中,我們定義一個有name和specialAbility屬性,以及getDtails方法的Hero類別。接著,我們藉由new關鍵字給予個別的屬性值當作參數,調用建構子的方法,實例化一個物件IronMan。

// traditional Function-based syntax
function Hero(name, specialAbility) {
// setting property values
this.name = name;
this.specialAbility = specialAbility;

// declaring a method on the object
this.getDetails = function() {
return this.name + ' can ' + this.specialAbility;
};
}

// ES6 Class syntax
class Hero {
constructor(name, specialAbility) {
// setting property values
this._name = name;
this._specialAbility = specialAbility;

// declaring a method on the object
this.getDetails = function() {
return `${this._name} can ${this._specialAbility}`;
};
}
}

// creating new instances of Hero
const IronMan = new Hero('Iron Man', 'fly');

console.log(IronMan.getDetails()); // Iron Man can fly

工廠模式(Factory Pattern)

工廠模式是另一種類別為基礎的創建型模式。在這裡,我們將提供通用的介面委派物件實例化的職責至其子類別之中。

這個模式經常被使用在,我們需要同時管理與操作,多個有相似特性卻仍不同的物件集合。

在這個例子裡,我們創建一個工廠類別"BallFactory",它有一個方法可傳入參數,並依據這些參數,委派物件實例化的職責給不同的類別。如果參數是”football”或者”soccer” 其物件實例化就由”Football”類別處理。但如果是”basketball”物件實例化就由”Basketball”類別處理。

class BallFactory {
constructor() {
this.createBall = function(type) {
let ball;
if (type === 'football' || type === 'soccer') ball = new Football();
else if (type === 'basketball') ball = new Basketball();
ball.roll = function() {
return `The ${this._type} is rolling.`;
};
return ball;
};
}
}
class Football {
constructor() {
this._type = 'football';
this.kick = function() {
return 'You kicked the football.';
};
}
}
class Basketball {
constructor() {
this._type = 'basketball';
this.bounce = function() {
return 'You bounced the basketball.';
};
}
}
// creating objects
const factory = new BallFactory();
const myFootball = factory.createBall('football');
const myBasketball = factory.createBall('basketball');
console.log(myFootball.roll()); // The football is rolling.
console.log(myBasketball.roll()); // The basketball is rolling.
console.log(myFootball.kick()); // You kicked the football.
console.log(myBasketball.bounce()); // You bounced the basketball.

原型模式(Prototype Pattern)

原型模式是另一種物件為基礎的創建型模式。在這裡,我們使用一連串存在物件的骨架去創建或實例化新的物件。

這個設計模式特別重要且對於Javascript很有益處。因為Javascript使用原型繼承代替類別式物件導向。因此,這個模式發揮Javascript的長處且原生就支援。

在這篇範例中,我們有一個car物件被作為prototype,並使用Javascript的object.create特性去創建另一個物件myCar,裡面並定義了額外的屬性owner。

// using Object.create as was recommended by ES5 standard
const car = {
noOfWheels: 4,
start() {
return 'started';
},
stop() {
return 'stopped';
},
};
// Object.create(proto[, propertiesObject])const myCar = Object.create(car, { owner: { value: 'John' } });console.log(myCar.__proto__ === car); // true

單例模式(Singleton Pattern)

單例是一種特別的創建型模式,該類別只能存在唯一的實例。它用起來就像是: 如果該實例的類別沒有存在任何實例化物件的話,則會創建一個新的實例並回傳它;但如果該實例已存在,則會回傳該實例的參考。

一個完美真實案例就是mongoose(著名的MongoDB在Node.js上使用ODM的library)。它使用了單例模式。

在這個範例,我們有一個使用單例名叫Database的類別。首先,我們藉由new運算子調用Database類別的建構子去創建一個物件叫做mongo。這時,因為沒有存在任何實例,所以一個物件被實例化出來了。第二次調用,我們創建了一個mysql物件,但沒有新的物件被實例化,取而代之的是,回傳了之前我們所實例化的物件,mongo的參考。

class Database {
constructor(data) {
if (Database.exists) {
return Database.instance;
}
this._data = data;
Database.instance = this;
Database.exists = true;
return this;
}
getData() {
return this._data;
}
setData(data) {
this._data = data;
}
}
// usage
const mongo = new Database('mongo');
console.log(mongo.getData()); // mongo
const mysql = new Database('mysql');
console.log(mysql.getData()); // mongo

介面卡模式(Adapter Pattern)

(Ivan: 新的API與舊的API共用要相容時使用)

這是一個結構型模式。它提供一個介面(interface)將類別可轉換至另外一個。這種模式讓多個類別一起使用且可阻擋其他不符合介面的類別。

這個模式經常被使用來,同時包裝新的重構好的API,且讓舊的API依然可以使用。這經常被實作在新的開發或者程式碼重構(為了取得更好的性能)時產生不同的API,但有些系統仍然使用舊的API,而彼此之間必須相容的狀況下。

舉個例子,我們有一個舊的API,叫做oldCalcaulator類別,以及一個新的API,叫做NewCalculator類別。這個oldCalcaulator類別提供一個operation方法同時包含增加和減少的功能,然後新的類別NewCalculator卻是把這兩個功能分開為兩個方法。而Adapter類別CalcAdapter包裝了NewCalculator 並增加了operation的方法作為公開的API,但在這種情況下,它裡面其實是使用它自己的增加和減少的方法來實作。

// old interface
class OldCalculator {
constructor() {
this.operations = function(term1, term2, operation) {
switch (operation) {
case 'add':
return term1 + term2;
case 'sub':
return term1 - term2;
default:
return NaN;
}
};
}
}
// new interface
class NewCalculator {
constructor() {
this.add = function(term1, term2) {
return term1 + term2;
};
this.sub = function(term1, term2) {
return term1 - term2;
};
}
}
// Adapter Class
class CalcAdapter {
constructor() {
const newCalc = new NewCalculator();
this.operations = function(term1, term2, operation) {
switch (operation) {
case 'add':
// using the new implementation under the hood
return newCalc.add(term1, term2);
case 'sub':
return newCalc.sub(term1, term2);
default:
return NaN;
}
};
}
}
// usage
const oldCalc = new OldCalculator();
console.log(oldCalc.operations(10, 5, 'add')); // 15
const newCalc = new NewCalculator();
console.log(newCalc.add(10, 5)); // 15
const adaptedCalc = new CalcAdapter();
console.log(adaptedCalc.operations(10, 5, 'add')); // 15;

組合模式(Composite Pattern)

這是結構型設計模式,組合物件如樹狀結構來呈現繼承關係。在這個模式,每一個樹狀結構中的節點,可以是獨立的物件,或者是物件的組合。但無論如何,每一個節點都會被一致性地對待。

一個多層次的選單結構

要圖像化這個模式有點複雜。最簡單去思考這個模式的方法,就是拿多層次選單當作例子。每一個節點可以是一個單獨的選項,或者可以是一個子選單,裡面有包含多個選項。如果一個元件節點有子元件就是一個組合元件,如果一個元件節點沒有子元件則是葉子元件。

在這個例子,我們創建一個元件的基礎類別,它實作了需要的通用功能,以及需要的抽相功能。這個基礎類別也有一個靜態方法,使用遞迴方式去遍歷一個由該類別的子類別組合而成的樹狀結構。然後我們創建兩個子類別去擴展基礎類別。一個叫做leaf,沒有子元件;一個叫做Composite,它可以有子元件,且還包含一些方法,能處理增加、搜尋、移除子元件的功能。在這個範例中,這兩個子類別就是被用來創建組合樹的結構。

class Component {
constructor(name) {
this._name = name;
}
getNodeName() {
return this._name;
}
// abstract methods that need to be overridden
getType() {}
addChild(component) {}removeChildByName(componentName) {}removeChildByIndex(index) {}getChildByName(componentName) {}getChildByIndex(index) {}noOfChildren() {}static logTreeStructure(root) {
let treeStructure = '';
function traverse(node, indent = 0) {
treeStructure += `${'--'.repeat(indent)}${node.getNodeName()}\n`;
indent++;
for (let i = 0, length = node.noOfChildren(); i < length; i++) {
traverse(node.getChildByIndex(i), indent);
}
}
traverse(root);
return treeStructure;
}
}
class Leaf extends Component {
constructor(name) {
super(name);
this._type = 'Leaf Node';
}
getType() {
return this._type;
}
noOfChildren() {
return 0;
}
}
class Composite extends Component {
constructor(name) {
super(name);
this._type = 'Composite Node';
this._children = [];
}
getType() {
return this._type;
}
addChild(component) {
this._children = [...this._children, component];
}
removeChildByName(componentName) {
this._children = [...this._children].filter(component => component.getNodeName() !== componentName);
}
removeChildByIndex(index) {
this._children = [...this._children.slice(0, index), ...this._children.slice(index + 1)];
}
getChildByName(componentName) {
return this._children.find(component => component.name === componentName);
}
getChildByIndex(index) {
return this._children[index];
}
noOfChildren() {
return this._children.length;
}
}
// usage
const tree = new Composite('root');
tree.addChild(new Leaf('left'));
const right = new Composite('right');
tree.addChild(right);
right.addChild(new Leaf('right-left'));
const rightMid = new Composite('right-middle');
right.addChild(rightMid);
right.addChild(new Leaf('right-right'));
rightMid.addChild(new Leaf('left-end'));
rightMid.addChild(new Leaf('right-end'));
// log
console.log(Component.logTreeStructure(tree));
/*
root
--left
--right
----right-left
----right-middle
------left-end
------right-end
----right-right
*/

裝飾者模式(Decorator Pattern)

這也是結構型設計模式,功能著重在對已存在的類別,動態增加行為或者功能。這是使用子類別的另外一種替代方式。

這個裝飾者的特性用Javascript很容易去實作,因為Javascript允許動態對物件增加方法與屬性。這最簡單達成的方式就是只對物件增加屬性,但這樣將不是很有效地能重複使用。

實際上,在Javascript語言中有一個草案可以加入裝飾者模式。請看Addy Osmani’s有關於Javascript中的decorators的文章

如果你想看這一份草案,請看這裡

這個範例,我們創建了一個名叫Book的類別。我們另外創建兩個有裝飾者功能的類別,它接受book物件並回傳"裝飾過"後的book物件,叫做giftWrap。這個類別增加一個新的屬性以及一個新的功能。另一個叫做hardbindBook類別,這個類別增加一個屬性且編輯一個已存在屬性的值。

class Book {
constructor(title, author, price) {
this._title = title;
this._author = author;
this.price = price;
}
getDetails() {
return `${this._title} by ${this._author}`;
}
}
// decorator 1
function giftWrap(book) {
book.isGiftWrapped = true;
book.unwrap = function() {
return `Unwrapped ${book.getDetails()}`;
};
return book;
}
// decorator 2
function hardbindBook(book) {
book.isHardbound = true;
book.price += 5;
return book;
}
// usage
const alchemist = giftWrap(new Book('The Alchemist', 'Paulo Coelho', 10));
console.log(alchemist.isGiftWrapped); // true
console.log(alchemist.unwrap()); // 'Unwrapped The Alchemist by Paulo Coelho'
const inferno = hardbindBook(new Book('Inferno', 'Dan Brown', 15));console.log(inferno.isHardbound); // true
console.log(inferno.price); // 20

外觀模式(Façade Pattern)

(Ivan:與Factory Pattern不同的是,Factory是在建構的時後決定物件,Facade是在使用某一個功能的時候,從物件裡面調用另一個物件的方法,對外面使用的人並不知道裡面怎麼做的)

這個是結構型設計模式,並在Javascript的library中被廣泛的使用。這被使用來提供統一、且較簡單、公開對外的介面,來移除其組成的子系統與子類別中複雜的使用屏障。

像在JQuery這個library中,這個模式就被廣泛的使用。

在這個範例,我們使用ComplaintRegistry這個被類別,創建一個公開對外的API。它只曝露一個方法給用戶端使用。那就是registerComplaint。這內部處理了實例化所需的物件,依據傳入的參數型態產生ProductComplaint或ServiceComplaint。同時它也處理其他複雜的功能像是產生唯一的ID,存入complaint到記憶體等。但是,所有這些複雜的動作都被藏在使用的外觀模式之中。

let currentId = 0;class ComplaintRegistry {
registerComplaint(customer, type, details) {
const id = ComplaintRegistry._uniqueIdGenerator();
let registry;
if (type === 'service') {
registry = new ServiceComplaints();
} else {
registry = new ProductComplaints();
}
return registry.addComplaint({ id, customer, details });
}
static _uniqueIdGenerator() {
return ++currentId;
}
}
class Complaints {
constructor() {
this.complaints = [];
}
addComplaint(complaint) {
this.complaints.push(complaint);
return this.replyMessage(complaint);
}
getComplaint(id) {
return this.complaints.find(complaint => complaint.id === id);
}
replyMessage(complaint) {}
}
class ProductComplaints extends Complaints {
constructor() {
super();
if (ProductComplaints.exists) {
return ProductComplaints.instance;
}
ProductComplaints.instance = this;
ProductComplaints.exists = true;
return this;
}
replyMessage({ id, customer, details }) {
return `Complaint No. ${id} reported by ${customer} regarding ${details} have been filed with the Products Complaint Department. Replacement/Repairment of the product as per terms and conditions will be carried out soon.`;
}
}
class ServiceComplaints extends Complaints {
constructor() {
super();
if (ServiceComplaints.exists) {
return ServiceComplaints.instance;
}
ServiceComplaints.instance = this;
ServiceComplaints.exists = true;
return this;
}
replyMessage({ id, customer, details }) {
return `Complaint No. ${id} reported by ${customer} regarding ${details} have been filed with the Service Complaint Department. The issue will be resolved or the purchase will be refunded as per terms and conditions.`;
}
}
// usage
const registry = new ComplaintRegistry();
const reportService = registry.registerComplaint('Martha', 'service', 'availability');
// 'Complaint No. 1 reported by Martha regarding availability have been filed with the Service Complaint Department. The issue will be resolved or the purchase will be refunded as per terms and conditions.'
const reportProduct = registry.registerComplaint('Jane', 'product', 'faded color');
// 'Complaint No. 2 reported by Jane regarding faded color have been filed with the Products Complaint Department. Replacement/Repairment of the product as per terms and conditions will be carried out soon.'

享元模式(Flyweight Pattern)

(Ivan: cache機制的一種,在呼叫方法時決定是否產生新的物件,已有的物件就從cache取得,不另外產生新的)

這是結構型設計模式,著重在藉由微粒的物件做到有效的資料分享。它被用來保留效能與記憶體為目的。

這個設計模式可以用來在任何暫存的目的上。事實上,現今的瀏覽器使用一個多變的享元模式,來避免讀取相同的圖片兩次。

在這個範例,我們創建一個微粒享元類別Icecream來分享有關冰淇淋口味的資料,另外一個工廠類別IcecreamFactory用來創建這些享元物件。為了保留更多的記憶體,這些物件會在有相同的物件被實例化兩次的時候自動回收。這是一個簡單實作享元模式的範例。

// flyweight class
class Icecream {
constructor(flavour, price) {
this.flavour = flavour;
this.price = price;
}
}
// factory for flyweight objects
class IcecreamFactory {
constructor() {
this._icecreams = [];
}
createIcecream(flavour, price) {
let icecream = this.getIcecream(flavour);
if (icecream) {
return icecream;
} else {
const newIcecream = new Icecream(flavour, price);
this._icecreams.push(newIcecream);
return newIcecream;
}
}
getIcecream(flavour) {
return this._icecreams.find(icecream => icecream.flavour === flavour);
}
}
// usage
const factory = new IcecreamFactory();
const chocoVanilla = factory.createIcecream('chocolate and vanilla', 15);
const vanillaChoco = factory.createIcecream('chocolate and vanilla', 15);
// reference to the same object
console.log(chocoVanilla === vanillaChoco); // true

代理模式(Proxy Pattern)

(Ivan: cache機制的一種,與享元模式不同的是,proxy模式連要執行的事項都一起代為處裡,可以透過cache決定是否執行該行為)

這是一種結構型設計模式,它的行為就像它的名字所說的一樣,扮演著另一個物件的一個替代角色,或者佔位者的角色,協助其控制存取。

這通常被使用在目標物件在某些限制之下,且可能無法有效率地處理它所有權責的情況下。一個代理者,在這種情況下,通常提供相同的介面給用戶端,且增加層級去支持對目標物件間接的控制存取,來避免對該物件不適當的存取壓力。

這個代理模式在網路請求負載高的應用程式下工作,是非常有幫助的。可以避免不需要或重複的網路請求。

在這個範例中,我們將使用兩個新的ES6特性,ProxyReflect。這一個代理物件是被用來針對一個Javascript物件的基礎操作(remember, function和arrays都是Javascript的物件),定義客製化的行為。它透過一個建構子方法來創建Proxy物件,它接受一個被代理的target物件,以及一個handler物件定義所需要的客製化項目。這個handler物件允許定義一些套路功能,比如像是get, set, has, apply等,是用來作為附加客戶的行為使用。Reflect,換句話說,是一個內建的物件,用來提供相似的方法,那些方法是藉由Proxy的handler物件當作一個靜態方法 所提供的。那不是一個建構子,它的靜態方法是用來作為可攔截Javascript的操作。

現在,我們創建一個可藉由網路請求的功能。我們叫它做networkFetch。它接受一個URL和相應的回應。我們想要實作一個Proxy,當這個Proxy沒有cache的時候,我們只可以從網路取得回應。否則,我們會直接從cache回傳這個回應。

這個cache全域變數將儲存我們要緩存的回應。我們創建一個proxy名稱叫做proxiedNetworkFetch,並以我們原先的networkFetch當作target,接著在我們的handler方法使用apply讓proxy去調用target的功能。這個apply方法傳遞一些值給target物件。這些值紀錄在thisArg,且這些參數利用像陣列的結構args的方式傳遞給target。

我們確認是否傳遞的url參數值在cache裡。如果是,我們從cache直接取得回應,不調用原本的target function;如果不是,我們透過Reflect.apply方法去調用target function,並藉由thisArg傳遞參數給target(雖然在我們這個案例裡這些參數並沒有意義)。

// Target
function networkFetch(url) {
return `${url} - Response from network`;
}
// Proxy
// ES6 Proxy API = new Proxy(target, handler);
const cache = [];
const proxiedNetworkFetch = new Proxy(networkFetch, {
apply(target, thisArg, args) {
const urlParam = args[0];
if (cache.includes(urlParam)) {
return `${urlParam} - Response from cache`;
} else {
cache.push(urlParam);
return Reflect.apply(target, thisArg, args);
}
},
});
// usage
console.log(proxiedNetworkFetch('dogPic.jpg')); // 'dogPic.jpg - Response from network'
console.log(proxiedNetworkFetch('dogPic.jpg')); // 'dogPic.jpg - Response from cache'

責任鏈模式(Chain of Responsibility Pattern)

(Ivan: 像reactive programming鏈式響應式動作)

這是一個行為型設計模式,其提供一連串的鬆耦合的物件。每一個物件可以選擇採取一些行動或者處理客戶端的請求。

一個好的責任鏈模式的例子就是,在DOM物件中的事件泡沫驅動。在DOM中事件藉由一系列的巢狀DOM元件來傳遞,其中一個元件可能會有"事件監聽者"去監聽這個事件,並且做出相對應的動作。

在這個範例,我們創建一個類別叫做CumulativeSum,它可以在實例化時選擇給予一個初始值initialValue。它有一個方法add,可以將傳遞的參數值與物件的sum屬性加總起來,並回傳物件本身,來允許一連串add方法的呼叫。

這個很普遍的模式在jQuery中很常見,幾乎任何一個jQuery的方法在呼叫後都會回傳一個jQuery的物件,所以可以一連串的呼叫不同方法。

class CumulativeSum {
constructor(intialValue = 0) {
this.sum = intialValue;
}
add(value) {
this.sum += value;
return this;
}
}
// usage
const sum1 = new CumulativeSum();
console.log(sum1.add(10).add(2).add(50).sum); // 62
const sum2 = new CumulativeSum(10);
console.log(sum2.add(10).add(20).add(5).sum); // 45

命令模式(Command Pattern)

(Ivan: 像redux邏輯與UI分離)

這是一個行為型模式,主要作為封裝行為或操作為一個物件。這個模式藉由分離物件,拆分(1)請求一個操作或調用功能,與(2)執行或處理實際上的實作,來允許系統與類別鬆耦合。

剪貼簿裡互動的API有部分就像是使用命令模式。如果你是Redux的使用者,你其實已經開始接觸到命令模式。actions允許很酷的時間回溯還能debug的功能已經不算什麼,還有封裝這些可以被追蹤來重做(redo)或者回復(undo)動作的功能。因此,時間旅行變成可能。

在這個範例,我們有一個類別叫做SpecialMath,裡面有多個方法。然後有一個Command類別可以封裝命令,並依照傳入的參數來執行該物件SpecialMath相對應的動作。這個Command類別也保留了所有命令的執行過程,因此可以去擴展它重做(redo)和回復(undo)這類型操作的功能。

class SpecialMath {
constructor(num) {
this._num = num;
}
square() {
return this._num ** 2;
}
cube() {
return this._num ** 3;
}
squareRoot() {
return Math.sqrt(this._num);
}
}
class Command {
constructor(subject) {
this._subject = subject;
this.commandsExecuted = [];
}
execute(command) {
this.commandsExecuted.push(command);
return this._subject[command]();
}
}
// usage
const x = new Command(new SpecialMath(5));
x.execute('square');
x.execute('cube');
console.log(x.commandsExecuted); // ['square', 'cube']

疊代器模式(Iterator Pattern)

(Ivan: 就是next和yield)

這是一個行為型設計模式,它提供一個存取連續聚合物件元件的方式,不需要暴露它的物件底下的行為。

疊代器有一種很特別的行為,那就是我們按照一個集合的順序,在每一次呼叫next()的時候執行一個步驟,直到我們到達最後一個步驟。這個在ES6所導入的Iterator和Generators非常清楚地實作出疊代器模式。

以下我們有兩個例子。第一個案例,IteratorClass使用iterator的規格,而另一個iteratorUsingGenerator使用generator的功能。

Symbol.iterator(Symbol是一個新的一種primitive的資料格式)是用在指定一個物件的預設疊代器。它必須定義一個集合,通常使用for…of的迴圈結構來表示。在第一個範例,我們定義這個結構來存入這些資料集合然後定義Symbol.iterator,它回傳一個有包含作為疊代的next方法的物件。

第二個案例,我們定義一個generator的功能,可傳送一個陣列資料並使用next和yield回傳疊代的元件。一個generator的功能是一個特定類型的功能,如同一個疊代工廠,可以很確實地維護它自己內部狀態,並疊代地產出值來。它可以暫停或恢復它自己執行的運作。

// using Iterator
class IteratorClass {
constructor(data) {
this.index = 0;
this.data = data;
}
[Symbol.iterator]() {
return {
next: () => {
if (this.index < this.data.length) {
return { value: this.data[this.index++], done: false };
} else {
this.index = 0; // to reset iteration status
return { done: true };
}
},
};
}
}
// using Generator
function* iteratorUsingGenerator(collection) {
var nextIndex = 0;
while (nextIndex < collection.length) {
yield collection[nextIndex++];
}
}
// usage
const gen = iteratorUsingGenerator(['Hi', 'Hello', 'Bye']);
console.log(gen.next().value); // 'Hi'
console.log(gen.next().value); // 'Hello'
console.log(gen.next().value); // 'Bye'

中介者模式(Mediator Pattern)

(Ivan: 把邏輯交給外面的物件控制)

這是一個行為型模式,它封裝好一群物件集合之間該如何互動。它為這群物件提供一個中心認證,並支持鬆耦合,明確地保持物件之間不會彼此參考。

在這個案例裡,我們有TrafficTower當作中介者,來控制這些Airplane的物件如何與其他物件互動。所有的Airplane物件藉由TrafficTower物件註冊它們自己。且透過mediator類別的物件,去處理Airplane物件接收端是如何去協調其他Airplane物件之間的資料。

class TrafficTower {
constructor() {
this._airplanes = [];
}
register(airplane) {
this._airplanes.push(airplane);
airplane.register(this);
}
requestCoordinates(airplane) {
return this._airplanes.filter(plane => airplane !== plane).map(plane => plane.coordinates);
}
}
class Airplane {
constructor(coordinates) {
this.coordinates = coordinates;
this.trafficTower = null;
}
register(trafficTower) {
this.trafficTower = trafficTower;
}
requestCoordinates() {
if (this.trafficTower) return this.trafficTower.requestCoordinates(this);
return null;
}
}
// usage
const tower = new TrafficTower();
const airplanes = [new Airplane(10), new Airplane(20), new Airplane(30)];
airplanes.forEach(airplane => {
tower.register(airplane);
});
console.log(airplanes.map(airplane => airplane.requestCoordinates()))
// [[20, 30], [10, 30], [10, 20]]

觀察者模式(Observer Pattern)

(Ivan: reflux subscribe)

這是很重要的行為型設計模式,它定義物件之間一對多的相依關係,當一個物件(publisher)改變它的狀態,其他所有相依的物件會被通知且也自動更新狀態。這也叫做PubSub(publisher/subscribers)或者even dispatcher/listeners模式。這個publisher有時候被稱作subject,而subscribers有時候被稱作監聽者(observers)。

可能你已經知道有個很熟悉的地方類似這個模式,如果你有使用過addEventListener或者jQuery的.on來寫事件處理的程式碼的話。這同時也是影響著Reactive programming的程式撰寫方式。

在這個範例,我們創建一個簡單的Subject類別,包含從subscriber集合中增加和移除監聽者物件的方法。另外,有一個觸發(fire)的方法去傳送Subject類別物件的任何改變給註冊的監聽者。另一方面,這個監聽者的類別有自己的狀態且有方法去更新自己內部的狀態,並依據所註冊Subject類別傳送改變的值來作改變。

class Subject {
constructor() {
this._observers = [];
}
subscribe(observer) {
this._observers.push(observer);
}
unsubscribe(observer) {
this._observers = this._observers.filter(obs => observer !== obs);
}
fire(change) {
this._observers.forEach(observer => {
observer.update(change);
});
}
}
class Observer {
constructor(state) {
this.state = state;
this.initialState = state;
}
update(change) {
let state = this.state;
switch (change) {
case 'INC':
this.state = ++state;
break;
case 'DEC':
this.state = --state;
break;
default:
this.state = this.initialState;
}
}
}
// usage
const sub = new Subject();
const obs1 = new Observer(1);
const obs2 = new Observer(19);
sub.subscribe(obs1);
sub.subscribe(obs2);
sub.fire('INC');console.log(obs1.state); // 2
console.log(obs2.state); // 20

狀態模式(State Pattern)

(Ivan: 就是透過呼叫function時改變狀態,每個狀態會使用自己狀態的物件,讓同樣一個方法產生不同的結果)

這是一個行為型模式,它允許一個物件依照它內部的狀態去改變它的行為。這個模式似乎是藉由狀態模式類別來改變這個類別的返回物件。它提供特定狀態邏輯來限制返回物件集合中,每一個類型呈現特定的狀態。

我們將拿一個簡單的例子”紅綠燈”來了解這個模式。TrafficLight類別藉由內部狀態決定返回Red、Yellow、Green中的哪一個類別。

class TrafficLight {
constructor() {
this.states = [new GreenLight(), new RedLight(), new YellowLight()];
this.current = this.states[0];
}
change() {
const totalStates = this.states.length;
let currentIndex = this.states.findIndex(light => light === this.current);
if (currentIndex + 1 < totalStates) this.current = this.states[currentIndex + 1];
else this.current = this.states[0];
}
sign() {
return this.current.sign();
}
}
class Light {
constructor(light) {
this.light = light;
}
}
class RedLight extends Light {
constructor() {
super('red');
}
sign() {
return 'STOP';
}
}
class YellowLight extends Light {
constructor() {
super('yellow');
}
sign() {
return 'STEADY';
}
}
class GreenLight extends Light {
constructor() {
super('green');
}
sign() {
return 'GO';
}
}
// usage
const trafficLight = new TrafficLight();
console.log(trafficLight.sign()); // 'GO'
trafficLight.change();
console.log(trafficLight.sign()); // 'STOP'
trafficLight.change();
console.log(trafficLight.sign()); // 'STEADY'
trafficLight.change();
console.log(trafficLight.sign()); // 'GO'
trafficLight.change();
console.log(trafficLight.sign()); // 'STOP'

策略模式(Strategy Pattern)

(Ivan: 將物件UI與邏輯分開,與命令模式不同的是,策略模式的物件可以從外面傳入,命令模式只能用command裡面已經定義的)

這是一個行為型設計模式,允許封裝給特殊任務的替代演算法。它定義一群演算法,並封裝它們成為在執行期間不需要使用者介面或額外知識也是可以彼此互換的。

以下的例子,我們創建一個類別Commute來封裝所有通勤工作的可能策略。然後,我們定義三種策略稱作,Bus、PersonalCar、和Taxi。使用這個模式我們可以在執行期間透過travel這個方法的實現來達成策略轉換。

// encapsulation
class Commute {
travel(transport) {
return transport.travelTime();
}
}
class Vehicle {
travelTime() {
return this._timeTaken;
}
}
// strategy 1
class Bus extends Vehicle {
constructor() {
super();
this._timeTaken = 10;
}
}
// strategy 2
class Taxi extends Vehicle {
constructor() {
super();
this._timeTaken = 5;
}
}
// strategy 3
class PersonalCar extends Vehicle {
constructor() {
super();
this._timeTaken = 3;
}
}
// usage
const commute = new Commute();
console.log(commute.travel(new Taxi())); // 5
console.log(commute.travel(new Bus())); // 10

模板方法(Template Pattern)

(Ivan: 就是樣板)

這是一個行為型設計模式,依據在演算法所定義的架構或者操作的實現下,但延遲對於子類別的一些步驟。它讓子類別在沒有改變演算法的對外架構下,重新定義某些演算法的步驟。

在這個範例,我們有一個模板類別叫做Employee實作了部分的work方法。它需要依靠子類別去實作responsibilities這個方法來讓work方法完整。我們可以接著創建兩個子類別Developer和Tester來擴展這個模板方法,並且實現它來填補這個實作的不完整。

class Employee {
constructor(name, salary) {
this._name = name;
this._salary = salary;
}
work() {
return `${this._name} handles ${this.responsibilities() /* gap to be filled by subclass */}`;
}
getPaid() {
return `${this._name} got paid ${this._salary}`;
}
}
class Developer extends Employee {
constructor(name, salary) {
super(name, salary);
}
// details handled by subclass
responsibilities() {
return 'application development';
}
}
class Tester extends Employee {
constructor(name, salary) {
super(name, salary);
}
// details handled by subclass
responsibilities() {
return 'testing';
}
}
// usage
const dev = new Developer('Nathan', 100000);
console.log(dev.getPaid()); // 'Nathan got paid 100000'
console.log(dev.work()); // 'Nathan handles application development'
const tester = new Tester('Brian', 90000);
console.log(tester.getPaid()); // 'Brian got paid 90000'
console.log(tester.work()); // 'Brian handles testing'

結論

設計模式對於軟體工程來說是很重要的,並且對於解決通用的問題是很有幫助的。但這是一個非常龐大的主題,我們不可能在一個簡短的篇章,包含關於設計模式的所有主題。 因此,我選擇簡而言之的說明,那些在現今Javascript裡真的可以實際撰寫出來的部份。如果要探究的更深入,我建議你去了解以下這些書籍:

  1. Design Patterns: Elements Of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides (Gang of Four)
  2. Learn JavaScript Design Patterns by Addy Osmani
  3. JavaScript Patterns by Stoyan Stefanov

--

--

Ivan Chiou

Rich experience in multimedia integration, cross-functional collaboration, and dedicated to be a mentor for young developers.