iT邦幫忙

2024 iThome 鐵人賽

DAY 25
0
JavaScript

Vue.js學習中的細節陷阱:30天自我學習指南系列 第 25

Day 25: SOLID - 依賴反轉原則(DIP) 和 Vue 的依賴注入模式

  • 分享至 

  • xImage
  •  

今天要介紹 SOLID 設計準則的最後一個 依賴反轉原則(Dependency Inversion Principle, DIP),我覺得是一個光看定義不太好懂也是最難的原則,但在Vue的設計模式面確實是用滿多的,也跟昨天文章結尾有提到,我們在表單互動的邏輯主要靠另一個組合式函式,當作類似介面interface來驅動我們整個表單的流動有關。

雖然目前實務上比較難直接使用到,不過一旦開發程式下手能夠考量到這些原則,就能夠避免掉一些不必要錯誤或把程式寫得失去彈性~~~一起看完SOLID最後一個準則吧。

今日學習目標

  1. 認識依賴反轉原則(DIP),什麼是依賴(dependency)?
  2. 抽象類別(abstract)是什麼,如何用依賴注入模式達成依賴反轉?
  3. JavaScript工廠函式,宣告式產生抽象工廠,達成依賴注入
  4. Vue的(Provide/Inject)其實就是一種依賴注入的靈活實踐

依賴反轉原則(DIP),什麼是依賴

依賴反轉原則,簡稱DIP(Dependency inversion principle),應該讓高階模組不依賴低階模組,並讓系統依賴性降至最低。依賴關係應只涉及抽象而不涉及具體。這裡的抽象通常指的是介面及抽象類別(Abstract Class)。

看到這裡一定跟我當初看一樣覺得是天書~!,而且還跑出兩個專有名詞 依賴抽象類別,我們慢慢保持耐心來理解它們跟依賴反轉的關係吧。

依賴(dependency)是什麼

簡單以 JavaScript 函式(function)來說,如果 function A 調用 function B,function A 就會依賴 function B,當 function B 變化的時候,function A 就有機會需要更改和增加出錯的風險。

當開發者修改或創建func B時,可能忘記設定login函式,也會造成出錯,因為function A的參數如果傳入的是一個function,目前並未約束其中應該包含哪些屬性

// funcA執行時運作時,需要依賴funcB 內部login方法,換成另一個funC未必會遵守這麼實作
funcA(funcB) {
 funcB().login()
}

依賴反轉(DIP)原則有強調高層模組不應該依賴於低層模組,用下圖來解說應該印象會比較深:

如果一個模組可能功能比較龐大,需要由其他子模組匯入執行組合而成,這個在上層的高階模組就會變得依賴下面的低階模組,也就是說,如果今天低階模組有更動,高階模組的連動執行可能會有錯誤產生。


(圖片出處)


抽象類別是什麼?

高層函式不應依賴低層函式,兩者都應依賴於抽象介面 (abstraction)。

DIP 依賴反轉的意思指的是反轉依賴的方向,而要反轉依賴的方向,我們可以在函式或類別的調用者 (caller) 和被調用者 (callee) 之間添加一個穩定的抽象介面 (stable abstract interface)

雖然在原生JavaScript中是沒有抽象類別(abstact)這個保留字,不過在 TypeScriptJava中有這類的功能,抽象類別最簡單來說就不能被實例化類別(不能用new關鍵字),而繼承到抽象類別的子類別,也必須強制實作出在抽象類別裡面定義好的方法,不然會噴錯,抽象類別主要功能是描繪一個基本的方法架構雛形,並強制制約後代類別繼承時的內部實作方法都要一致,就能達到穩定介面

下面是一段TypeScript程式碼使用抽象類別abstract範例:

通常我們在使用類別class繼承extend時,你可選擇覆寫重新製作原本的方法,或不宣告重新製作,但如果子類別用繼承了抽象類別,就必須定義出來。

https://ithelp.ithome.com.tw/upload/images/20241008/201452511KjyXHl8ol.png

abstract class Engine {
  abstract start(): void;
}

class PetrolEngine extends Engine {
  // 沒有實作會出錯
}

class DieselEngine extends Engine {
  start(): void {
    console.log('Start diesel engine');
  }
}

class Car {
  // private engine: Engine = new PetrolEngine();
  private engine: Engine = new DieselEngine();

  start(): void {
    this.engine.start();
  }
}

const car: Car = new Car();
car.start(); // Start diesel engine

依賴注入模式(Dependency Injection)

如果有理解上面抽象類別的基本功能後,我們可以在模組間引用就可以設定一層穩定介面,降低彼此的依賴性。
不過光靠抽象類別,我們在模組的靈活度可能會不太夠,這時候會需要另一種依賴注入模式,來達成在不同模組間能夠靈活切換,而不會因為類別或介面寫死。

用一篇文章的案例來理解依賴注入模式的好處:

實作一個汽車物件

假設今天有一個需求我們需要製作組裝生成一批汽車,同時面對兩種不同客戶,可能只是內裝引擎的不同,一個是柴油引擎,一個是汽油引擎。 即便我們已經用抽象類別定義好引擎該有的啟動功能,要組裝成這兩種類型汽車,需要靠內部程式碼切來切去,有點不太牢靠。

abstract class Engine {
  abstract start(): void;
}

// class PetrolEngine extend Engine {
//   start() {
//     console.log('Start petrol engine');
//   }
// }

class DieselEngine extend Engine {
  start() {
    console.log('Start diesel engine');
  }
}

class Car {
  // private petrolEngine: PetrolEngine = new PetrolEngine();
  private dieselEngine: DieselEngine = new DieselEngine();

  start(): void {
    this.dieselEngine.start();
  }
}

const car: Car = new Car();
car.start(); // Start diesel engine

這有點像之前提到的程式面開放封閉原則(OCP),要怎麼讓生產汽車這個動作,變得更靈活呢? 文章中有提到,那就來製作一個專門產生不同引擎的方法,而不是直接定義在汽車這個類別的建構子(constructor)上:

  • setEngine(dieselEngine),提供一個安裝引擎方法,讓開發者可以有需要時注入改變引擎種類

同時,一部汽車生產有著不同引擎這件事,就能透過一層抽象層隔離開來,我們也會發現抽象層其實不會涉及太多實作細節的定義(像啟動方法那些),抽象層大多是負責調配流程


(圖片出處)

class Car {
  private engine: Engine | null = null;

  setEngine(engine: Engine) {
    this.engine = engine;
  }

  start(): void {
    if ((this, this.engine)) this.engine.start();
    else console.log('Engine does not exist');
  }
}

const car: Car = new Car();
car.start(); // Engine does not exist

// 先產生一個汽車骨架
const dieselEngine = new DieselEngine();

// 再透過不同類型引擎注入安裝
car.setEngine(dieselEngine);
car.start(); // Start diesel engine

const petrolEngine = new PetrolEngine();
car.setEngine(petrolEngine);
car.start(); // Start petrol engine

JavaScript 抽象工廠函式

我們可以使用之前有稍微碰到的 JavaScript 工廠函式類別class 來實作抽象工廠模式。

這種模式一樣有提供動態依賴注入的效果,而無需實際像強型別語言的才具有抽象類別。

我們使用工廠函式來動態創建不同品牌的機車實例,再次練習類別和工廠函式來模擬抽象工廠的概念,也是一種有效隔離模組間依賴性滿不錯的做法:

抽象工廠有點像是,本身不像一般工廠函式會返回一個內部定義好的物件,而是透過有點宣告式,告訴你排定好的流程,該去呼叫那些類別來實作。

  • 抽象工廠模式不僅是生成物件的方式,更像是一種宣告式的流程管理
// 定義基礎 Class 一台機車包含牌照及品牌
class Motorcycle {
  constructor(license, brand) {
    this.license = license;
    this.brand = brand;
  }
}

// 定義各品牌的具體類別
class SymMotor extends Motorcycle {
  constructor() {
    super("green", "sym");
  }
}

class KymcoMotor extends Motorcycle {
  constructor() {
    super("green", "kymco");
  }
}

// 抽象工廠函數
const motorFactory = ({ brand }) => {
  // 內部沒有定義方法或物件基本資料,透過流程控制去呼叫上面那些定義好的類別
  if (brand === 'sym') {
    return new SymMotor();
  }
  if (brand === 'kymco') {
    return new KymcoMotor();
  }
  throw new Error(`Unknown brand: ${brand}`);
};

// 使用工廠創建不同品牌的機車
const symMotor = motorFactory({ brand: "sym" });
const kymcoMotor = motorFactory({ brand: "kymco" });

console.log(symMotor);  // { license: 'green', brand: 'sym' }
console.log(kymcoMotor);  // { license: 'green', brand: 'kymco' }

Vue的(Provide/Inject)其實就是一種依賴注入的靈活實踐

之前有提到 Vue 的跨元件資料傳遞中,provide/inject 會提供了一種全域或子孫元件之間共享資料的機制,這種機制允許父組件「提供」一個依賴,而子組件可以在不必直接知道其具體來源或實作的情況下「注入」這些依賴,使用Vue開發時常不知不覺用到呢~。

  • 抽象依賴關係
    provide 在父元件中提供的數據或功能,實際上是作為一種依賴注入給子元件,子元件透過 inject 使用這些數據或方法,並且不關心具體的實現細節,這樣的設計確保了父子元件之間依賴於抽象的接口。

  • 降低耦合度
    當使用 provide 和 inject 時,子元件並不直接依賴父元件的實作,這樣在修改父元件的同時,子元件不需要更改任何邏輯,很像 DIP 依賴反轉的概念,因為子元件僅僅依賴於provider提供的數據或方法,而不會管內部具體的實現流程。


(圖片出處)


總結

  • DIP 與抽象類別
    DIP 通常依賴於抽象類別或接口,達到強制性的介面一致和穩定,透過有效介面隔離來實現依賴反轉,使高層模組不依賴於低層模組。
  • 依賴注入模式
    依賴注入可以通過工廠函數或是另外式設定類別中的注入方法等方法來注入具體實作,讓系統更具動態靈活性能夠依據不同業務條件,靈活生產不同資料或物件。
  • Vue Provide/Inject
    Vue 中的 provide/inject 機制使得父子元件間的依賴變得更抽象,很像 DIP 抽離依賴的理念,並讓元件之間的邏輯更容易維護和測試。

這些概念在不同框架或語言中都有類似的應用,幫助開發者構建低耦合的系統架構

感謝今天下班前一個小時撰寫前文章前,卡在JavaScript沒有的抽象類別(abstract class)很久,一度想打掉重練,後來後端同事看到才現發現對他們來說,是設計模式中的物件導向的基本概念,跟我稍微提點才終於知道它的一些作用,也結束SOLID的辛苦介紹,但也激起心中更多的想法和火花,接下來會慢慢拉回一些基本的JavaScript其它應用囉 ~~繼續加油。


學習資源

  1. https://www.jyt0532.com/2020/03/24/dip/
  2. https://www.thinkinmd.com/post/2020/03/04/oo-dependency-inversion-principle/
  3. https://rock070.me/notes/designpattern/solid/solid-dip-dependendy-inversion-principlehttps://huashen87.medium.com/dependency-inversion-principle-dip-依賴反轉原則-defea3a16031
  4. https://www.appcoda.com.tw/dependency-inversion-principle/
  5. https://medium.com/程式愛好者/物件導向中的介面與抽象類別是什麼-1199804ccc5f
  6. https://www.appcoda.com.tw/dependency-inversion-principle/
  7. https://dev.to/lukeskw/solid-principles-theyre-rock-solid-for-good-reason-31hn

上一篇
Day 24: SOLID - 介面分離原則(ISP) 和 Vue 的動態元件切換
下一篇
Day 26: JavaScript 的錯誤處理和 Vue元件錯誤捕捉 - onErrorCaptured
系列文
Vue.js學習中的細節陷阱:30天自我學習指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言