前二章我們談了架構分層以後,相信大家對一般程式開發所該有的結構都有了初步的了解,架構的分層是為了隔離依賴,讓程式碼的依賴關係提升變成模組間的關係,那我們該怎麼管裡或提供程式碼間的依賴呢?
在 SOLID Principle 這章我們談到 Dependency Inversion Principle 的時候有提到,高階物件不應該依賴於低階物件,二者最好都依賴於抽象,而抽象的實體由外部提供,這好處是當外面 dependency 變化的時候我們不用改既有程式碼,同一份程式碼也可以在多個情境下重用。
這種依賴的實體由外部提供的行為就叫做 Dependency Injection,有以下幾種 inject 的方式:
setXXX
的 function 讓外部呼叫直接設定 dependency,比起 Constructor 的方式較彈性,但可能要自己掌控呼叫順序的時間點。雖然很複雜但精神上都是一樣的,那就是 dependency 應該由外部注入,有了這個概念後我們就一起來看個例子吧:
interface Wood
interface Water
interface Ink
interface Paper
interface Printer
class MyWood : Wood
class MyWater : Water
class MyInk(val water: Water) : Ink
class MyPaper(val wood: Wood) : Paper
class MyPrinter(val paper: Paper, val ink: Ink) : Printer
Printer
是由 Ink
跟 Paper
組成,而 Ink
由 Water
組成、Paper
由 Wood
組成,所以我們會有個地方需要產生所有 dependency 如下:
val printer = MyPrinter(MyInk(MyWater()), MyPaper(MyWood()))
非常的醜,如果分成很多行的話:
val water = MyWater()
val wood = MyWood()
val ink = MyInk(water)
val paper = MyPaper(wood)
val printer = MyPrinter(ink, paper)
還是非常的雜亂,有沒有辦法優雅的完成 dependency injection 呢?
Dagger is here to the rescue!
由於由外部提供 dependency 這件事情非常繁瑣且較無生產力,dagger 透過 annotation processing 的方式幫我們在 compile 之後動態生成這些程式碼。我們要做的事情就是標註 annotation 跟簡單設定而已,而在 dagger 裡需要以下四大重要的 annotation:
如果你沒看過 annotation processing 的介紹可以參考:
https://ithelp.ithome.com.tw/articles/10222408
起手式一樣是 dependency 的宣告:
dependencies {
implementation 'com.google.dagger:dagger:2.21'
kapt 'com.google.dagger:dagger-compiler:2.21'
}
建立一份 PrinterModule
的程式碼來提供 Printer
相關的 dependency 如下:
@Module
class PrinterModule {
@Provides
fun providePrinter(paper: Paper, ink: Ink): Printer {
return MyPrinter(paper, ink)
}
@Provides
fun providePaper(wood: Wood): Paper {
return MyPaper(wood)
}
@Provides
fun provideWood(): Wood {
return MyWood()
}
@Provides
fun provideInk(water: Water): Ink {
return MyInk(water)
}
@Provides
fun provideWater(): Water {
return MyWater()
}
}
dagger 可以可以透過參數的方式幫我們完成遞迴的取得 dependency,比如說 providePrinter
裡需要 Paper
跟 Ink
,我們可以把它定義成參數假設他會由外部傳進來:
@Provides
fun providePrinter(paper: Paper, ink: Ink): Printer {
return MyPrinter(paper, ink)
}
然後 dagger 會自動去找回傳型別相同的 providePaper
跟 provideInk
提供給 providePrinter
當 function 的參數。
@Provides
fun providePaper(wood: Wood): Paper {
return MyPaper(wood)
}
@Provides
fun provideInk(water: Water): Ink {
return MyInk(water)
}
值得注意的是 function 名字並不重要,重點在於回傳的型別,而 Wood
跟 Water
如果有需要的話,也可以一層一層遞迴地去尋找正確的 dependency,如果尋找的過程有一個地方失敗的話,會導致 compile fail ,所以我們的使用者不會遇到問題,這也是 annotation processing 的一個蠻好的優點。
另一個值得一提的是我們在這個檔案裡的 function 的參數與回傳都只有指定 interface 型別,將來我們要替換的時候就會非常方便。
第二步驟就是要在我們需要 dependency 的物件上,標上 @Inject
讓 dagger 知道我們需要 inject 的物件位置與名稱是什麼,假設我們需要在 MainActivity
使用 Printer
的話,範例如下:
class MainActivity : AppCompatActivity() {
@Inject
lateinit var printer: Printer
}
有了整個 object graph 之後,第三步就是要靠 @Component
來串接 module 跟需要 dependency 的物件,我們建立 PrinterComponent
如下:
@Component(modules = [PrinterModule::class])
interface PrinterComponent {
fun inject(mainActivity: MainActivity)
}
PrinterComponent
透過 modules 來指定連結的 module class,而 inject
function 代表著 dependency 的需求者可以透過呼叫這個 function 讓 dagger 提供 dependency。
特別注意 PrinterComponent
是個 interface,compile 之後 dagger 會幫我們建立真正的 DaggerPrinterComponent
類別。
接下來回到 MainActivity
把 component 的實體建立起來如下:
class MainActivity : AppCompatActivity() {
@Inject
lateinit var printer: Printer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
DaggerPrinterComponent.create().inject(this)
}
}
呼叫了 inject
之後,printer
就會自動被設值了,而且 MainActivity
並不知道 Printer
的具體 class 在哪,所有依賴都建構在 interface 之下,這樣的程式是不是乾淨許多呢?
當我們哪天需要換 dependency 的時候,比如 MyPrinter
要換成 EpsonPrinter
或是 MyPaper
要換成 DoubleAPaper
,都只要在 PrinterModule
改建構實體的程式碼即可,整個 graph 都會自動更新,所有的程式還是會依賴同樣的 interface 來運作,維護起來是不是也很方便呢?
@Provides
fun providePrinter(paper: Paper, ink: Ink): Printer {
// return MyPrinter(paper, ink)
return EpsonPrinter(paper, ink)
}
以上就是今天的全部內容了,
如果有興趣的話可以接著看 Dagger as a Pro,
希望對大家都有幫助!
Android 十全大補已經正式出書上架囉!
有興趣的讀者歡迎參考:
https://www.tenlong.com.tw/products/9789864345786