1. 程式人生 > 實用技巧 >前端需要了解的9種設計模式

前端需要了解的9種設計模式

什麼是設計模式?

設計模式是對軟體設計開發過程中反覆出現的某類問題的通用解決方案。設計模式更多的是指導思想和方法論,而不是現成的程式碼,當然每種設計模式都有每種語言中的具體實現方式。學習設計模式更多的是理解各種模式的內在思想和解決的問題,畢竟這是前人無數經驗總結成的最佳實踐,而程式碼實現則是對加深理解的輔助。

設計模式的型別

設計模式可以分為三大類:

  1. 結構型模式(Structural Patterns):通過識別系統中元件間的簡單關係來簡化系統的設計。
  2. 建立型模式(Creational Patterns):處理物件的建立,根據實際情況使用合適的方式建立物件。常規的物件建立方式可能會導致設計上的問題,或增加設計的複雜度。建立型模式通過以某種方式控制物件的建立來解決問題。
  3. 行為型模式(Behavioral Patterns):用於識別物件之間常見的互動模式並加以實現,如此,增加了這些互動的靈活性。

以上定義非常的抽象和晦澀,對於我們初學者並沒有太多幫助,要了解這些設計模式真正的作用和價值還是需要通過實踐去加以理解。這三大類設計模式又可以分成更多的小類,如下圖:

下面我們選擇一些在前端開發過程中常見的模式進行一一講解。

一. 結構型模式(Structural Patterns)

1. 外觀模式(Facade Pattern)

外觀模式是最常見的設計模式之一,它為子系統中的一組介面提供一個統一的高層介面,使子系統更容易使用。簡而言之外觀設計模式就是把多個子系統中複雜邏輯進行抽象,從而提供一個更統一、更簡潔、更易用的API。很多我們常用的框架和庫基本都遵循了外觀設計模式,比如JQuery就把複雜的原生DOM操作進行了抽象和封裝,並消除了瀏覽器之間的相容問題,從而提供了一個更高階更易用的版本。其實在平時工作中我們也會經常用到外觀模式進行開發,只是我們不自知而已。

比如,我們可以應用外觀模式封裝一個統一的DOM元素事件繫結/取消方法,用於相容不同版本的瀏覽器和更方便的呼叫:

// 繫結事件
function addEvent(element, event, handler) {
  if (element.addEventListener) {
    element.addEventListener(event, handler, false);
  } else if (element.attachEvent) {
    element.attachEvent('on' + event, handler);
  } else {
    element['on' + event] = fn;
  }
}

// 取消繫結
function removeEvent(element, event, handler) {
  if (element.removeEventListener) {
    element.removeEventListener(event, handler, false);
  } else if (element.detachEvent) {
    element.detachEvent('on' + event, handler);
  } else {
    element['on' + event] = null;
  }
}

2. 代理模式(Proxy Pattern)

首先,一切皆可代理,不管是在實現世界還是計算機世界。現實世界中買房有中介、打官司有律師、投資有經紀人,他們都是代理,由他們幫你處理由於你缺少時間或者專業技能而無法完成的事務。類比到計算機領域,代理也是一樣的作用,當訪問一個物件本身的代價太高(比如太佔記憶體、初始化時間太長等)或者需要增加額外的邏輯又不修改物件本身時便可以使用代理。ES6中也增加了Proxy的功能。

歸納一下,代理模式可以解決以下的問題:

  1. 增加對一個物件的訪問控制
  2. 當訪問一個物件的過程中需要增加額外的邏輯

要實現代理模式需要三部分:

  1. Real Subject:真實物件
  2. Proxy:代理物件
  3. Subject介面:Real Subject 和 Proxy都需要實現的介面,這樣Proxy才能被當成Real Subject的“替身”使用

比如有一個股票價格查詢介面,呼叫這個介面需要比較久的時間(用setTimeout模擬2s的呼叫時間):

StockPriceAPI:

function StockPriceAPI() {
  // Subject Interface實現
  this.getValue = function (stock, callback) {
    console.log('Calling external API ... ');
    setTimeout(() => {
      switch (stock) {
        case 'GOOGL':
          callback('$1265.23');
          break;
        case 'AAPL':
          callback('$287.05');
          break;
        case 'MSFT':
          callback('$173.70');
          break;
        default:
          callback('');
      }
    }, 2000);
  }
}

我們不希望每次都去請求遠端介面,而是增加快取機制,當有快取的時候就直接從快取中獲取,否則再去請求遠端介面。我們可以通過一個proxy來實現:

StockPriceAPIProxy:

function StockPriceAPIProxy() {
  // 快取物件
  this.cache = {};
  // 真實API物件
  this.realAPI = new StockPriceAPI();
  // Subject Interface實現
  this.getValue = function (stock, callback) {
    const cachedPrice = this.cache[stock];
    if (cachedPrice) {
      console.log('Got price from cache');
      callback(cachedPrice);
    } else {
      this.realAPI.getValue(stock, (price) => {
        this.cache[stock] = price;
        callback(price);
      });
    }
  }
}

注意,Proxy需要和真實物件一樣實現getValue()方法,getValue()就屬於Subject 介面

測試一下:

const api = new StockPriceAPIProxy();
api.getValue('GOOGL', (price) => { console.log(price) });
api.getValue('AAPL', (price) => { console.log(price) });
api.getValue('MSFT', (price) => { console.log(price) });

setTimeout(() => {
  api.getValue('GOOGL', (price) => { console.log(price) });
  api.getValue('AAPL', (price) => { console.log(price) });
  api.getValue('MSFT', (price) => { console.log(price) });
}, 3000)

輸出:

Calling external API ... 
Calling external API ... 
Calling external API ... 
$1265.23
$287.05
$173.70
Got price from cache
$1265.23
Got price from cache
$287.05
Got price from cache
$173.70

二. 建立型模式(Creational Patterns)

1. 工廠模式(Factory Pattern)

現實生活中的工廠按照既定程式製造產品,隨著生產原料和流程不同生產出來的產品也會有區別。應用到軟體工程的領域,工廠可以看成是一個製造其他物件的物件,製造出的物件也會隨著傳入工廠物件引數的不同而有所區別。

什麼場景適合應用工廠模式而不是直接new一個物件呢?當建構函式過多不方便管理,且需要建立的物件之間存在某些關聯(有同一個父類、實現同一個介面等)時,不妨使用工廠模式。工廠模式提供一種集中化、統一化的方式,避免了分散建立物件導致的程式碼重複、靈活性差的問題。

以上圖為例,我們構造一個簡單的汽車工廠來生產汽車:

// 汽車構造函式
function SuzukiCar(color) {
  this.color = color;
  this.brand = 'Suzuki';
}

// 汽車建構函式
function HondaCar(color) {
  this.color = color;
  this.brand = 'Honda';
}

// 汽車建構函式
function BMWCar(color) {
  this.color = color;
  this.brand = 'BMW';
}

// 汽車品牌列舉
const BRANDS = {
  suzuki: 1,
  honda: 2,
  bmw: 3
}

/**
 * 汽車工廠
 */
function CarFactory() {
  this.create = function (brand, color) {
    switch (brand) {
      case BRANDS.suzuki:
        return new SuzukiCar(color);
      case BRANDS.honda:
        return new HondaCar(color);
      case BRANDS.bmw:
        return new BMWCar(color);
      default:
        break;
    }
  }
}

測試一下:

const carFactory = new CarFactory();
const cars = [];

cars.push(carFactory.create(BRANDS.suzuki, 'brown'));
cars.push(carFactory.create(BRANDS.honda, 'grey'));
cars.push(carFactory.create(BRANDS.bmw, 'red'));

function say() {
  console.log(`Hi, I am a ${this.color} ${this.brand} car`);
}

for (const car of cars) {
  say.call(car);
}

輸出:

Hi, I am a brown Suzuki car
Hi, I am a grey Honda car
Hi, I am a red BMW car

使用工廠模式之後,不再需要重複引入一個個建構函式,只需要引入工廠物件就可以方便的建立各類物件。

2. 單例模式(Singleton Pattern)

顧名思義,單例模式中Class的例項個數最多為1。當需要一個物件去貫穿整個系統執行某些任務時,單例模式就派上了用場。而除此之外的場景儘量避免單例模式的使用,因為單例模式會引入全域性狀態,而一個健康的系統應該避免引入過多的全域性狀態。

實現單例模式需要解決以下幾個問題:

  1. 如何確定Class只有一個例項?
  2. 如何簡便的訪問Class的唯一例項?
  3. Class如何控制例項化的過程?
  4. 如何將Class的例項個數限制為1?

我們一般通過實現以下兩點來解決上述問題:

  1. 隱藏Class的建構函式,避免多次例項化
  2. 通過暴露一個getInstance()方法來建立/獲取唯一例項

JavaScript中單例模式可以通過以下方式實現:

// 單例構造器
const FooServiceSingleton = (function () {
  // 隱藏的Class的建構函式
  function FooService() {}

  // 未初始化的單例物件
  let fooService;

  return {
    // 建立/獲取單例物件的函式
    getInstance: function () {
      if (!fooService) {
        fooService = new FooService();
      }
      return fooService;
    }
  }
})();

實現的關鍵點有:1. 使用IIFE建立區域性作用域並即時執行;2.getInstance()為一個閉包,使用閉包儲存區域性作用域中的單例物件並返回。

我們可以驗證下單例物件是否建立成功:

const fooService1 = FooServiceSingleton.getInstance();
const fooService2 = FooServiceSingleton.getInstance();

console.log(fooService1 === fooService2); // true

三. 行為型模式(Behavioral Patterns)

1. 策略模式(Strategy Pattern)

策略模式簡單描述就是:物件有某個行為,但是在不同的場景中,該行為有不同的實現演算法。比如每個人都要“交個人所得稅”,但是“在美國交個人所得稅”和“在中國交個人所得稅”就有不同的算稅方法。最常見的使用策略模式的場景如登入鑑權,鑑權演算法取決於使用者的登入方式是手機、郵箱或者第三方的微信登入等等,而且登入方式也只有在執行時才能獲取,獲取到登入方式後再動態的配置鑑權策略。所有這些策略應該實現統一的介面,或者說有統一的行為模式。Node 生態裡著名的鑑權庫Passport.jsAPI的設計就應用了策略模式。

還是以登入鑑權的例子我們仿照passport.js的思路通過程式碼來理解策略模式:

/**
 * 登入控制器
 */
function LoginController() {
  this.strategy = undefined;
  this.setStrategy = function (strategy) {
    this.strategy = strategy;
    this.login = this.strategy.login;
  }
}

/**
 * 使用者名稱、密碼登入策略
 */
function LocalStragegy() {
  this.login = ({ username, password }) => {
    console.log(username, password);
    // authenticating with username and password... 
  }
}

/**
 * 手機號、驗證碼登入策略
 */
function PhoneStragety() {
  this.login = ({ phone, verifyCode }) => {
    console.log(phone, verifyCode);
    // authenticating with hone and verifyCode... 
  }
}

/**
 * 第三方社交登入策略
 */
function SocialStragety() {
  this.login = ({ id, secret }) => {
    console.log(id, secret);
    // authenticating with id and secret... 
  }
}

const loginController = new LoginController();

// 呼叫使用者名稱、密碼登入介面,使用LocalStrategy
app.use('/login/local', function (req, res) {
  loginController.setStrategy(new LocalStragegy());
  loginController.login(req.body);
});

// 呼叫手機、驗證碼登入介面,使用PhoneStrategy
app.use('/login/phone', function (req, res) {
  loginController.setStrategy(new PhoneStragety());
  loginController.login(req.body);
});

// 呼叫社交登入介面,使用SocialStrategy
app.use('/login/social', function (req, res) {
  loginController.setStrategy(new SocialStragety());
  loginController.login(req.body);
});

從以上示例可以得出使用策略模式有以下優勢:

  1. 方便在執行時切換演算法和策略
  2. 程式碼更簡潔,避免使用大量的條件判斷
  3. 關注分離,每個strategy類控制自己的演算法邏輯,strategy和其使用者之間也相互獨立

2. 迭代器模式(Iterator Pattern)

ES6中的迭代器Iterator相信大家都不陌生,迭代器用於遍歷容器(集合)並訪問容器中的元素,而且無論容器的資料結構是什麼(Array、Set、Map等),迭代器的介面都應該是一樣的,都需要遵循迭代器協議。

迭代器模式解決了以下問題:

  1. 提供一致的遍歷各種資料結構的方式,而不用瞭解資料的內部結構
  2. 提供遍歷容器(集合)的能力而無需改變容器的介面

一個迭代器通常需要實現以下介面:

  • hasNext():判斷迭代是否結束,返回Boolean
  • next():查詢並返回下一個元素

為JavaScript的陣列實現一個迭代器可以這麼寫:

const item = [1, 'red', false, 3.14];

function Iterator(items) {
  this.items = items;
  this.index = 0;
}

Iterator.prototype = {
  hasNext: function () {
    return this.index < this.items.length;
  },
  next: function () {
    return this.items[this.index++];
  }
}

驗證一下迭代器是否工作:

const iterator = new Iterator(item);

while(iterator.hasNext()){
  console.log(iterator.next());
}

輸出:

1,red,false,3.14

ES6提供了更簡單的迭代迴圈語法for...of,使用該語法的前提是操作物件需要實現可迭代協議(The iterable protocol),簡單說就是該物件有個Key為Symbol.iterator的方法,該方法返回一個iterator物件。

比如我們實現一個Range類用於在某個數字區間進行迭代:

function Range(start, end) {
  return {
    [Symbol.iterator]: function () {
      return {
        next() {
          if (start < end) {
            return { value: start++, done: false };
          }
          return { done: true, value: end };
        }
      }
    }
  }
}

驗證一下:

for (num of Range(1, 5)) {
  console.log(num);
}

輸出:

1,2,3,4

3. 觀察者模式(Observer Pattern)

觀察者模式又稱釋出訂閱模式(Publish/Subscribe Pattern),是我們經常接觸到的設計模式,日常生活中的應用也比比皆是,比如你訂閱了某個博主的頻道,當有內容更新時會收到推送;又比如JavaScript中的事件訂閱響應機制。觀察者模式的思想用一句話描述就是:被觀察物件(subject)維護一組觀察者(observer),當被觀察物件狀態改變時,通過呼叫觀察者的某個方法將這些變化通知到觀察者

比如給DOM元素繫結事件的addEventListener()方法:

target.addEventListener(type,listener[, options]);

Target就是被觀察物件Subject,listener就是觀察者Observer。

觀察者模式中Subject物件一般需要實現以下API:

  • subscribe(): 接收一個觀察者observer物件,使其訂閱自己
  • unsubscribe(): 接收一個觀察者observer物件,使其取消訂閱自己
  • fire(): 觸發事件,通知到所有觀察者

用JavaScript手動實現觀察者模式:

// 被觀察者
function Subject() {
  this.observers = [];
}

Subject.prototype = {
  // 訂閱
  subscribe: function (observer) {
    this.observers.push(observer);
  },
  // 取消訂閱
  unsubscribe: function (observerToRemove) {
    this.observers = this.observers.filter(observer => {
      return observer !== observerToRemove;
    })
  },
  // 事件觸發
  fire: function () {
    this.observers.forEach(observer => {
      observer.call();
    });
  }
}

驗證一下訂閱是否成功:

const subject = new Subject();

function observer1() {
  console.log('Observer 1 Firing!');
}


function observer2() {
  console.log('Observer 2 Firing!');
}

subject.subscribe(observer1);
subject.subscribe(observer2);
subject.fire();

輸出:

Observer 1 Firing! 
Observer 2 Firing!

驗證一下取消訂閱是否成功:

subject.unsubscribe(observer2);
subject.fire();

輸出:

Observer1Firing!

4. 中介者模式(Mediator Pattern)

在中介者模式中,中介者(Mediator)包裝了一系列物件相互作用的方式,使得這些物件不必直接相互作用,而是由中介者協調它們之間的互動,從而使它們可以鬆散偶合。當某些物件之間的作用發生改變時,不會立即影響其他的一些物件之間的作用,保證這些作用可以彼此獨立的變化。

中介者模式和觀察者模式有一定的相似性,都是一對多的關係,也都是集中式通訊,不同的是中介者模式是處理同級物件之間的互動,而觀察者模式是處理Observer和Subject之間的互動。中介者模式有些像婚戀中介,相親物件剛開始並不能直接交流,而是要通過中介去篩選匹配再決定誰和誰見面。中介者模式比較常見的應用比如聊天室,聊天室裡面的人之間並不能直接對話,而是通過聊天室這一媒介進行轉發。一個簡易的聊天室模型可以實現如下:

聊天室成員類:

function Member(name) {
  this.name = name;
  this.chatroom = null;
}

Member.prototype = {
  // 傳送訊息
  send: function (message, toMember) {
    this.chatroom.send(message, this, toMember);
  },
  // 接收訊息
  receive: function (message, fromMember) {
    console.log(`${fromMember.name} to ${this.name}: ${message}`);
  }
}

聊天室類:

function Chatroom() {
  this.members = {};
}

Chatroom.prototype = {
  // 增加成員
  addMember: function (member) {
    this.members[member.name] = member;
    member.chatroom = this;
  },
  // 傳送訊息
  send: function (message, fromMember, toMember) {
    toMember.receive(message, fromMember);
  }
}

測試一下:

const chatroom = new Chatroom();
const bruce = new Member('bruce');
const frank = new Member('frank');

chatroom.addMember(bruce);
chatroom.addMember(frank);

bruce.send('Hey frank', frank);

輸出:

bruce to frank:hellofrank

這只是一個最簡單的聊天室模型,真正的聊天室還可以加入更多的功能,比如敏感資訊攔截、一對多聊天、廣播等。得益於中介者模式,Member不需要處理和聊天相關的複雜邏輯,而是全部交給Chatroom,有效的實現了關注分離。

廣州vi設計公司 http://www.maiqicn.com 我的007辦公資源網 https://www.wode007.com

5. 訪問者模式(Visitor Pattern)

訪問者模式是一種將演算法與物件結構分離的設計模式,通俗點講就是:訪問者模式讓我們能夠在不改變一個物件結構的前提下能夠給該物件增加新的邏輯,新增的邏輯儲存在一個獨立的訪問者物件中。訪問者模式常用於拓展一些第三方的庫和工具。

訪問者模式的實現有以下幾個要素:

  1. Visitor Object:訪問者物件,擁有一個visit()方法
  2. Receiving Object:接收物件,擁有一個accept()方法
  3. visit(receivingObj):用於Visitor接收一個Receiving Object
  4. accept(visitor):用於Receving Object接收一個Visitor,並通過呼叫Visitor的visit()為其提供獲取Receiving Object資料的能力

簡單的程式碼實現如下:

Receiving Object:

function Employee(name, salary) {
  this.name = name;
  this.salary = salary;
}

Employee.prototype = {
  getSalary: function () {
    return this.salary;
  },
  setSalary: function (salary) {
    this.salary = salary;
  },
  accept: function (visitor) {
    visitor.visit(this);
  }
}

Visitor Object:

function Visitor() { }

Visitor.prototype = {
  visit: function (employee) {
    employee.setSalary(employee.getSalary() * 2);
  }
}

驗證一下:

const employee = new Employee('bruce', 1000);
const visitor = new Visitor();
employee.accept(visitor);

console.log(employee.getSalary());

輸出:

2000

本文僅僅初步探討了部分設計模式在前端領域的應用或者實現,旨在消除大部分同學心中對設計模式的陌生感和畏懼感。現有的設計模式就有大約50中,常見的也有20種左右,所以設計模式是一門巨集大而深奧的學問需要我們不斷的去學習和在實踐中總結。本文所涉及到的9種只佔了一小部分,未涉及到的模式裡面肯定也有對前端開發有價值的,希望以後有機會能一一補上。謝謝閱讀