前端 IOC 思想簡單實踐

你有沒有過這樣一個經歷,一個項目立項之時,什麼模塊化啊,什麼抽象啊,什麼解耦啊,什麼可複用組件啊什麼的,哪一個高端用哪一個,但是項目發展到中期,隨着模塊的增長,什麼可複用,能用就行,什麼模塊化,載入就行,長此以往,項目愈來愈大,隨之也愈來愈臃腫,愈來愈難以維護,改一處看似簡單的模塊,卻發現八杆子打不着的地方竟然也被影響了,真真是寫時一時爽,維護時更加爽!前端

那項目大了,維護成了難題,如何優化呢,怎麼解決呢!git

IOC (InversionofControl 控制反轉)

看英文縮寫,是否是有點高大上,其實這個理念在後端是很是常見的,而前端不多涉及到。不過現代前端也能夠在項目中實踐了,並且很契合。github

三個準則

  • 高層次的模塊不該該依賴於低層次的模塊,它們都應該依賴於抽象
  • 抽象不該該依賴於具體實現,具體實現應該依賴於抽象
  • 面向接口編程而不是面向實現編程

一個案例

放着這些個準則不說,先用咱們熟悉的macbook來案例來講明下吧! 咱們都知道,mac電腦裏面都是一個個模塊組合成的,換算成代碼就是這樣樣子的:編程

// screen.ts
export default class Screen {
  name = "Retina";
}

// cpu.ts
export default class Cpu {
  name = "i5";
}

// battery
// 電池模式,普通模式,低電量,高電量
type TMode = "normal" | "low" | "high";
export default class Battery {
  mode: string;
  constructor(option: { mode: TMode } = { mode: "normal" }) {
    this.mode = option.mode;
  }
}


// mac.ts

import Screen from "./screen";
import Cpu from "./cpu";
import Battery from "./battery";

export default class MacBook {
  cpu: Cpu;
  screen: Screen;
  battery: Battery;
  constructor() {
    this.cpu = new Cpu();
    this.screen = new Screen();
    this.battery = new Battery();
  }
  start() {
    console.log(
      `your mac screen is battery mode is ${this.battery.mode}, screen is ${ this.screen.name } and cpu is ${this.cpu.name}`
    );
  }
}


// index.ts

import MacBook from "./mac";

let mac = new MacBook();

mac.start();
複製代碼

首先創建一個index.ts啓動文件,mac殼子 mac.ts,它內部有三個模塊,cpuscreenbattery ,這個三個屬性分別引用的是文件外的模塊。後端

代碼這樣寫,其實沒有什麼問題的,執行 index 就能看到結果,查看到這個mac類的配置,那麼,若是說我要設置mac電池配置 mode 爲低電量,那麼我就不得不去 mac.ts 主模塊裏修改電池的配置。api

this.battery = new Battery({mode: "low"});
複製代碼

這樣改,實際上是沒有什麼問題的,可是,mac其中的一個模塊修改了,爲何殼子 mac.ts 這個文件也要跟着動呢,並且這個殼子裏有mac全部的模塊依賴,以前測試經過了,此次修改了,能不能保證必定沒有出錯呢,因此此次的模塊改動就是我上面說到的問題,那如何改動呢?框架

第一次優化

// mac.ts

import Screen from "./screen";
import Cpu from "./cpu";
import Battery from "./battery";

interface IMac {
  cpu: Cpu;
  screen: Screen;
  battery: Battery;
}

export default class MacBook {
  cpu: Cpu;
  screen: Screen;
  battery: Battery;
  constructor(option: IMac) {
    this.cpu = option.cpu;
    this.screen = option.screen;
    this.battery = option.battery;
  }
  start() {
    console.log(
      `your mac screen is battery mode is ${this.battery.mode}, screen is ${ this.screen.name } and cpu is ${this.cpu.name}`
    );
  }
}


// index.ts

import MacBook from "./mac";
import Battery from "./battery";
import Cpu from "./cpu";
import Screen from "./screen";

let mac = new MacBook({
  cpu: new Cpu(),
  screen: new Screen(),
  battery: new Battery()
});

mac.start();

複製代碼

將模塊的依賴全都放在了啓動文件 index.ts 處,不管模塊如何改動,殼子模塊 mac.ts 是否是都不用改了,模塊之間的耦合度也下降了。koa

簡單來講,mac.ts 是高層模塊,battery.ts 是底層模塊,優化以前 mac.ts 依賴了 battery.ts ,是否是違背了 IOC 的第一條準則呢,優化後的代碼是將高層次的模塊所須要的依賴經過參數傳遞到模塊內部,這個方法有一個專業術語 - 依賴注入(Dependency Injection)。模塊化

所須要傳入的參數類型 IMac 就是所定義的抽象,殼子模塊 mac.ts 就是依賴了這個抽象,而這個抽象也沒有依賴於某個具體的實現。學習

那麼問題又來了,若是我想給這個mac實例再增長一個觸摸板模塊 touchpad.ts 呢,是否是又要修改殼子模塊 mac.ts 了,難道新增一個就要修改一次,就沒有一個通用方案麼?

第二次優化

// mac.ts

type IModule<T> = T | T[];

export default class MacBook {
  private modules: any[];

  use<T>(module: IModule<T>) {
    Array.isArray(module)
      ? module.map(item => this.use(item))
      : this.modules.push(module);
    return this;
  }

  start() {
    console.log(this.modules);
  }
}


// index.ts

import MacBook from "./mac";
import Battery from "./battery";
import Cpu from "./cpu";
import Screen from "./screen";
import Touchpad from "./touchpad";

let mac = new MacBook();

mac
  .use(new Cpu())
  .use(new Screen())
  .use([new Battery({mode: "high"}), new Touchpad()])
  .start();

複製代碼

模仿 koa 載入模塊的 use 方法,能夠鏈式,這樣殼子模塊 mac.ts 就徹底與低層次模塊解藕了,不管mac新增多少個模塊它都不會發生修改。mac.ts 內部已經看不到什麼業務代碼了,全部的配置都放在了最外層,即使修改添加也及其方便。

第三次優化

那麼問題又來了,mac.ts 對模塊但是有要求的,不是任何一個牌子的模塊就能安裝到個人mac上,得按照必定的標準是執行,也就是依照必定的 約定,這也就是第三個準則,面向接口編程而不是面向實現編程,下面就用代碼來展現這個準則:

// mac.ts
type IModule<T> = T | T[];

export default class MacBook {
  private modules: any[] = [];

  use<T>(module: IModule<T>) {
    Array.isArray(module)
      ? module.map(item => this.use(item))
      : this.modules.push(module);
    return this;
  }

  start() {
    this.modules.map(
      module =>
        module.init && typeof module.init === "function" && module.init()
    );
  }
}

複製代碼

mac.ts 的啓動方法中,咱們看到了,對接的模塊內部,必定要有一個 init 屬性,且這個屬性必定是一個可執行方法,那麼所對接的模塊要如何處理呢:

// cpu.ts
export default class Cpu {
  name = "i5";
  init() {
    console.log(`${this.name} start`);
  }
}
複製代碼

相似於這樣的,要對接這個殼子,就必須在模塊內部實現一個init方法,這樣這個模塊才能在殼子內部起做用。

init 方法對於 mac.ts 來講,只是一個抽象方法,一個約定的接口,將實現交給了所來對接的各個模塊,這不就是 面向接口編程 而不要面向實現編程 最好的詮釋麼!

總結

其實在 IOC 的術語中,mac.ts 更應該稱做爲 容器(Container) ,上面稱它爲殼子比較貼近現實好理解,它跟業務實現其實沒有太大的關聯,僅僅是作一些初始化的操做,因此殼子不該該隨着它所依賴的模塊的改變也跟着改變。 因此就須要一種 IOC 的編程思想去優化它,依賴注入只是這種思想的一種實現。

最後說一句,思想纔是提升編程的最佳手段,而不是學習怎麼用框架!

原文地址

相關文章
相關標籤/搜索