DIP、IoC、DI、JS

在這個標題中,除了 JS 是亂入之外,其它的幾個詞彙都是存在一個共同點的,那就是依賴。

那麼,依賴是什麼呢?

比如,現在我正在寫這篇博客文,但是我得在電腦上編輯,電腦便是我完成這件事的依賴。而在代碼中,最直觀的體現是模塊之間的依賴。如某個模塊依賴另外一個模塊,那麼另外的那個模塊就是該模塊的依賴。其實在上篇博客文章《JaVaScript中的模塊》中,我們也手寫了一個模塊依賴管理器。

依賴這個理解起來很簡單,但這不代表可以隨意的依賴。在寫模塊的時候,講究個高內聚低耦合,以提高模塊的可拓展性和可維護性。模塊依賴了誰,怎麼去依賴,都關乎了最終模塊的好與壞。

還好在編程界有着提高代碼質量的金科玉律,我們可以用理論來指導實踐,寫出更好的代碼。

依賴反轉原則

依賴反轉原則(Dependency inversion principle,DIP),是一種特定的解耦形式,使得高層次的模塊不依賴於低層次的模塊的實現細節,依賴關係被顛倒(反轉),從而使得低層次模塊依賴於高層次模塊的需求抽象。———— 維基百科

該原則規定:

  1. 高層次的模塊不應該依賴與低層次的模塊,兩者都應該依賴於抽象接口。
  2. 抽象接口不應該依賴於具體實現。而具體實現則應該依賴於抽象接口。

現在用一個例子來解釋一波。

// Ajax.js
class Ajax {
  get() {
    return this.constructor.name;
  }
}
export default Ajax;

// main.js
import Ajax from './Ajax';
class Main {
  constructor() {
    this.render()
  }
  render() {
    let content = (new Ajax()).get();
    console.log('content from', content);
  }
}
new Main();

剛開始的時候,我們基於 XMLHttpRequest 對象,封裝了 Ajax 用於請求數據。後來 fetch 出來了,我們打算跟上時代的腳步,封裝 fetch 以取代 Ajax

// Fetch.js
class Fetch {
  fetch() {
    return this.constructor.name;
  }
}
export default Fetch;

// main.js
import Fetch from './Fetch';
class Main {
  constructor() {
    this.render();
  }
  render() {
    let content = (new Fetch()).fetch();
    console.log('content from', content);
  }
}
new Main();

從以上可以看出來,整個替代過程很麻煩,我們需要找出封裝請求模塊(AjaxFetch)的所有引用,然後替換掉。又由於 AjaxFetch 的方法命名也是不同,所以也需要對應地做更改。

這就是傳統的處理依賴關係的方式。在這裏 Main 是高層次模塊,AjaxFetch 是低層次模塊。依賴關係創建於高層次模塊,且高層次模塊直接依賴低層次模塊,這種依賴關係限制了高層次模塊的複用性。

依賴反轉原則則顛倒這種依賴關係,並以上面提到的兩個規定作爲指導思想。

// Service.js
class Service {
  request(){
    throw `${this.constructor.name} 沒有實現 request 方法!`
  }
}
class Ajax extends Service {
  request(){
      return this.constructor.name;
  }
}
export default Ajax;

// Main.js
import Service from './Service.js';
class Main {
  constructor() {
    this.render();
  }
  render() {
    let content = (new Service).request();
    console.log('content from', content);
  }
}
new Main();

在這裏我們把共同依賴的 Service 作爲抽象接口,它就是高層次模塊與低層次模塊需要共同遵守的契約。在高層次模塊中,它會默認 Service 會有 request 方法用來請求數據。在低層次模塊中,它會遵從 Service 複寫應該存在的方法。這在《在JavaScript中嘗試組合模式》中,無論分支對象還是葉對象都實現 expense() 方法的道理差不多。

即使後來需要封裝 axios 取代 fetch,我們也只需要在 Service.js 中修改即可。

再次回顧下傳統的依賴關係。

依賴關係創建於高層次模塊,且高層次模塊直接依賴低層次模塊。

經過以上的折騰,我們充其量只是解決了高層次模塊直接依賴低層次模塊的問題。那麼依賴關係創建於高層次模塊的問題呢?

控制反轉

如果說依賴反轉原則告訴我們該依賴誰,那麼控制反轉則告訴們誰應該來控制依賴。

像上面的 Main 模塊,它依賴 Service 模塊。爲了獲得 Service 實例的引用,Main 在內部靠自身 new 出了一個 Service 實例。這樣明顯地引用其它模塊,無異加大了模塊間的耦合。

控制反轉(Inversion of Control,IoC),通過控制反轉,對象在被創建的時候,有一個控制系統內所有對象的外界實體,將其所依賴的對象的引用傳遞給它。可以說,依賴被注入到對象中。———— 維基百科

這些話的意思就是將依賴對象的創建和綁定轉移到被依賴對象類的外部來實現。實現控制反轉最常見的方式是依賴注入,還有一種方式依賴查找。

依賴注入

依賴注入(Dependency Injection,DI),在軟件工程中,依賴注入是種實現控制反轉用於解決依賴性設計模式。一個依賴關係指的是可被利用的一種對象(即服務提供端)。依賴注入是將所依賴的傳遞給將使用的從屬對象(即客戶端)。該服務將會變成客戶端的狀態的一部分。傳遞服務給客戶端,而非允許客戶端來建立或尋找服務,是本設計模式的基本要求。

沒看懂?沒關係。這句話講的是,把過程放在外面,將結果帶入內部。在《JaVaScript中的模塊》中,我們已經用到過依賴注入,就是對於依賴模塊的模塊,則把依賴作爲參數使用

所以我們再次改造下,

// Service.js
class Service {
  request() {
    throw `${this.constructor.name} 沒有實現 request 方法!`
  }
}
class Ajax extends Service {
  request() {
    return this.constructor.name;
  }
}
export default Ajax;
// Main.js
class Main {
  constructor(options) {
    this.Service = options.Service;
    this.render();
  }
  render() {
    let content = this.Service.request();
    console.log('content from', content);
  }
}
export default Main;
// index.js
import Service from './Service.js';
import Main from './Main.js';
new Main({
  Service: new Service()
})

Main 模塊中, Service 的實例化是在外部完成,並在 index.js 中注入。相比上一次,改動後的代碼並沒有看出帶來多大的好處。如果我們再增加一個模塊呢?

class Router {
  constructor() {
    this.init();
  }
  init() {
    console.log('Router::init')
  }
}
export default Router;
# Main.js
+   this.Service = options.Router;

# index.js
+   import Router from './Router.js'
    new Main({
+        Router: new Service()
    })

若是內部實例化就不好處理了。可換成依賴注入後,這個問題就很好解決了。

// utils.js
export const toOptions = params =>
  Object.entries(params).reduce((accumulator, currentValue) => {
    accumulator[currentValue[0]] = new currentValue[1]()
    return accumulator;
  }, {});

// Main.js
class Main {
  constructor(options) {
    Object.assign(this, options);
    this.render();
  }
  render() {
    let content = this.Service.request();
    console.log('content from', content);
  }
}
export default Main;

// index.js
import Service from './Service.js';
import Router from './Router.js';
import Main from './Main.js';
import { toOptions } from './utils.js'
/**
 * toOptions 轉換成參數形式
 * @params {Object} 類
 * @return {Object} {Service: Service實例, Router: Router實例}
 */
const options = toOptions({Service, Router});
new Main(options);

因爲依賴注入把依賴的引用從外部引入,所以這裏使用 Object.assign(this, options) 方式,把依賴全部加到了 this 上。即使再增加模塊,也只需要在 index.js 中引入即可。

到了這裏,DIPIoCDI 的概念應該有個清晰的認識了。然後我們再結合實際,加個功能再次鞏固以下。作爲一功能個獨立的模塊,一般都有個初始化的過程。

現在我們要做的是遵守一個初始化的約定,定義一個抽象接口,

// Interface.js
export class Service {
  request() {
    throw `${this.constructor.name} 沒有實現 request 方法!`
  }
}
export class Init {
  init() {
    throw `${this.constructor.name} 沒有實現 init 方法!`
  }
}
// Service.js
import { Init, Service } from './Interface.js';
import { mix } from './utils.js'
class Ajax extends mix(Init, Service) {
  constructor() {
    super();
  }
  init() {
    console.log('Service::init')
  }
  request() {
    return this.constructor.name;
  }
}
export default Ajax;

MainServiceRouter 都依賴 Init 接口(在這裏就是一種協定),Service 模塊比較特殊,所以做了 Mixin 處理。要做到統一初始化,Main 還需要做些事。

// Main.js
import { Init } from './Interface.js'
class Main extends Init {
  constructor(options) {
    super();
    Object.assign(this, options);
    this.options = options;
    this.render();
  }
  init() {
    (Object.values(this.options)).map(item => item.init());
    console.log('Main::init');
  }
  render() {
    let content = this.Service.request();
    console.log('content from', content);
  }
}
export default Main;

至此,結束

// index.js
import Service from './Service.js';
import Router from './Router.js';
import Main from './Main.js';
import { toOptions } from './utils.js'

/**
 * toOptions
 * 轉換成參數形式
 * @params {Object} 類
 * @return {Object}
 * {
 *    Service: Service實例,
 *    Router: Router實例
 * }
 */
const options = toOptions({ Service, Router });

(new Main(options)).init();

//  content from Ajax
//  Service::init
//  Router::init
//  Main::init

(以上所有示例可見GitHub

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