Inversion of Control (IoC) 的那一兩個心得

Jast Lai
Jastzeonic
Published in
12 min readSep 17, 2018

回想開始學 Android 以來,使用 Dagger 2 從入門到放棄大概五六次了吧,現在還是深深覺得要搞 IoC 真的得要實際操作會比去看來得更有效率,嘛,軟體工程的東西就是這樣,很多時候看到不懂,但是實際撞牆之後,再去看就會明白為啥要這樣搞了。

那為了要講 Dependency Injection 的 framework,要先提一下為什麼要使用這個 framework 的目的,為了明白這目的,就先從 Inversion of Control (控制反轉,簡稱 IoC)開始說起唄。

Inversion of Control 控制反轉

Inversion of Control (控制反轉,簡稱 IoC) 是什麼?聽起來很像以下犯上,什麼東西亂酷一把 ? 是可以把老闆壓在地上對他做很多色…咳呃,我頭一回聽到以為是兩個主從關係的 module 身份互換,但是顯然只是把兩個 module 原先身份對調,這樣對程式本身會有什麼幫助嗎?

等等,說起來,究竟是需要解決什麼問題,才會需要採用這項設計原則呢?

目的其實很簡單,就是要解耦合(decoupling)。

那這邊就不贅述 module 和 module 之間高耦合會造成什麼樣的問題,這邊只要知道我有一對 modules 之間的關係很好甜,甜到工程師看到就覺得自己跳下去弄會羨慕嫉妒覺得恨還肯定會蛀牙就好。

顯然的,只是兩個 module 身份對調似乎對程式解耦沒什麼幫助。

那麼,究竟是要反轉什麼呢?

Control 可以理解是 Control flow ,那要反轉 Control flow 的矛盾對立點在哪呢?拿英文 Wiki 上的 Inversion of Control 條目其中來說:

In traditional programming, the flow of the business logic is determined by objects that are statically bound to one another. With inversion of control, the flow depends on the object graph that is built up during program execution.Such a dynamic flow is made possible by object interactions that are defined through abstractions.

翻譯成中文就是說:

在過往的程式設計,商業邏輯流程所需的物件是靜態封裝的,那若是控制反轉的話,流程所需的物件則是由程序執行時所產生的,這個動態的流程中物件可能是藉由抽象型別來相互作用

Ok 好我花了三十分鐘翻譯出了一句自己也看不懂的咒文(被毆。

也就是說反轉控制流程的相對點是指靜態綁定(Statically binding)和動態綁定(Dynamic binding)。

這邊解釋一下靜態封裝(Statically bound)也就是靜態綁定(Statically binding)或稱早期綁定(Early binding)是什麼意思,可以理解成,通俗一點的說法就是在編譯前就已經知道他是什麼的東西。例如說我要決定法米是隻狗:

val fami = Dog()

那法米就會是狗,他不會是貓、龍之類的

那麼與靜態綁定相對,就是動態綁定(Dynamic binding)或者是後期綁定(late binding),同樣用通俗一點的說法就是得等到程式跑下去的來知道他是什麼東西。舉例來說,我會有一個 int 決定現在需要 fami 是狗、是貓、是猴、是龍

fun method(creatureNumer:Int) {
val fami: Creature
when (creatureNumber) {
IS_DOG -> fami = Dog()
IS_CAT -> fami = Cat()
IS_MONKEY -> fami = Monkey()
IS_GODZILLA -> fami = Godzilla()

}
}

那法米有可能是狗、是貓、是猴、也有可能是哥吉拉,這得根據程式怎麼去跑來決定了。

那前面嘴了這麼多,其實就只是要了解反轉是反轉了什麼,那話說回來, IoC 的定義,其實可以簡單地用幾句話來解釋:

A 物件裡有用 B 物件,為 A 依賴 B 的關係,在傳統的方法上,A 直接對 B 做流程控制,雙方會有一定程度的耦合;那控制反轉則將 A 對 B 的直流程控制反轉交由第三方,讓雙方都依賴彼此的抽象型別,藉此降低雙方的耦合。

那麼將直接控制流程交給第三方的其中一個方法就是 Dependency Injection (簡稱 DI)依賴注入了。

Dependency Injection 依賴注入

那既然提到 DI 了,就來解釋一下 DI 為何物吧。

一樣地,借用一下英文 Wiki 上的 Dependency Injection 條目部分敘述:

dependency injection is a technique whereby one object (or static method) supplies the dependencies of another object. A dependency is an object that can be used (a service). An injection is the passing of a dependency to a dependent object (a client) that would use it.

翻成中文就是:

依賴注入是一個物件提供另一個物件依賴關係的技術。依賴對象(dependency)指的是一個被依賴使用的物件;注入(Injection)指得是將依賴對象傳遞給需要使用到該依賴對象依賴者(dependent object)的過程。

其實沒有比前面 IoC 來得難翻譯,不過一下依賴對象一下依賴者的很繞口。每次讀軟體工程相關的英文覺得我有本事寫一本大家都唸得出來,但是完全搞不懂中文意思的魔法書(誤。

那這個方法還有一個基礎條件就是這句:

Passing the service to the client, rather than allowing a client to build or find the service, is the fundamental requirement of the pattern.

中文是指:

將依賴對象(Service)傳遞給依賴者(Client),而不是允許依賴者建立或者是自己去找依賴對象,是這個模式的基本要求。

這邊刻意不翻服務和客端是因為自己把這兩個詞彙加進去自己都搞混惹,所以乾脆依照上面原先內文的備註去翻了。

這邊這個重要的概念是,依賴者不應該自己建立自己的依賴對象,或者是知道自己的依賴對象是從哪來,他只要知道自己有沒有依賴對象就可,他從哪來,或者怎麼生出來的,對他而言並不重要,那因為區分了建立依賴對象的動作使得這個方法讓程式設計上較為低耦合。

簡單舉例來說,我有個 class 需要展示生物的走動,那在這個 case 要用到法米這隻狗,那因為某些需求,在實作這隻狗的時候需要他的表皮樣式。

val fami = Dog(skin)

那因為身長重量不可能無中生有,我必須要在 class 實作的時候提供

class CreatureShow(skin:Skin){

val fami = Dog(skin)

fun showCreatureWalk(){
fami.walk()
}
}

但實際跑一次後發現這樣的走動不太真實,於是被要求再加上身長:

class CreatureShow(skin:Skin, height:Int){

val fami = Dog(skin,height)

fun showCreatureWalk(){
fami.walk()
}

}

然後發現法米實際在走的時候跟月球漫步一樣,因為沒有重量,於是又加上了重量:

class CreatureShow(skin:Skin, height:Int, weight:Int){

val fami = Dog(skin, height, weight)

fun showCreatureWalk(){
fami.walk()
}

}

連續改了三次,覺得很微妙,明明這個 class 就只是為了展示該生物的走動模樣而已,為什麼要接二連三的為它定義外皮、身高、體重呢?這些數值只用了一次,就是為了定義法米,顯然定義這些數值並不是這個 class 的職責。

那這時候就改成依賴注入的模式:

class CreatureShow {
var fami: Dog? = null

fun setFami(fami: Dog?) {
this.fami = fami
}

fun showCreatureWalk() {
if (fami != null) {
fami.walk()
}
}
}

改成這樣,其實法米這隻狗需要什麼數值就跟這個 class 無關了,因為是從外面注入的,這邊只負責注入的 fami 存不存在,是否要走動而已,這樣就降低了 CreatureShow 這個 class 與 Dog 的關聯。

PS:其實 Kotlin 這樣寫可以不用寫 setFami(fami:Dog?) 這個 method,我這邊這樣寫是為了展示。

那很多時候需要標示清楚從屬關係,或者是重視依賴對象的產生時間,這個做起來 hen 花時間,所以有一個叫做 Dagger 的玩意,應該是處理這方面事情最著名的 framework,另外還有一個最近小生我很喜歡的 framework 叫 Koin ,這兩個就擇日再做說明唄。

還沒說完,關於 IoC 的抽象化

上面的 CreatureShow 其實也只做完 DI 的部分,但是這個 CreatureShow 實際上還不夠靈活。

舉例來說,今天被要求要做出法米是狗的走動模樣,同時也要法米是貓、法米是猴子、法米是哥吉拉的走動模樣,照這個 class 的樣式設計,顯然會至少需要分成四個 CreatureShow

class CreatureShow1 {
var fami: Dog? = null

fun setFaim(fami: Dog) {
this.fami = fami
}

fun showCreatureWalk() {
fami.walk()
}
}
class CreatureShow2 {
var fami: Cat? = null

fun setFaim(fami: Cat) {
this.fami = fami
}

fun showCreatureWalk() {
fami.walk()
}
}
class CreatureShow3 {
var fami: Monkey? = null

fun setFaim(fami: Monkey) {
this.fami = fami
}

fun showCreatureWalk() {
fami.walk()
}
}
class CreatureShow4 {
var fami: Godzilla? = null

fun setFaim(fami: Godzilla) {
this.fami = fami
}

fun showCreatureWalk() {
fami.walk()
}
}

這個 code 重複很多次,更要命的是,上面突然說要看到這四種生物,會跳,會甩尾巴,先不論要各自為各自的生物寫一個兩個方法,這四種 class 都需要另外再加上跳、甩尾巴,實在有點費功夫。

那是不是這個四個 class 可以濃縮成一個 class 就好呢?當然可以,畢竟已經控制反轉了,這個 class 就只負責演示生物的動作,生物為何物,從哪來、怎麼生的對該 class 而言並不重要。

那要怎麼開始呢?先將行為抽象化,現在知道有三個行為是走、跳、甩尾巴:

interface CreatureMovement{
fun walk()
fun jump()
fun shakeTail()
}

讓四種生物的型別繼承 CreatureMovement ,修改一下 CreatureShow:

class Dog : CreatureMovement {...}
class Cat : CreatureMovement {...}
class Monkey : CreatureMovement {...}
class Godzilla : CreatureMovement {...}

class CreatureShow {
var fami: CreatureMovement = null

fun setFaim(fami: CreatureMovement) {
this.fami = fami
}

fun showCreatureWalk() {
fami.toString()
}

fun showCreatureJump() {
fami.jump()
}

fun showCreatureShakeTail() {
fami.shakeTail()
}

}

改成這樣省事多了,至少在加動作的時候,不用因為每一個動物都有自己的 show class 而去重複更改數次。

那麼哪天上頭的人又發 Godzilla 展示毀滅的噴射藍光….呃,顯然噴射藍光會是 Godzilla 獨有的,是可以考慮把 CretureMovement 改成範型,不過這就會是另一個故事了。

結語

不知道為什麼,寫的過程又想到 No Sliver Bullet 。這個我從大學時期就讀過,至今又看了三次左右的論文。實作 IoC 確實可以解決不少問題,但它絕對不是萬靈丹,用了它之後要面對的問題依舊不少,而且也有可能因為導入了 IoC 而延伸不少問題(人員教育、規範變更之類的),軟體開發果然還是得考慮到此項方案是否合適此項專案。

其實原先是想要寫用 DI 工具並實作出 IoC 的心得(是說本來想寫 Rx 運算子的心得…結果心血來潮換寫 IoC 的心得惹),結果不知不覺就寫成了 IoC 的概念了,寫了這篇才又再次想起了,翻譯軟體工程的英文原來是這麼促咪的。如果有任何意見、問題、或者是對我的論點有質疑想要反駁,希望各位不要吝嗇提出疑惑對我多多指教。

--

--

Jast Lai
Jastzeonic

A senior who happened to be an Android engineer.