使用Zenject
May 23, 2017
過去的時間一直花在了解Zenject的使用上,Zenject誠如它自己所提及的,是一套以關連(Binding)為主要核心功能的函式庫。由於是以Binding為出發點,故在使用上,可以當做相依性注入(Dependency Injection,DI)的函式庫。雖然這個觀念很難被植入到Unity開發的環境中,但隨著時間經過,Version 3到Version 4而現在邁入了Version 5,總算看到了不少的開發者對它有更多的關注,而它本身的版本演進,也一步步更符合在Unity環境中可使用的DI。
不論是從Unity Asset Store(UAS)或是直接從GitHub裡拿取後自行置入都可,但也不論是哪個版本,在放入到Unity中,都會產生編譯時會出現的錯誤,必須將其中一個檔案整個移除,才能順利進行編譯。還好此檔案是Editor中的某個整合測試用檔案,完全不影響到任何實際的功能,故移除後也不會有不預期的功能發生問題。
Zenject裡有二個Demo,一個是初學者可以參考的,而另一個則是較進階的用法。雖然用法上大致可從此二個Demo了解,但DI的概念本身就有些複雜加上要置入到Unity的環境中,其實並不容易。所以不但要反覆的看此二個專案,最重要的,還是要去翻閱Google Group中相關的討論串,看到別人使用後產生的問題,記下來後再親自去試驗,但饒是如此,還是會花去不少時間才能夠真正的將其用在專案在,享受Zenject給予的好處。
在花了不少時間後,總算可以將Zenject套用在目前進行的專案裡,並用它子容器概念所實現的全域相依性引用,更是可以安全且完善的取代以往需要用單例模式(Singleton)並實現出階層式的Singleton。
Zenject和一般的DI函式庫並沒有太大想法上的不同,但比較偏向以Binding為主要出發點的設計。不論是在其文件上或是範例專案中都可以看到MonoInstaller、Container等使用。對於一個以Unity環境為出發點設計出的DI,有些地方仍是和一般的DI有些不同。
簡單來說,不論是哪種DI函式庫,會在某個地方宣告相依性如何解決,要參考哪個型別為主,後續的引用則會是以此宣告為主。在DI的領域中,這個步驟是稱為產生Object Graph。也就是說如果以往有個界面(interface),由於界面是虛的,必須產生物件後才能進行使用,故常會有類似這樣的寫法:
`IPlayer player = new Player();
如果從DI的概念去看,這是不好的方式,在產生物件的當下,產生了一定程度的相依性。相依性本身並不沒有問題,畢竟沒有相依性是沒有辦法寫出任何較具規模的專案,但有問題的地方通常在於產生相依性的時間點。
專案在執行時,若是在執行某一處時需要某個物件的功能時才做產生的動作,負責產生的相依性並使用的那個型別是否有足夠的資訊確定要產生何種型別出來使用?除此之外從單一責任(Single Responsiblity)的角度去看,也是很不妥善的,該型別主要的任務並不包含產生相關的物件後進行引用。符何Single Responsiblity的想法需要的相依物件早已產生後,只需在當下進行引用即可。
然而該型別在運行時,要確保執行時所有相依的物件皆已產生,否則會有空引用的異常(NullReference Exception)發生。為了要確保任何時段所關聯到的物件都準備好,通常一般的DI概念中,相依到的物件多數都是從建構式(Constructor)那獲取到相依物件,進行Cache後以確保之後的引用都不會發生問題。
而為了要確保每個型別的Constructor都可以拿到已產生好的型別,通常都會在程式一開始的進入點如Main函式中,將Object Graph產生出來,以利後續的注入。
到此,也就是一般的DI概念,但換到了Unity的環境中,Zenject做了一些調整以適應其不同的生態。首先就是Unity的物件主要是MonoBehaviour的子物件,而這些子物件官方不建議用建構式,取而代之的是用Awake和Start這二個函式進行初始化給值的地方。若是Zenject遵循以往的DI架構,則會難以在MonoBehaviour的子物件上進行。為此它做了個調整,加入了Inject屬性(Attribute),讓有此屬性的函式做為初始化的地方。
而另一點就是在Unity的專案中並沒有Main 函式可用,而對於專案中的進入點來說,主要是場景先做區格,故Zenject產生了一個SceneContext的元件,當做此場景中最先的進入點,配合其MonoInstaller的型別,在MonoInstaller中進行Object Graph的定義,以達成之後相依性引用的依據。