更新於 2024/08/31閱讀時間約 10 分鐘

閒談軟體設計:友善的距離

https://en.pandaid.jp/hygiene/social-distancing

https://en.pandaid.jp/hygiene/social-distancing

心態轉變

因為組織調整,我又變成 architect 了 (2016 ~ 2017),早期對 architect 有很多憧憬,面對問題,心中總有一張設計圖,但實際當 architect 後,發現這是一個很不容易的職位,特別是要能說服別人使用某個設計或不用某個設計,另外,是在有時程壓力下,對一個有點歪的架構上,如何微妙地讓它保持像比薩斜塔般不至於垮掉,還能持續成長,又是另一個難題。

在讀《建構微服務》有段內容讓我玩味很久:

我們借用其他行業的名稱,稱自己為 software engineer 或 architect,但我們不是,對吧?architect 與 engineer 的嚴謹性和紀律性是我們在現實中無法冀求的,而且他們對社會的重要性是眾所周知的。記得有一次我和一位朋友聊天,就在他成為合格 architect 的前一天,他說:『明天,如果我在酒吧裡建議你如何建立某個軟體,而它是錯誤的,我就得承擔責任,我可能會被告,因為,我在法律上是一個合格的 architect,如果我犯錯,就必須負責』

怎樣才是稱職的軟體架構師呢?老實說,我恐怕還沒那資格說。

設計抉擇

軟體架構有很多 pattern,坊間也有很多書探討,像是《Pattern-Oriented Software Architecture》系列、《Software Architecture in Practice》、《Patterns of Enterprise Application Architecture》,但當我們套了這些 pattern 時,我們真的該用嗎?用對了嗎?還是只有有個殼而已,內容根本不是那麼一回事呢?

若深究每個 pattern 的形式,都會找到情境 (context)、遭遇的問題 (forces) 和對應的平衡解 (solution),但再仔細想想,這些解都圍繞在 separation of concerns 上,將不同的問題分離,以合適的方式處理,因為我們總是希望有個 high cohesion 及 loose coupling 的系統,但在面對實際的設計抉擇時,有時反而會做出違背上述兩原則的選擇,因為每個專案要考慮的因素都不同 (參考閒談軟體設計:設計抉擇的因素)。

例如,為了不重新造輪子,我們使用第三方的函式庫,這聽起來很合理,但我們真的了解我們引入的函式庫嗎?我們對該函式庫的掌握度有多高呢?每個被引入的函式庫意味著一種 coupling,不論是在 Java 上使用 Maven 或 Gradle 或是在 Objective C 中使用 CocoaPods,在編譯時,會看到這些套件管理工具幫我們下載眾多的第三方函式庫,這意味著我們不用重寫這些東西,開發效率能提升數倍甚至數百倍,但我們真的都能掌握這些 coupling 嗎?當這其中任何一個環節出錯,我們的系統架構真的很優雅地應付嗎?

這是為什麼我蠻喜歡 Onion Architecture (洋蔥架構) 和 Hexagonal Architecture (六角架構) 的原因了,在過去的專案中,我並沒有刻意使用這二個架構,畢竟我進去時,早已有龐大的程式碼基礎,不可能說改就改,只有在 Android 專案起始時,因為我是較資深的軟體工程師 (那時還不是架構師),主導整個架構走向 (參考閒談軟體設計:Android App Architecture),即便如此,我也只堅持 domain 要與 Android SDK 分離,維持使用而不相依的關係,而這也造就了後來開發PC 版時,有完整的 domain 核心可以直接使用不需修改,雖然這也不在當初的規劃就是了,但也省去了大量重複開發的時間。當要離職準備交接時,回頭檢視架構,上述二個架構的影子就穿插在程式碼之間了。

抽象滲漏

可能跟過去在學校的 OOAD 訓練養成有關,從 problem context 和 use case 中提取名詞,接著提取動詞找出關係與函式,然後利用 GRASP 逐步建構出整個 domain model 與 design model,這中間,完全沒思考過 UI (但如何與系統互動很重要) 與框架,可能是這樣,我後來在做設計時,很自然地就與框架保持距離,不論是用 C# 寫 Windows Forms 還是用 Java 寫 Web applications 。但這其實並不容易,我剛開始工作時,看《Hibernate in Action》時,有幾句話讓我印象深刻,第一句是:

We use transparent to mean a complete separation of concerns between the persistent classes of the domain model and the persistence logic itself, where the persistent classes are unaware of — and have no dependency to — the persistence mechanism.

其實,我畢業前就自學了 JPA,之後才學 Hibernate,上面那句話讓我開始思考,雖說 JPA 的 annotation 很方便,但不正是破壞了 transparency 嗎?但 Hibernate 也無法達到完全的 transparent:

We regard transparency as required. In fact, transparent persistence should be one of the primary goals of any ORM solution. However, no automated persistence solution is completely transparent: Every automated persistence layer, including Hibernate, imposes some requirements on the persistent classes.

因為再怎麼樣抽象化,資料庫對資料的描述與物件導向語言對物件的描述,存在著無法消除的 paradigm mismatch (有興趣可以找書來看,《Hibernate in Action》在第一章的第二節,花了整整一節說明這不匹配的情況),這讓我想起《約耳趣談軟體》第 26 章抽象滲漏法則中的一段:

所有重大的抽象機制在某種程式上都是有漏洞的。

而且有時候這些框架或工具會反過來影響 domain model 的設計,舉例來說,從 OO 設計的角度來看關係,若要好維護,一般會以單向關係 (unidirectional reference) 為主,但若要使用 ORM 或 CoreData 工具,為了確保工具能檢查資料完整性,會反過來在 domain model 上加上雙向關係 (bidirectional reference),但程式碼卻不見得需要去維護這雙向關係 (部分是 ORM 工具處理),這導致讀程式碼時會有點奇怪。

另一個例子是,過去在學 RDBMS 時,會學到正規化 (normalization)、主鍵 (primary key)、外來鍵 (foreign key)、索引 (index) 及一些 RDBMS 能在資料完整性幫上忙的工具,像是 cascade delete 等東西,因此 ORM 工具也常把這些資訊滲漏出來,滲漏也許還好,但把物件關聯的維護轉交給 ORM 工具上 (依賴 cascade delete 刪除不該存在的關聯),就是值得討論的設計,到底這物件間關聯的維護是商業邏輯層的責任,還是資料儲存層的責任?如果哪天,資料儲存層換了,偏偏不支援原有的 metadata (例如不支援 JPA 的annotation),那物件關聯的維護該怎麼處理?

保持友善的距離

所以重點是如何取得平衡?以上述的 ORM 工具來說,極端的兩邊:完全不使用 ORM 工具和毫不顧忌的讓 ORM 工具散布在 domain model 中,又或者是將 ORM 的滲透透過其他方式控管在特定的範圍中,例如:再建立一層抽象 (DAO 或 Repository),在實作中建立 ORM 所需的 data model;又或者是使用污染性較低的方案,例如:以傳統的 XML 取代 JPA annotation 描述 metadata,事實上,這沒有標準答案,每個軟體架構師的選擇都不同,上述二兩種折衷方案我都用過,也曾經完全不使用 ORM 工具,完全視情境而定,但原則到沒什麼不同,與框架及工具保持友善的距離,這同樣影響我之後在開發 iOS 時使用 CoreData 的方式。

或許是這樣,感覺自己比較像是 old-school 的軟體架構師,在選擇第三方函式庫或是框架時,相對比較保守,有時,還會為組織內部重新打造輪子,像是曾經在 Android 專案中復刻 iOS SDK 的 NSNotificationCenter,事實上,在 GitHub 上可以找到類似的第三方函式庫,像是 EventBus,但要不要採用一個第三方函式庫,除了該函式庫穩不穩定、文件夠不夠充足,還要看是否符合專案與組織的特性。

就專案來說,Android 專案分成兩個子專案:一個是只有 domain model 的 pure Java 專案,另一個是實際 Android UI 的專案,若要導入 EventBus,就只能在 Android UI 的專案中使用,因為 domain model 沒有相依 EventBus 所需的 Android SDK,這樣並無法滿足當初想用 NotificationCenter 減少 model 與 UI 之間 coupling 的初衷;另外,考量到希望 iOS developer 也能協助開發 Android,所當時以決定自己動手寫,並且在進行在復刻時,API 命名特意維持與 NSNotificationCenter 相似。

心得

因此,軟體架構不是一旦決定了就穩固了,它需要後續開發時,時時想著當下這個設計是否會把架構搞歪了,架構需要細心的照顧,否則很容易歪掉,歪掉也許沒事,程式也可能還能繼續正常執行,但埋伏在裡面的技術債,何時會引爆則是未知,一旦反撲,對專案的影響不僅是時程,還有開發團隊對整體架構的信心。

分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.