[轉(zhuǎn)載]前端IoC理念

什么是 IoC?

IoC 的全稱叫做 Inversion of Control,可翻譯為為「控制反轉(zhuǎn)」或「依賴倒置」,它主要包含了三個準(zhǔn)則:

1.高層次的模塊不應(yīng)該依賴于低層次的模塊,它們都應(yīng)該依賴于抽象
2.抽象不應(yīng)該依賴于具體實(shí)現(xiàn),具體實(shí)現(xiàn)應(yīng)該依賴于抽象
3.面向接口編程 而不要面向?qū)崿F(xiàn)編程

概念總是抽象的,所以下面將以一個例子來解釋上述的概念:
假設(shè)需要構(gòu)建一款應(yīng)用叫 App,它包含一個路由模塊 Router 和一個頁面監(jiān)控模塊 Track,一開始可能會這么實(shí)現(xiàn):

// app.js
import Router from './modules/Router';
import Track from './modules/Track';

class App {
    constructor(options) {
        this.options = options;
        this.router = new Router();
        this.track = new Track();

        this.init();
    }

    init() {
        window.addEventListener('DOMContentLoaded', () => {
            this.router.to('home');
            this.track.tracking();
            this.options.onReady();
        });
    }
}

// index.js
import App from 'path/to/App';
new App({
    onReady() {
        // do something here...
    },
});

嗯,看起來沒什么問題,但是實(shí)際應(yīng)用中需求是非常多變的,可能需要給路由新增功能(比如實(shí)現(xiàn) history 模式)或者更新配置(啟用 history, new Router({ mode: 'history' }))。這就不得不在 App 內(nèi)部去修改這兩個模塊,這是一個 INNER BREAKING 的操作,而對于之前測試通過了的 App 來說,也必須重新測試。

很明顯,這不是一個好的應(yīng)用結(jié)構(gòu),高層次的模塊 App 依賴了兩個低層次的模塊 Router 和 Track,對低層次模塊的修改都會影響高層次的模塊 App。那么如何解決這個問題呢,解決方案就是接下來要講述的 依賴注入(Dependency Injection)。

依賴注入

所謂的依賴注入,簡單來說就是把高層模塊所依賴的模塊通過傳參的方式把依賴「注入」到模塊內(nèi)部,上面的代碼可以通過依賴注入的方式改造成如下方式:

// app.js
class App {
    constructor(options) {
        this.options = options;
        this.router = options.router;
        this.track = options.track;

        this.init();
    }

    init() {
        window.addEventListener('DOMContentLoaded', () => {
            this.router.to('home');
            this.track.tracking();
            this.options.onReady();
        });
    }
}

// index.js
import App from 'path/to/App';
import Router from './modules/Router';
import Track from './modules/Track';

new App({
    router: new Router(),
    track: new Track(),
    onReady() {
        // do something here...
    },
});

可以看到,通過依賴注入解決了上面所說的 INNER BREAKING 的問題,可以直接在 App 外部對各個模塊進(jìn)行修改而不影響內(nèi)部。

是不是就萬事大吉了?理想很豐滿,但現(xiàn)實(shí)卻是很骨感的,沒過兩天產(chǎn)品就給你提了一個新需求,給 App 添加一個分享模塊 Share。這樣的話又回到了上面所提到的 INNER BREAKING 的問題上:你不得不對 App 模塊進(jìn)行修改加上一行 this.share = options.share,這明顯不是我們所期望的。

雖然 App 通過依賴注入的方式在一定程度上解耦了與其他幾個模塊的依賴關(guān)系,但是還不夠徹底,其中的 this.router 和 this.track 等屬性其實(shí)都還是對「具體實(shí)現(xiàn)」的依賴,明顯違背了 IoC 思想的準(zhǔn)則,那如何進(jìn)一步抽象 App 模塊呢。
Talk is cheap, show you the code:

class App {
    static modules = []
    constructor(options) {
        this.options = options;
        this.init();
    }
    init() {
        window.addEventListener('DOMContentLoaded', () => {
            this.initModules();
            this.options.onReady(this);
        });
    }
    static use(module) {
        Array.isArray(module) ? module.map(item => App.use(item)) : App.modules.push(module);
    }
    initModules() {
        App.modules.map(module => module.init && typeof module.init == 'function' && module.init(this));
    }
}

經(jīng)過改造后 App 內(nèi)已經(jīng)沒有「具體實(shí)現(xiàn)」了,看不到任何業(yè)務(wù)代碼了,那么如何使用 App 來管理我們的依賴呢:

// modules/Router.js
import Router from 'path/to/Router';
export default {
    init(app) {
        app.router = new Router(app.options.router);
        app.router.to('home');
    }
};
// modules/Track.js
import Track from 'path/to/Track';
export default {
    init(app) {
        app.track = new Track(app.options.track);
        app.track.tracking();
    }
};

// index.js
import App from 'path/to/App';
import Router from './modules/Router';
import Track from './modules/Track';

App.use([Router, Track]);

new App({
    router: {
        mode: 'history',
    },
    track: {
        // ...
    },
    onReady(app) {
        // app.options ...
    },
});

可以發(fā)現(xiàn) App 模塊在使用上也非常的方便,通過 App.use() 方法來「注入」依賴,在 ./modules/some-module.js 中按照一定的「約定」去初始化相關(guān)配置,比如此時需要新增一個 Share 模塊的話,無需到 App 內(nèi)部去修改內(nèi)容:

// modules/Share.js
import Share from 'path/to/Share';
export default {
    init(app) {
        app.share = new Share();
        app.setShare = data => app.share.setShare(data);
    }
};

// index.js
App.use(Share);
new App({
    // ...
    onReady(app) {
        app.setShare({
            title: 'Hello IoC.',
            description: 'description here...',
            // some other data here...
        });
    }
});

直接在 App 外部去 use 這個 Share 模塊即可,對模塊的注入和配置極為方便。

那么在 App 內(nèi)部到底做了哪些工作呢,首先從 App.use 方法說起:

class App {
    static modules = []
    static use(module) {
        Array.isArray(module) ? module.map(item => App.use(item)) : App.modules.push(module);
    }
}

可以很清楚的發(fā)現(xiàn),App.use 做了一件非常簡單的事情,就是把依賴保存在了 App.modules 屬性中,等待后續(xù)初始化模塊的時候被調(diào)用。

接下來我們看一下模塊初始化方法 this.initModules() 具體做了什么事情:

class App {
    initModules() {
        App.modules.map(module => module.init && typeof module.init == 'function' && module.init(this));
    }
}

可以發(fā)現(xiàn)該方法同樣做了一件非常簡單的事情,就是遍歷 App.modules 中所有的模塊,判斷模塊是否包含 init 屬性且該屬性必須是一個函數(shù),如果判斷通過的話,該方法就會去執(zhí)行模塊的 init 方法并把 App 的實(shí)例 this 傳入其中,以便在模塊中引用它。

從這個方法中可以看出,要實(shí)現(xiàn)一個可以被 App.use() 的模塊,就必須滿足兩個「約定」:
模塊必須包含 init 屬性
init 必須是一個函數(shù)
這其實(shí)就是 IoC 思想中對「面向接口編程 而不要面向?qū)崿F(xiàn)編程」這一準(zhǔn)則的很好的體現(xiàn)。App 不關(guān)心模塊具體實(shí)現(xiàn)了什么,只要滿足對 接口 init 的「約定」就可以了。

此時回去看 Router 的模塊的實(shí)現(xiàn)就可以很容易理解為什么要怎么寫了:

// modules/Router.js
import Router from 'path/to/Router';
export default {
    init(app) {
        app.router = new Router(app.options.router);
        app.router.to('home');
    }
};

總結(jié)

App 模塊此時應(yīng)該稱之為「容器」比較合適了,跟業(yè)務(wù)已經(jīng)沒有任何關(guān)系了,它僅僅只是提供了一些方法來輔助管理注入的依賴和控制模塊如何執(zhí)行。

控制反轉(zhuǎn)(Inversion of Control)是一種「思想」,依賴注入(Dependency Injection)則是這一思想的一種具體「實(shí)現(xiàn)方式」,而這里的 App 則是輔助依賴管理的一個「容器」。

本文轉(zhuǎn)載地址:知乎-[黑客與畫家]-前端中的 IoC 理念 作者:大板栗

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容