iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 2
0
Modern Web

前端工程師一起來種一棵後端技能樹吧!系列 第 2

[Day 02] IOC 控制反轉 & DI 依賴注入 - (1)

第一個主題先來暖暖身子,要講的不是一個 backend 專屬的技術或概念,更像是屬於寫出優質程式碼的一種方法或者說模式,畢竟不管是前端後端,或是任何領域的工程師,寫出優質、易擴展、易維護的程式碼都非常重要啊! Let's Go!

學習寫程式一段時間後,漸漸會明白除了寫出能 work 的 code 之外,更重要的是寫出一份好 code,而程式碼的品質不外乎可以從可讀性、可維護性、可擴展性等幾個面向去看,在物件導向程式設計中,如果能遵守 SOLID 原則,寫出來的 code 的品質也較有保障,也較易於進行測試。

SOLID 原則為:

  • Single responsibility principle: a class should have one, and only one, reason to change;
  • Open-closed principle: it should be possible to extend the behavoir of a class without modifying it;
  • Liskov Substitution principle: subclasses should be substitutable for their superclasses;
  • Interface segregation principle: many small, client-specific interfaces are better than one general purpose interface;
  • Dependency inversion principle: depends on abstractions not concretions;

而接下來的兩篇文章則是會聚焦在 Dependency inversion principle (依賴反轉原則)。

何謂控制反轉與依賴注入?

在物件導向中,難免會需要建立一大堆的 class(類別),而如果類別間的依賴很複雜(例如 A class 需要 new 一個 B class 去使用 B 的功能,那麼萬一 B class 改動了很可能 A 的邏輯也需要更改),那麼假使專案擴展後,就會造成高耦合性,此時離我們撰寫優質程式碼的目標似乎更加遙遠了。

而控制反轉(Inversion Of Control)就是代表了避免類別間產生依賴的設計原則與概念,依賴注入則是實現控制反轉的手段之一。

大體而言實現控制反轉可以獲得以下幾項好處:

  • 解耦性
  • 易於測試
  • 快速開發

接下來我們用程式碼來體會一下

首先要來說個故事,老莫是一個剛畢業的社會新鮮人,他的夢想是成為一個頂尖的軟體工程師,而熱愛生活的他也培養出了很多興趣,用 class 可以如下表示:

export class Person {
    name: string = '';
}

class KyleMo extends Person {
    name = 'Kyle Mo';
    private age: number = 23;
    // 一堆興趣
    read() { console.log('read') }
    watchMovie() { console.log('watchMovie') }
    coding() { console.log('coding') }
    // 還有很多...
}

const kyle = new KyleMo()
kyle.coding();

買電腦

既然要成為一名軟體工程師,電腦可就是少不了的配備啊,老莫興奮的拿著外婆贊助的三萬元,買了一台 Asus 牌 Windows 系統的筆電,畢竟以前桌機也都是 Windows 系統,能夠快速上手沒有問題!

export class Notebook {
    status: string = 'normal';
    coding() {
        console.log('coding');
    }

    broken() {
        this.status = 'broken';
    }
}

export class AsusNotebook extends Notebook {
    name: string = 'AsusNotebook';

    constructor() {
        super();
        console.log(this.name);
    }

    coding() {
        console.log(`coding using ${this.name}`);
    }
}

export class Person{
    name: string = '';
}

class KyleMo extends Person {
    name = 'Kyle Mo';
    private age: number = 23;
    // 一堆興趣
    read() { console.log('read') }
    watchMovie() { console.log('watchMovie') }
    coding() {
        const notebook = new AsusNotebook();
        notebook.coding();
    }
    // 還有很多...
}

const kyle = new KyleMo()
kyle.coding();

跳槽到 MacOS

使用 Windows 筆電開發一段時間後,老莫發現使用較接近 Linux 系統的 MacOS 在開發上的效率對他而言會提升不少,於是他忍痛用自己的存款又買了一台 MacOS 來開發,希望可以加速達成他成為頂尖軟體工程師的夢想。

程式碼的變動大概如下

// 新增 Macbook 的 class
export class Macbook extends Notebook {
    name: string = 'Macbook Pro';

    constructor() {
        super();
        console.log(this.name);
    }

    coding() {
        console.log(`coding using ${this.name}`);
    }
}

// 將 KyleMo class 中 coding method 中的電腦改為 Mac
class KyleMo extends Person {
    name = 'Kyle Mo';
    private age: number = 23;
    // 一堆興趣
    read() { console.log('read') }
    watchMovie() { console.log('watchMovie') }
    coding() {
        // 改這裡
        const notebook = new Macbook();
        notebook.coding();
    }
    // 還有很多...
}

思考更好的方式

聰明的老莫馬上發現一個問題:每次換一個裝置,除了新增新裝置的類別外,還要去改變自身的邏輯。懶惰的老莫馬上思考要怎麼解決這個問題,畢竟每次都改變自己體內的構造真的會壞掉呀!這樣實在太依賴筆電了,完全被筆電牽著鼻子走,不如讓筆電注入到自己身上吧!

class KyleMo extends Person {
    name = 'Kyle Mo';
    private age: number = 23;
    private readonly notebook: Notebook;

    constructor(notebook: Notebook) {
        super();
        this.notebook = notebook;
    }
    // 一堆興趣
    read() { console.log('read') }
    watchMovie() { console.log('watchMovie') }
    coding() {
        this.notebook.coding();
    }
    // 還有很多...
}

const notebook = new Macbook();

const kyle = new KyleMo(notebook);
kyle.coding();

如此一來老莫總算是脫離對筆電的依賴了,不用更改自身的邏輯,反正外面注入什麼樣的筆電跟他沒有關係,用就對了!

桌電的降臨

因為老莫總是習慣將所有還會再用到的應用程式保持開啟的狀態,常常導致 CPU 的使用率過高,造成筆電效能低落,因此他決定從台中老家加他以前的寶物 : 8核心的桌上型電腦給搬到台北來使用(用途假設為跟筆電一樣主要拿來開發)。

聰明的你可能會想到以下寫法

export class Pc {
    name: string = 'PC';

    constructor() {
        console.log(this.name);
    }

    coding() {
        console.log(`coding using ${this.name}`);
    }
}

class KyleMo extends Person {
    name = 'Kyle Mo';
    private age: number = 23;
    private readonly notebook: Notebook;
    private readonly pc: Pc;

    constructor(notebook: Notebook, pc: Pc) {
        super();
        this.notebook = notebook;
        this.pc = pc;
    }
    // 一堆興趣
    read() { console.log('read') }
    watchMovie() { console.log('watchMovie') }
    coding_with_notebook() {
        this.notebook.coding();
    }
    cosing_with_pc() {
        this.pc.coding();
    }
    // 還有很多...
}

const notebook = new Macbook();
const pc = new Pc();

const kyle = new KyleMo(notebook, pc);
kyle.coding_with_notebook();
kyle.coding_with_pc();

再抽象一點

就算老莫再愛寫程式,一天能夠運用的時間也是有限的,不太可能會同時使用桌機跟筆電開發,再者兩者的功能都是一樣的(coding),也許我們可以提出更抽象的要求,沒有必要將筆電跟桌電都同時注入到 KyleMo class 中。

更改後的程式碼大致為

interface CodingDevice {
    status: string;
    coding: () => void;
    broken: () => void;
}

export class Notebook {
    status: string = 'normal';
    coding() {
        console.log('coding');
    }

    broken() {
        this.status = 'broken';
    }
}

export class Macbook extends Notebook implements CodingDevice {
    name: string = 'Macbook Pro';

    constructor() {
        super();
        console.log(this.name);
    }

    coding() {
        console.log(`coding using ${this.name}`);
    }
}

export class Pc implements CodingDevice {
    status: string = 'normal';
    name: string = 'PC';

    constructor() {
        console.log(this.name);
    }

    coding() {
        console.log(`coding using ${this.name}`);
    }

    broken() {
        this.status = 'broken';
    }
}

export class Person{
    name: string = '';
}

class KyleMo extends Person {
    name = 'Kyle Mo';
    private age: number = 23;
    private readonly codingDevice: CodingDevice;

    constructor(codingDevice: CodingDevice) {
        super();
        this.codingDevice = codingDevice;
    }
    // 一堆興趣
    read() { console.log('read') }
    watchMovie() { console.log('watchMovie') }
    coding() {
        this.codingDevice.coding();
    }
    // 還有很多...
}

let codingDevice: CodingDevice;
let kyle: KyleMo;

codingDevice = new Pc();

kyle = new KyleMo(codingDevice);
kyle.coding();

如此一來老莫就可以輕鬆換自己要 coding 的裝置,也解決依賴的問題了。

參考連結

參考-1
參考-2

下篇文章

老莫發現未來還有很多東西需要做 DI (依賴注入),不過自己實作真的有點累啊,有沒有辦法透過其他方式統一管理呢?
下一篇將介紹使用 InversifyJS ,讓實作 IOC 更加方便與擴展。

團隊成員系列文

想盡辦法當好一個Junior Backend Developer
用舒服的姿勢開發 Python Project


上一篇
[Day 01] 系列文動機與大綱
下一篇
[Day 03] IOC 控制反轉 & DI 依賴注入 - (2)
系列文
前端工程師一起來種一棵後端技能樹吧!30

1 則留言

0
pjchender
iT邦新手 5 級 ‧ 2020-09-15 21:00:43

已追蹤老莫團隊!

感謝你!雖然很多篇我都先發在 medium 你好像有些看過了XDD

我要留言

立即登入留言