Dependency Injection of AngularJS

在公開 javascript-libraries-syntax.vim 之後,我收到的第一個 Issue 就是支援 AngularJS,因為我一直都沒寫過,所以就花了些時間下去研究,然後就看到一個很有趣的名詞:Dependency Injection

AngularJS 有一些很特別的參數,像是$scope$http之類的,只要你的 function 是在 AngularJS 的 framework 內,然後接收的參數用到這些名字,例如:

function($http, $q) {
    // do something with $http and $q   
}

這個 function 執行的時候,你不用管怎麼把正確的 $http 和 $q 傳給他,AngularJS 自己會幫你把他處理好,而這個特色就被稱為 Dependency Injection,當然除了內建的那些模組外,也可以自己註冊新的名稱對應到你開發的模組。

其實一開始看到這個特性的時候覺得有點神奇,很好奇怎麼做的,因為我想的到的就只有一個方法可以辦到,就是 Function.prototype.toString,這個 method 會把整個 function 的定義從第一行開始輸出成字串,然後分析字串把參數抓出來,這樣就可以在執行前知道需要哪些模組,本來一直期待是不是有什麼神奇技巧,而不是用 toString 辦到的,結果去看了看原始碼,發現真的就是這樣做的,一下子神秘感就消失了~~

Merrick 有一篇 JavaScript Dependency Injection,有比較清楚的介紹到 AngularJS 這塊是怎麼做的。不過其實我對於 AngularJS 的這個特性是不是能稱為 Dependency Injection 一直存疑。

在看介紹 Dependency Injection 的文章時,幾乎舉的例子都是在有 Interface 的語言下,程式內的的相依性都相依在 Interface 上,而不相依在實做的 instance 上,等到實際上跑的時候才根據情況丟不同的實做(implement),而 JavaScript 並沒有這層 interface 的設計,AngularJS 自然也沒有,為此我想了很久,也一直在找網路上的資源,直到昨天終於想通了。

Dependency Injection 的重點不在於用程式語言的 Interface 特性來把相依性抽離,而是利用這樣的設計,讓你的程式不要直接相依於實際的實體(instance),由於 JavaScript 是弱型別的語言,所以其實在定義 function 時,並不需要指定輸入參數的型別,利用這個特性,其實就已經可以達成 Depedency Injection 了,只要實際執行程式時,根據狀況丟入應該丟入的 instance 即可。

AngularJS 則是在這特性之上,利用參數名稱來當成相依的 Interface,實際上程式執行時會是那個模組的實體傳入,就要看是哪一個模組註冊到對應的名稱了。

在我終於想通之後,發現到其實 RequireJS 也是很相似的架構,所以就搜尋了一下,果然也有人利用 RequireJS 來做 Dependency Injection,Eric Feminella 寫了 Basic Dependency Injection with RequireJS 這篇文章,裡面的例子很不錯,假設開發一個 Web Applicaiotion,同時需要支援 Desktop 和 Mobile,兩種環境要不同的介面,但是核心的程式運作是一樣的,和 UI 相關的東西則要有 Mobile 用和 Desktop 用,這時候就可以用 Dependency Injection 來根據不同環境,決定要丟 Mobile 版的 UI 模組還是 Desktop 版的,核心的模組則不需要考慮到不同環境的差異,只要確保不同的 UI 模組的介面是一樣的就可以了。

最後還要講的是 RequireJS 的一個語法:

define(function (require) {
    var core = require('core'),
        ui = require('ui');

    // do something with core and ui
});

我第一次看到這種寫法時就覺得:「不對吧,這樣沒辦法確保這個模組執行時,裡面需要的 core 和 ui 已經有了啊。」因為除了去解析程式碼外,這樣子寫從外面根本不知道 function 裡面 require 了什麼東西,而且現在也不可能讓程式跑到某行暫時停下等其他模組準備好,所以看了 RequireJS 原始碼的結論是,它就是去分析程式碼裡面 require 了什麼東西,然後自動加到這個模組的相依性清單裡面這樣,一樣是用 Function.prototype.toString 辦到的。