理解設計模式

Eric_暱稱已被使用發表於2018-10-28
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中的實現,但是還有部分在本文沒有涉及。

雖然瞭解審設計模式很重要,但是不要過度使用它們。在使用某個設計模式之前,應該考慮是否能解決你的痛點。要了解模式是否適合你,你應該研究設計模式以及該設計模式的應用。

相關文章