l

2018年7月5日 星期四

Clean Architecture(5):架構三原則二部曲—相依性原則

July 5 16:17~17:23

螢幕截圖 2018-07-05 17.21.14


前言

軟體架構依據「距離I/O遠近」分層之後,接下來遇到一個問題:「如何管理各層之間的相依性?」。在《Clean Architecture》書中,把這個規則稱為相依性原則(Dependency Rule)。

***

低層相依於高層

《Clean Architecture》書中所定義的相依性原則只有如下的短短一句話:

Source code dependencies must point only inward, toward higher-level policies. (程式碼相依性必須只能往內,指向更高層級的策略。)


▼如下圖所示(圖片來源在此),內圈(較高層級)的元件不能知道(相依)任何外層的元件。也就是說,外層的函數、類別、變數或任何有名字的軟體實體,都不能出現在內層的元件之中。相依性只有一個方向,就是由外往內,由低層往高層。

CleanArchitecture

***

煩人的實作細節

相依性原則本身非常簡單,但實務上在系統中要完全落實此原則有許多細節要注意。

▼例如下圖中位於Entities層(最高層)的Host類別,依據相依性原則它不能相依於任何位於此層以外的「任何東西」。為了將Host物件儲存於資料庫中,Teddy使用了javax.persistence package裡面的annotation(@Entity、@Table、@EmbeddedId、@Column等)。

螢幕截圖 2018-07-05 16.34.14

雖然這些annotation是 Java Persistence API(JPA)標準的一部分,但它們的實作是由Hibernate所提供。嚴格來說,上面的程式違反了《Clean Architecture》的相依性原則。

如果不用annotation,也可以用XML檔案來註記,把對hibernate-jpa-2.1-api的相依性從原始碼中移除。但廣義的來講,雖然在原始碼中Host不直接相依於hibernate-jpa-2.1-api,但透過XML設定檔案還是間接的相依於hibernate-jpa-2.1-api,Teddy認為兩者效用實際上是差不多,只是把「顯性相依」轉成「隱性相依」,並沒有好到哪裡去,甚至更糟糕。

***

要完全把Host對hibernate-jpa-2.1-api的相依性拿掉,一個常見的作法是定義另一組針對持久化(Persistence )所需的物件,例如針對原本的Host物件的持久化需求另外定義一個HostEntity類別,如此一來原本Host類別中JPA annotation就可以移到HostEntity之中,然後再透過Hibernate把HostEntity直接存到資料庫。

但這種做法又衍生另其他問題,首先,必須另外撰寫物件負責把Host轉成HostEntity,以及把HostEntity轉成Host。這種物件稱之為Mapper

其次,HostEntity要擺在哪一層也是需要考慮一下,很顯然不會擺在Entities這一層。因為Repository通常被Use Case所使用,所以放在Use Cases層似乎很合理:當要儲存Host時,Use Case先產生或是獲得一個Host物件,然後呼叫Mapper把Host轉成HostEntity,在透過Repository將HostEntity存到資料庫。反之,要讀取Host時,Use Case透過Repository從資料庫讀取HostEntity,再把它轉成Host。

但是這麼做其實還是違反了相依性原則,因為Use Case相依於外部的hibernate-jpa-2.1-api。忙了大半天其實是白忙一場XD。

***

另一種作法是,不透過任何JPA annotation來簡化物件永久化的問題,只定義Repository介面,然後透過main元件注入Repository介面的實作。Repository介面的實作程式碼位於Interface Adapter層,也就是《Clean Architecture》架構圖裡面的Gateways。它再透過相依性注入的方式,獲得不同持久化框架的實作,如此一來便符合相依性原則。

但是,原本只要貼一貼annotation就可以把透過持久化框架(例如Hibernate)達到持久化的目的,現在為了嚴格遵守相依性原則,程式碼變得麻煩好多。這就是在實作上必須注意與取捨的地方。

***

結論

滿足相依性原則可以獲得兩大好處,首先,因為高層的物件不再相依於框架等低層的物件,所以測試碼可以單獨測試,大幅簡化測試的工作。

其次,系統的核心功能位於Entities與Use Cases這兩層(蛋黃區與蛋白區),要更換I/O或是應用程式框架「理論上」變得簡單很多。例如,如果要將Web-Based UI換成Android App,只需異動最外面兩層(Framework & Drivers與Interface Adapters),便可直接銜接原本的Use Cases與Entities。

***

友藏內心獨白:跟有潔癖的人住在一起家裡肯定很乾淨,但你可能會被對方煩死。

沒有留言:

張貼留言