理解設計模式

By Sukhjinder Arora | Oct 16, 2018

原文

當你開始了一個新項目,你不會馬上開始編寫代碼。第一步,你必須定義這個項目解決什麼問題和適用範圍,然後列出這個項目的特性或者規格。在你開始編碼或者正在處理更復雜的項目之後,你應該選擇最合適你項目的設計模式。

什麼是設計模式

在軟件工程裏,設計模式是軟件設計的一種常見問題的可重用解決方案。設計模式是經驗豐富的軟件開發人員所使用的最佳實踐,可以認爲是編程模版。

爲什麼使用設計模式

許多程序員要麼認爲使用設計模式是浪費時間的,要麼他們不知道如何正確使用設計模式。但是正確使用了設計模式會幫助你寫出可維護性高的代碼。

最重要的是,設計模式給軟件開發者共同的話題、術語。會讓學習了設計模式的初學者更快看懂你寫的代碼。

舉個例子,如果你在項目中使用了裝飾器模式,那麼新來的程序員馬上知道你這段代碼正在做什麼(譯者:前提是這名程序員知道這個設計模式),並且他們會更加專注解決業務問題,而不是試圖理解這段代碼做的是什麼。

現在我們知道什麼是設計模式,並且知道了它們爲什麼那麼重要,讓我們開始深入各個應用在js的設計模式吧。

<center> * </center>

模塊模式

模塊是一段獨立的代碼,因此我們可以在不影響其他代碼的情況下修改模塊的代碼。模塊還允許我們通過變量來創建單獨的範圍以避免命名空間的污染。當模塊與其它代碼耦合度低時(譯者:類似用依賴導入第三方庫的那種程度),我們還可以在其他項目複用模塊。

模塊是任何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中的實現,但是還有部分在本文沒有涉及。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章