1. 程式人生 > >一統江湖的大前端(10)——inversify.js控制反轉

一統江湖的大前端(10)——inversify.js控制反轉

> 《大史住在大前端》前端技術博文集可在下列地址訪問: > > [【github總基地】](http://www.github.com/dashnowords/blogs)[【部落格園】](https://www.cnblogs.com/dashnowords/p/10127926.html)[【華為雲社群】](https://bbs.huaweicloud.com/community/usersnew/id_1524611247318830)[【掘金】](https://juejin.im/user/2946346892662136) > > 位元組跳動幸福裡**大前端團隊**邀請各路高手前來玩耍,團隊和諧有愛,技術硬核,位元組範兒正,覆蓋前端各個方向技術棧,總有位置適合你,Base北京,社招實習都有HC,不要猶豫,內推簡歷請直接瞄準[email protected]~ [TOC] Angular是由Google推出的前端框架,曾經與React和Vue一起被開發者稱為“前端三駕馬車”,但從隨著技術的迭代發展,它在國內前端技術圈中的存在感變得越來越低,通常只有Java技術棧的後端工程師在考慮轉型全棧工程師時才會優先考慮使用。Angular沒落的原因並不是因為它不夠好,反而是因為它過於優秀,還有點高冷,忽略了國內前端開發者的學習意願和接受能力,就好像一個學霸,明明成績已經很好了,但還是不斷尋求挑戰來實現自我突破,儘管他從不吝嗇分享自己的所思所想,但他所接觸的領域令廣大學渣望塵莫及,而學渣們感興趣的事物在他看來又有些無聊,最終的結果通常都只能是大家各玩各的。 瞭解過前端框架發展歷史的讀者可能會知道在2014年時Angular1.x版本有多火,儘管它並不是第一個將MVC思想引入前端的框架,但的確可以算作第一個真正撼動jQuery江湖地位的黑馬,由於在升級Angular2.0版本的過程中強制使用Typescript作為開發語言,使它失去了大量使用者,Vue和React也開始趁勢崛起,很快便形成“三足鼎立”之勢。但Angular似乎並沒有回頭的意思,而是保持著半年一個大版本的迭代速度將更多的新概念帶給前端,從而推動前端領域的技術演進,也推動著前端向正規的軟體工程方向逐步靠攏。我常說Angular是一個孤傲的變革者,它喜歡引入和傳播思想層面的概念,將那些被公認為正確優雅且有助於工程實踐的事物帶給前端,它似乎總是在說“這個是好的,那我們就在Angular裡實現它吧”,從早期的模組化和雙向資料繫結的引入,到後來的元件化、Typescript、Cli、RxJS、DI、AOT等等,一個個特性的引入都引導著開發者從不同的角度去思考,擴充套件著前端領域的邊界,也對團隊的整體素養提出更高的要求。如果你看看今天Typescript在前端開發領域的江湖地位,回顧一下早期的Vue和Angular1.x之間的差異性,看看RxJS和React Hooks出現的時間差,就不難明白Angular的思想有多前衛。 “如果一件事情是軟體工程師應該懂的,那麼你就應該弄懂它”,這在筆者看來是Angular帶給前端開發者最有價值的思想,精細化分工對企業而言是非常有利的,但卻非常容易限制技術人員本身的視野和職業發展,就好像流水線上從事體力勞動的工人,就算對自己負責的環節再熟悉,也無法僅僅憑此來保障整個零件加工最終的質量。我們應該在協作中對自己的產出負責,但只有摘掉職位頭銜帶來的思維枷鎖,你才能成為一個更全面的軟體工程師,它並不是關於技能的,而是關於思維方式的,那些源於內心深處的認知和定位會決定一個人未來所能達到的高度。 無論你是否會在自己的專案中使用Angular,都希望你能夠花一點時間瞭解它的理念,它能夠擴充套件你對於程式設計的認知,領略軟體技術思想層面的美。本章中我們就一起來學習Angular框架中最具特色的技術——DI(依賴注入),瞭解相關的IOC設計模式、AOP程式設計思想以及實現層面的裝飾器語法,最後再看看如何使用Inversify.js來在自己的程式碼中實現“依賴注入”。如果你對此感興趣,可以通過Java的Spring框架進行更深入的研究。 ## 依賴為什麼需要注入 依賴注入(Dependency Injection,簡稱DI)並不算一個複雜的概念,但想要真正理解它背後的原理卻不是一件容易的事情,它的上游有更加抽象的IOC設計思想,下游有更加具體的AOP程式設計思想和裝飾器語法,只有搞清楚整個知識脈絡中各個術語之間的聯絡,才能夠建立相對完整的認知,從而在適合的場景使用它,核心概念的關係如下圖所示: ![](https://img2020.cnblogs.com/blog/1354575/202102/1354575-20210206111759962-16656485.png) 面向物件的程式設計是基於“類”和“例項”來運作的,當你希望使用一個類的功能時,通常需要先對它進行例項化,然後才能呼叫相關的例項方法。由於遵循“單一職責”的設計原則,開發者在實現複雜的功能時並不會將程式碼都寫在一起,而是依賴於多個子模組協作來實現相應的功能,如何將這些模組組合在一起對於面向物件程式設計而言是非常關鍵的,這也是設計模式相關的知識需要解決的主要問題,程式碼結構設計不僅影響團隊協作的開發效率,也關係著程式碼在未來的維護和擴充套件成本。畢竟在真實的開發中,不同的模組可能由不同的團隊來開發維護,如果模組之間耦合度太高,那麼偏底層的模組一旦發生變更,整個程式在程式碼層面的修改可能會非常多,這對於軟體可靠性的影響是非常嚴重的。 在普通的程式設計模式中,開發者需要引入自己所依賴的類或者相關類的工廠方法(工廠方法是指執行後會得到例項的方法)並手動完成子模組的例項化和繫結,如下所示: ```js import B from ‘../def/B’; import createC from ‘../def/C’; class A{ constructor(paramB, paramC){ this.b = new B(paramB); this.c = createC(paramC); } actionA(){ this.b.actionB(); } } ``` 從功能實現的角度而言,這樣做並沒有什麼問題,但從程式碼結構設計的角度來看卻存在著一些潛在的風險。首先,在生成A的例項時所接受的構造引數實際上並不是由A自身來消費的,而是將其透傳分發給它所依賴的B類和C類,換句話說,A除了需要承擔其本身的職責之外,還額外承擔了B和C的例項化任務,這與面向物件程式設計中的SOLID基本設計原則中的“單一職責”原則是相悖的;其次,A類的例項a僅僅依賴於B類例項的actionB方法,如果對actionA方法進行單元測試,理論上只要actionB方法執行正確,那麼單元測試就應該能夠通過,但在前文的示例程式碼中,這樣的單元測試實際上已經變成了包含B例項化過程、C例項化過程以及actionB方法呼叫的小範圍整合測試,任何一個環節發生異常都會導致單元測試無法通過;最後,對於C模組而言,它對外暴露的工廠方法createC可以對例項化的過程進行控制,例如維護一個全域性單例物件,但對於直接匯出類定義的B模組而言,每個依賴它的模組都需要自己完成對它的例項化,如果未來B類的構造方法發生了變化,恐怕開發者只能利用IDE全域性搜尋所有對B類進行例項化的程式碼然後手動進行修改。 “依賴注入”的模式就是為了解決以上的問題而出現的,在這種程式設計模式中,我們不再接收構造引數然後手動完成子模組的例項化,而是直接在建構函式中接受一個已經完成例項化的物件,在程式碼層面的基本實現形式變成了下面的樣子: ```js class A{ constructor(bInstance, cInstance){ this.b = bInstance; this.c = cInstance; } actionA(){ this.b.actionB(); } } ``` 對於A類而言,它所依賴的b例項和c例項都是在構造時從外部注入進來的,這意味著它不再需要關心子模組例項化的過程,而只需要以形參的方式宣告對這個例項的依賴,然後專注於實現自己所負責的功能即可,對子模組例項化的工作交給A類外部的其他模組來完成,這個外部模組通常被稱為“IOC容器”,它本質上就是“類登錄檔+工廠方法”,開發者通過“key-value”的形式將各個類註冊到IOC容器中,然後由IOC容器來控制類的例項化過程,當建構函式需要使用其他類的例項時,IOC容器會自動完成對依賴的分析,生成需要的例項並將它們注入到建構函式中,當然需要以單例模式來使用的例項都會儲存在快取中。 另一方面,在“依賴注入”的模式下,上層的A類對下層模組B和C的強制依賴已經消失了,它和你在JavaScript基礎中瞭解到的“鴨式辨形”機制非常類似,只要實際傳入的bInstance引數也實現一個actionB方法,且在函式簽名(或者說型別宣告)上和B類的actionB方法保持一致,對於A模組而言它們就是一樣的,這可以極大地降低對A模組進行單元測試的難度,而且方便開發者在開發環境、測試環境和生產環境等不同的場景中對特定的模組提供完全不同的實現,而不是被B模組的具體實現所限制,如果你瞭解過面向物件程式設計的SOLID設計原則就會明白,“依賴注入”實際上就是對“依賴倒置原則”的一種體現。 > 依賴倒置原則(Dependency Inversion): > > 1. 上層模組不應該依賴底層模組,它們應該依賴於共同的抽象。 > > 2. 抽象不應該依賴於細節,細節應該依賴於抽象。 這就是“依賴注入”和“控制反轉”的基本知識,依賴的例項由原本手動生成的方式轉變為由IOC容器自動分析並以注入的方式提供,原本由上層模組控制的例項化過程被轉移給IOC容器來完成,本質上它們都是對面向物件基本設計原則的實現手段,目的就是為了降低模組之間的耦合性,隱藏更多細節。很多時候,設計模式的應用的確會讓本來直觀清晰的程式碼變得晦澀難懂,但換來的卻是整個軟體對於需求不確定性的抵禦能力。初級開發者在程式設計時千萬不要只滿足於實現眼前的需求,而是應該多思考如何降低需求變動可能給自己造成的影響,甚至直接“控制反轉”將細節定製的環節以配置檔案的形式提供給產品人員。請時刻記得,軟體工程師的任務是設計軟體,讓軟體和可複用的模組去幫助自己實現需求,而不是把自己變成一個擅長搬磚的工具。 ## IOC容器的實現 基於前文的分析,你可以先自己來思考一下基本的IOC容器應該實現的功能,然後再繼續下面的內容。IOC容器的主要職責是接管所有例項化的過程,那麼它肯定能夠訪問到所有的類定義,並且知道每個類的依賴,但類的定義可能編寫在多個不同的檔案中,IOC容器要如何來完成依賴收集呢?比較容易想到的方法就是為IOC容器實現一個註冊方法,開發者在每個類定義完成後呼叫註冊方法將自己的建構函式和依賴模組的名稱註冊到IOC容器中,IOC容器以閉包的形式維護一個私有的類登錄檔,其中以鍵值對的形式記錄了每個類的相關資訊,例如工廠方法、依賴列表、是否使用單例以及指向單例的指標屬性等等,你可以根據實際需要去新增更多的配置資訊,這樣一來IOC容器就擁有了訪問所有類並進行例項化的能力;除了收集資訊外,IOC容器還需要實現一個獲取所需例項的呼叫方法,當呼叫方法執行時,它可以根據傳入的鍵值去找到對應的配置物件,根據配置資訊返回正確的例項給呼叫者。這樣一來,IOC容器就可以完成例項化的基本職能。 IOC容器的使用對於模組之間耦合關係的影響是非常明顯的,在原來的手動例項化模型中,模組之間的關係時相互耦合的,模組的變動很容易直接導致依賴它的模組發生修改,因為上層模組對底層模組本身產生了依賴;在引入IOC容器後,每個類只需要呼叫容器的註冊方法將自己的資訊登記進去,其他模組如果對它有依賴,通過呼叫IOC容器提供的方法就可以獲取到所需要的例項,這樣一來,子模組例項化的過程和主模組之間就不再是強依賴關係,子模組發生變化時也不需要再去修改主模組,這樣的處理模式對於保障大型應用的穩定性非常有幫助。現在我們再回過頭看看那張經典的控制反轉示意圖,就比較容易理解其背後完成的工作了: ![](https://img2020.cnblogs.com/blog/1354575/202102/1354575-20210206111808980-1674044586.png) IOC的機制其實和招聘是非常類似的,公司的專案要落地實施,需要專案經理、產品、設計、研發、測試等多個不同崗位的員工協作來完成,對公司而言,更加關注每個崗位需要多少人,低中高不同級別的人員比例大概是多少,從而可以從巨集觀的角度來評估人力配置是否足以保障專案落地,至於具體招聘到的人是誰,公司並不需要在意;而HR的角色就像是IOC容器,只需要按照公司的標準去市場上搜尋符合條件的候選人,並用面試來檢驗他是否符合用人要求就可以了。 ### 手動實現IOC容器 下面我們使用Typescript來手動實現一個簡單的IOC容器類,你可以先體會一下它的基本用法,因為強型別的特點,它更容易幫助你在抽象層面瞭解自己所寫的程式碼,另外它的面向物件特性也更加完備,語法特徵和Java非常相似,是學習收益率很高的選擇。相比於JavaScript的靈活,Typescript的程式碼增加了非常多的限制,最開始你可能會被型別系統搞的暈頭轉向,但當你熟悉後,就會慢慢開始享受這種程式碼層面的約束和自律帶來的工程層面的清晰。我們先來編寫基本的結構和必要的型別限制: ```js // IOC成員屬性 interface iIOCMember { factory: Function; singleton: boolean; instance?: {} } // 定義IOC容器 Class IOC { private contai