五分鐘掌握 JavaScript 中的 IoC

王亮hengg發表於2020-04-04

IoC,控制反轉(Inversion of Control)。它是依賴倒置原則(Dependence Inversion Principle)的一種實現方式,也就是面向介面程式設計。IoC的實現藉助於第三方容器,可以解耦具有依賴關係的物件,降低開發維護成本。

接下來我們一起通過一個完整的示例來進一步瞭解這些概念。

一個亟待擴充套件的業務模組

首先,我們來看一個示例:

class Order{
    constructor(){}
    getInfo(){
        console.log('這是訂單資訊')
    }
}

let order = new Order('新訂單');
order.getInfo()
複製程式碼

以上程式碼為某系統的訂單管理模組,目前的功能是輸出訂單資訊。

為訂單模組新增評價功能

隨著業務的發展,需要對訂單新增評價功能:允許使用者對訂單進行評價以提高服務質量。

非常簡單的需求對不對?對原有程式碼稍作修改,增加評價模組即可:

class Rate{
    star(stars){
        console.log('您對訂單的評價為%s星',stars);
    }
}
class Order{
    constructor(){
        this.rate = new Rate();
    }
    // 省去模組其餘部分 ...
}

let order = new Order('新訂單');
order.getInfo();
order.rate.star(5);
複製程式碼

一個小小的改動而已,很輕鬆就實現了:新增一個評價模組,將其作為依賴引入訂單模組即可。很快 QA 測試也通過了,現在來杯咖啡慶祝一下吧 ☕️

為模組新增分享功能

剛剛端起杯子,發現 IM 上產品同學的頭像亮了起來:

PM:如果訂單以及評論能夠分享至朋友圈等場景那麼將會大幅提升 xxxxx

RD:好的 我調研一下

剛剛新增了評分模組,分享模組也沒什麼大不了的:

class Rate(){ /** 評價模組的實現 */}

class Share(){
    shareTo(platform){
        switch (platform) {
            case 'wxfriend':
                console.log('分享至微信好友');
                break;
            case 'wxposts':
                console.log('分享至微信朋友圈');
                break;
            case 'weibo':
                console.log('分享至微博');
                break;
            default:
                console.error('分享失敗,請檢查platform');
                break;
        }
    }
}

class Order{
    constructor(){
        this.rate = new Rate();
        this.share = new Share();
    }
    // 省去模組其餘部分 ...
}

const order = new Order();
order.share.shareTo('wxposts');
複製程式碼

這次同樣新增一個分享模組,然後在訂單模組中引入它。重新編寫執行單測後,接下來QA需要對Share模組進行測試,並且對Order模組進行迴歸測試。

好像有點不對勁兒?可以預見的是,訂單這個模組在我們產品生命週期中還處於初期,以後對他的擴充套件/升級或者維護將是一件很頻繁的事情。如果每次我們都去修改主模組和依賴模組的話,雖然能夠滿足需求,但是對開發及測試不足夠友好:需要雙份的單測(如果你有的話),冒煙,迴歸...而且生產環境的業務邏輯和依賴關係遠遠要比示例中複雜,這種不完全符合開閉原則的方式很容易產生額外的bug。

使用IoC的思想改造模組

顧名思義,IoC的主要行為是將模組的控制權倒置。上述示例中我們將Order稱為高層模組,將RateShare稱為低層模組;高層模組中依賴低層模組。而IoC則將這種依賴關係倒置:高層模組定義介面,低層模組實現介面;這樣當我們修改或新增低層模組時就不會破壞開閉原則。其實現方式通常是依賴注入:也就是將所依賴的低層模組注入到高層模組中。

在高層模組中定義靜態屬性來維護依賴關係:

class Order {
    // 用於維護依賴關係的Map
    static modules = new Map();
    constructor(){
        for (let module of Order.modules.values()) {
            // 呼叫模組init方法
            module.init(this);
        }
    }
    // 向依賴關係Map中注入模組
    static inject(module) {
        Order.modules.set(module.constructor.name, module);
    }
    /** 其餘部分略 */
}

class Rate{
    init(order) {
        order.rate = this;
    }
    star(stars){
        console.log('您對訂單的評價為%s星',stars);
    }
}

const rate = new Rate();
// 注入依賴
Order.inject(rate);
const order = new Order();
order.rate.star(4); 
複製程式碼

以上示例中通過在Order類中維護自己的依賴模組,同時模組中實現init方法供Order在建構函式初始化時呼叫。此時Order即可稱之為容器,他將依賴關係收於囊中。

再次理解IoC

完成了訂單模組的改造,我們回過頭來再看看IoC:

依賴注入就是把高層模組的所依賴的低層次以引數的方式注入其中,這種方式可以修改低層次依賴而不影響高層次依賴。

但是注入的方式要注意一下,因為我們不可能在高層次模組中預先知道所有被依賴的低層次模組,也不應該在高層次模組中依賴低層次模組的具體實現。

因此注入需要分成兩部分:高層次模組中通過載入器機制解耦對低層次模組的依賴,轉而依賴於低層次模組的抽象;低層次模組的實現依照約定的抽象實現,並通過注入器將依賴注入高層次模組。

這樣高層次模組就脫離了業務邏輯轉而成為了低層次模組的容器,而低層次模組則面向介面程式設計:滿足對高層次模組初始化的介面的約定即可。這就是控制反轉:通過注入依賴將控制權交給被依賴的低層級模組。

更簡潔高效的IoC實現

上述示例中IoC的實現仍略顯繁瑣:模組需要顯式的宣告init方法,容器需要顯示的注入依賴並且初始化。這些業務無關的內容我們可以通過封裝進入基類、子類進行繼承的方式來優化,也可以通過修飾器方法來進行簡化。

修飾器(Decorators)為我們在類的宣告及成員上通過超程式設計語法新增標註提供了一種方式。 Javascript裡的修飾器目前處在 建議徵集的第二階段,但在TypeScript裡已做為一項實驗性特性予以支援。

接下來我們就著重介紹一下通過修飾器如何實現IoC。

通過類修飾器注入

以下示例程式碼均為TypeScript

首先我們實現低層模組,這些業務模組只處理自己的業務邏輯,無需關注其它:


class Aftermarket {
    repair() {
        console.log('已收到您的售後請求');
    }
}

class Rate {
    star(stars: string) {
        console.log(`評分為${stars}星`);
    }
}

class Share {
    shareTo(platform: string) {
        switch (platform) {
            case 'wxfriend':
                console.log('分享至微信好友');
                break;
            case 'wxposts':
                console.log('分享至微信朋友圈');
                break;
            case 'weibo':
                console.log('分享至微博');
                break;
            default:
                console.error('分享失敗,請檢查platform');
                break;
        }
    }
}
複製程式碼

接下來我們實現一個類修飾器,用於例項化所依賴的低層模組,並將其注入到容器內:

function Inject(modules: any) {
    return function(target: any) {
        modules.forEach((module:any) => {
            target.prototype[module.name] = new module();
        });
    };
}
複製程式碼

最後在容器類上使用這個修飾器:

@Inject([Aftermarket,Share,Rate])
class Order {
    constructor() {}
    /** 其它實現略 */
}

const order:any = new Order();
order.Share.shareTo('facebook');
複製程式碼

使用屬性修飾器實現

Ursajs中使用屬性修飾器來實現注入依賴。

Ursajs提供了@Resource修飾器和@Inject修飾器。

其中@Resource為類修飾器,它所修飾類的例項將注入到UrsajsIoC容器中:

@Resource()
class Share{}
複製程式碼

@Inject為屬性修飾器,在類中使用它可以將@Resource所修飾類的例項注入到指定變數中:

class Order{
    @Inject('share')
    share:Share;
    /** 其它實現略 */
}
複製程式碼

在此之外,作為一個簡潔優雅的框架,Ursajs還內建了定址優化,可以更高效的獲取資源。

沒有銀彈

雖然IoC很強大,但它仍然只是一種設計思想,是對某些場景下解決方案的提煉。它無法也不可能解決全部高耦合所帶來的問題。而做為開發者,我們有必要識別哪些場景適合什麼方案。

小結

  • 複雜系統中高耦合度會導致開發維護成本變高
  • IoC藉助容器實現解耦,降低系統複雜度
  • 裝飾器實現IoC更加簡潔高效
  • 沒有銀彈

參考

?️ R.I.P.

相關文章