五分鐘掌握 JavaScript 中的 IoC

IoC,控制反轉(Inversion of Control)。它是依賴倒置原則(Dependence Inversion Principle)的一種實現方式,也就是面向接口編程。IoC的實現藉助於第三方容器,能夠解耦具備依賴關係的對象,下降開發維護成本。html

接下來咱們一塊兒經過一個完整的示例來進一步瞭解這些概念。git

一個亟待擴展的業務模塊

首先,咱們來看一個示例:github

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 上產品同窗的頭像亮了起來:post

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.

相關文章
相關標籤/搜索