By Sukhjinder Arora | Oct 16, 2018
當你開始了一個新專案,你不會馬上開始編寫程式碼。第一步,你必須定義這個專案解決什麼問題和適用範圍,然後列出這個專案的特性或者規格。在你開始編碼或者正在處理更復雜的專案之後,你應該選擇最合適你專案的設計模式。
什麼是設計模式
在軟體工程裡,設計模式是軟體設計的一種常見問題的可重用解決方案。設計模式是經驗豐富的軟體開發人員所使用的最佳實踐,可以認為是程式設計模版。
為什麼使用設計模式
許多程式設計師要麼認為使用設計模式是浪費時間的,要麼他們不知道如何正確使用設計模式。但是正確使用了設計模式會幫助你寫出可維護性高的程式碼。
最重要的是,設計模式給軟體開發者共同的話題、術語。會讓學習了設計模式的初學者更快看懂你寫的程式碼。
舉個例子,如果你在專案中使用了裝飾器模式,那麼新來的程式設計師馬上知道你這段程式碼正在做什麼(譯者:前提是這名程式設計師知道這個設計模式),並且他們會更加專注解決業務問題,而不是試圖理解這段程式碼做的是什麼。
現在我們知道什麼是設計模式,並且知道了它們為什麼那麼重要,讓我們開始深入各個應用在js的設計模式吧。
模組模式
模組是一段獨立的程式碼,因此我們可以在不影響其他程式碼的情況下修改模組的程式碼。模組還允許我們通過變數來建立單獨的範圍以避免名稱空間的汙染。當模組與其它程式碼耦合度低時(譯者:類似用依賴匯入第三方庫的那種程度),我們還可以在其他專案複用模組。
模組是任何js應用程式不可或缺的一部分,有助於保持程式碼高內聚低耦合。有許多方法可以在JavaScript中建立模組,其中一種是模組模式。
不同於其它的程式語言,js沒有修飾符,也就是說你無法將變數宣告為私有或公有。因此Module模式也用於模擬封裝的概念。
我們可以在js使用IIFE(立即呼叫函式表示式)、閉包、函式範圍來實現模組。例子如下:
const myModule = (function() {
const privateVariable = 'Hello World';
function privateMethod() {
console.log(privateVariable);
}
return {
publicMethod: function() {
privateMethod();
}
}
})();
myModule.publicMethod();
複製程式碼
由於是IIFE,程式碼立即執行,並返回物件給myModule物件。由於是閉包,即使IIFE執行完成,但是返回的物件依然能夠訪問IIFE內定義的函式和變數。
因為,在IIFE內定義的變數和函式基本上在外部不能直接呼叫的,可以看作是myModule的私有的。
程式碼執行後,myModule變數像這樣:
const myModule = {
publicMethod: function() {
privateMethod();
}};
複製程式碼
因此,我們可以呼叫publicMethod方法,而它有呼叫privateMethod方法。例如:
// Prints 'Hello World'
module.publicMethod();
複製程式碼
揭示模組模式
揭示模組模式是經Christian Heilmann基於模組模式略微改進的一種模式。模組模式的問題是我們必須建立公共的方法,然後才能通過這個公共的方法來呼叫私有變數和方法。在這個模式中,我們將返回物件的屬性對映到我們想要暴露出去的私有方法。這就是揭示模組模式。例如:
const myRevealingModule = (function() {
let privateVar = 'Peter';
const publicVar = 'Hello World';
function privateFunction() {
console.log('Name: '+ privateVar);
}
function publicSetName(name) {
privateVar = name;
}
function publicGetName() {
privateFunction();
}
/** reveal methods and variables by assigning them to object properties */
return {
setName: publicSetName,
greeting: publicVar,
getName: publicGetName
};
})();
myRevealingModule.setName('Mark');
// prints Name: Mark
myRevealingModule.getName();
複製程式碼
這種模式讓我們更容易理解我們可以公開訪問那些方法和變數,這有助於提高程式碼可讀性。
程式碼執行後,myRevealingModule像這樣:
const myRevealingModule = {
setName: publicSetName,
greeting: publicVar,
getName: publicGetName
};
複製程式碼
我們可以呼叫myRevealingModule.setName('Mark'),它是對內部publicSetName的引用,而myRevealingModule.getName()的引用是對內部publicGetName方法。例如:
myRevealingModule.setName('Mark');
// prints Name: Mark
myRevealingModule.getName();
複製程式碼
對比普通模組模式,揭示模組模式的優點如下:
-
通過修改return語句中的一行程式碼,我們可以將成員從public更改為private,反之亦然。
-
返回的物件裡不包含函式定義,所有右側表示式都在IIFE中定義了,程式碼清晰易讀。
ES6模組
在es6之前,js沒有模組,所以開發者不得不第三方庫或者模組模式來實現模組。但在es6,js有了自己模組實現。
ES6的模組用檔案來儲存。每個檔案只能有一個模組。預設情況下,模組內的所有內容都是私有的。開發者可以使用export關鍵字公開函式、變數和類。模組內的程式碼始終以嚴格模式執行。
輸出模組
有兩種方式輸出函式和變數宣告:
-
在函式和變數的前面使用export關鍵字。例如:
// utils.js export const greeting = 'Hello World'; export function sum(num1, num2) { console.log('Sum:', num1, num2); return num1 + num2; } export function subtract(num1, num2) { console.log('Subtract:', num1, num2); return num1 - num2; } // This is a private function function privateLog() { console.log('Private Function'); } 複製程式碼
-
在程式碼的末尾加上export關鍵字,匯出我們要公開的函式和變數:
// utils.js function multiply(num1, num2) { console.log('Multiply:', num1, num2); return num1 * num2; } function divide(num1, num2) { console.log('Divide:', num1, num2); return num1 / num2; } // This is a private function function privateLog() { console.log('Private Function'); } export {multiply, divide}; 複製程式碼
匯入模組
跟匯出模組差不多,也有兩種方法匯入模組:
-
匯入特定的方法
// main.js // importing multiple items import { sum, multiply } from './utils.js'; console.log(sum(3, 7)); console.log(multiply(3, 7)); 複製程式碼
-
匯入所有
// main.js // importing all of module import * as utils from './utils.js'; console.log(utils.sum(3, 7)); console.log(utils.multiply(3, 7)); 複製程式碼
匯入和輸出的別名
如果要避免命名衝突,可以在匯出和匯入期間更改匯出的名稱。例如:
-
重新命名匯出的方法
// utils.js function sum(num1, num2) { console.log('Sum:', num1, num2); return num1 + num2; } function multiply(num1, num2) { console.log('Multiply:', num1, num2); return num1 * num2; } export {sum as add, multiply}; 複製程式碼
-
重新命名匯入的方法
// main.js import { add, multiply as mult } from './utils.js'; console.log(add(3, 7)); console.log(mult(3, 7)); 複製程式碼
單例模式
單例指的是隻能例項化一次的物件。如果不存在,則單例模式會建立類的新例項。 如果存在例項,則它只返回對該物件的引用。 對建構函式的任何重複呼叫總是會獲取相同的物件。
js支援單例模式,它有關於單例模式的實現。在js我們不應該叫單例,應該叫物件字面量。例如:
const user = {
name: 'Peter',
age: 25,
job: 'Teacher',
greet: function() {
console.log('Hello!');
}
};
複製程式碼
因為在js中,每個物件佔用一個唯一的記憶體位置,當我們呼叫使用者物件時,實際上返回的是該物件的引用。
如果我們嘗試複製user物件到另一個變數,並且修改其中的值。例如:
const user1 = user;
user1.name = 'Mark';
複製程式碼
我們會看到兩個物件都被修改,因為js中的物件是引用而不是值傳遞。所以記憶體中只有一個物件。例如:
// prints 'Mark'
console.log(user.name);
// prints 'Mark'
console.log(user1.name);
// prints true
console.log(user === user1);
複製程式碼
可以用建構函式實現單例模式。例如:
let instance = null;
function User() {
if(instance) {
return instance;
}
instance = this;
this.name = 'Peter';
this.age = 25;
return instance;
}
const user1 = new User();
const user2 = new User();
// prints true
console.log(user1 === user2);
複製程式碼
當這個建構函式被呼叫,它會檢查instance是否存在。如果物件不存在,它會分配值給instance變數。如果存在,返回已存在的物件。
也可以使用模組模式實現單例。例如:
const singleton = (function() {
let instance;
function init() {
return {
name: 'Peter',
age: 24,
};
}
return {
getInstance: function() {
if(!instance) {
instance = init();
}
return instance;
}
}
})();
const instanceA = singleton.getInstance();
const instanceB = singleton.getInstance();
// prints true
console.log(instanceA === instanceB);
複製程式碼
在上面的程式碼中,我們用singleton.getInstance方法建立了新的instance例項。如果例項已存在,則此方法返回已存在例項,如果例項不存在,則通過呼叫init()函式建立新例項。
工廠模式
工廠模式使用工廠方法建立物件,而不指定所建立物件的確切類或建構函式。
工廠模式用於建立物件,不暴露例項化邏輯。當我們需要根據特定條件生成不同的物件時,可以使用此模式。例如:
class Car{
constructor(options) {
this.doors = options.doors || 4;
this.state = options.state || 'brand new';
this.color = options.color || 'white';
}
}
class Truck {
constructor(options) {
this.doors = options.doors || 4;
this.state = options.state || 'used';
this.color = options.color || 'black';
}
}
class VehicleFactory {
createVehicle(options) {
if(options.vehicleType === 'car') {
return new Car(options);
} else if(options.vehicleType === 'truck') {
return new Truck(options);
}
}
}
複製程式碼
我建立了car和truck類(帶有一些預設值),用於建立新的汽車和卡車物件。然後我定義了VehicleFactory類,用於根據物件中收到的vehicleType屬性建立並返回一個新物件。
const factory = new VehicleFactory();
const car = factory.createVehicle({
vehicleType: 'car',
doors: 4,
color: 'silver',
state: 'Brand New'
});
const truck= factory.createVehicle({
vehicleType: 'truck',
doors: 2,
color: 'white',
state: 'used'
});
// Prints Car {doors: 4, state: "Brand New", color: "silver"}
console.log(car);
// Prints Truck {doors: 2, state: "used", color: "white"}
console.log(truck);
複製程式碼
我是例項化了VehicleFactory類,之後我們呼叫factory.createVehicle方法建立car和truck物件,並且傳遞vehicleType屬性值為car或truck的options物件。
裝飾器模式
裝飾器模式用於擴充套件物件的功能,而無需修改現有的類或建構函式。此模式可用於向物件新增新功能,無需修改它們的基礎程式碼。
一個簡單的例子:
function Car(name) {
this.name = name;
// Default values
this.color = 'White';
}
// Creating a new Object to decorate
const tesla= new Car('Tesla Model 3');
// Decorating the object with new functionality
tesla.setColor = function(color) {
this.color = color;
}
tesla.setPrice = function(price) {
this.price = price;
}
tesla.setColor('black');
tesla.setPrice(49000);
// prints black
console.log(tesla.color);
複製程式碼
這種模式更實際的例子是:
比方說,汽車的成本取決於它的功能數量。 如果沒有裝飾器模式,我們必須為不同的功能組合建立不同的類,每個類都有一個成本方法來計算成本。例如:
class Car() {
}
class CarWithAC() {
}
class CarWithAutoTransmission {
}
class CarWithPowerLocks {
}
class CarWithACandPowerLocks {
}
複製程式碼
但有了裝飾器模式,我們可以基於car類建立物件,並且使用裝飾器函式新增不同的方法。例如:
class Car {
constructor() {
// Default Cost
this.cost = function() {
return 20000;
}
}
}
// Decorator function
function carWithAC(car) {
car.hasAC = true;
const prevCost = car.cost();
car.cost = function() {
return prevCost + 500;
}
}
// Decorator function
function carWithAutoTransmission(car) {
car.hasAutoTransmission = true;
const prevCost = car.cost();
car.cost = function() {
return prevCost + 2000;
}
}
// Decorator function
function carWithPowerLocks(car) {
car.hasPowerLocks = true;
const prevCost = car.cost();
car.cost = function() {
return prevCost + 500;
}
}
複製程式碼
首先,我們建立了用於建立car物件的基類Car。然後,我們為要新增到其上的要素建立裝飾器,並將Car物件作為引數傳遞。 然後我們覆蓋該物件的cost函式,該函式返回汽車的更新成本,並向該物件新增新屬性以指示新增了哪個特徵。
為了新增新功能,我們可以這樣做:
const car = new Car();
console.log(car.cost());
carWithAC(car);
carWithAutoTransmission(car);
carWithPowerLocks(car);
複製程式碼
最後,我們這樣子計算成本:
// Calculating total cost of the car
console.log(car.cost());
複製程式碼
結論
我們已經瞭解部分設計模式在js中的實現,但是還有部分在本文沒有涉及。
雖然瞭解審設計模式很重要,但是不要過度使用它們。在使用某個設計模式之前,應該考慮是否能解決你的痛點。要了解模式是否適合你,你應該研究設計模式以及該設計模式的應用。