應對複雜軟體的思考
由於自己身處SAAS行業,在經歷了幾輪複雜需求的蹂躪之後,我一直試圖尋找一種解法,可以儘量cover住複雜多變的需求。在過去的一年中,通過反覆閱讀和實踐,似乎讓我對此有了一些清晰的思路,所以我想寫一點東西總結一下自己的這一年裡的思考。
在我們的專案初期,專案的規模可能比較小,程式碼量很少,我們的程式碼或許還能整理的比較乾淨,就像這幾組交換機的網線一樣,比較有條理。
但是隨著功能複雜之後,專案也隨之變得龐大,整個程式碼就可能會和這個機房一樣,非常的混亂。在經歷幾次這種狀況之後,於是我便在想,究竟是什麼問題導致了這種混亂。
首先來看一段程式碼( Kotlin Code
):
fun executeRequest(request: Request) : String { // 校驗身份 val isValidate = validateRequest(request) if( isValidate ) { return "Request is not valid" } // 處理業務 dealBiz(request) try { // 儲存資料 saveToDB(request) } catch (exception:Exception) { return "Occur error when save results to DB" } // 傳送訊息 val isSendSuccess = sendMessage(request) if (isSendSuccess == false) { return "message send unsuccessfully." } return "success" }
這是我們比較常見的一些程式碼結構,其實看起來問題也不算很大,但是隨著業務複雜, 業務邏輯 的控制和 控制邏輯 耦合的很厲害,閱讀這種" 麵條程式碼 "的成本越來越高。每一位新進入專案的夥伴猶如進入了一個“程式碼迷宮”。來來回回去尋找自己需要的那一段程式碼,實際上這個時候已經形成了:
只有上帝和我能看得懂的“上帝程式碼”了。
這顯然是我們不願見到的程式碼,在 左耳聽風專欄 裡有一篇《程式設計的本質》裡講到:
有效地分離 Logic、Control 和 Data 是寫出好程式的關鍵所在。
那什麼又是 Logic
, Control
, Data
呢?
- Logic : 就是一般的業務程式碼,類似上面程式碼中的
dealBiz()
,sendMessage()
等等 - Control : 對業務邏輯的流程控制,比如遍歷資料、查詢資料、多執行緒、併發、非同步等等
- Data :函式和程式之間傳遞的這部分資訊
如果有效地將這幾種程式碼分離,程式碼可讀性將會大大提升。通過這種拆分,我們也降低除了 自己之外的維護者 閱讀程式碼 翻譯業務 內容的成本。通過分離,我們可以將程式碼寫成這樣:
fun executeRequest(request: Request) : String { return Result .of(request) .flatMap { validateRequest(request) } .flatMap { dealBiz(request) } .flatMap { saveToDB(request) } .flatMap { sendMessage(request) } .fold( success = { return "success" }, failure = { return it.message } ) }
當然這裡說了如何寫細節的程式碼,那麼程式碼架構又如何去做才可能保證可以應對這麼多的變化?
一般的專案中我們把一個軟體系統進行分層,這是我們目前做工程專案的一個共識,我們最初學習的分層架構就是經典的三層架構了。它自頂向下分成三層:
- 使用者介面層(User Interface Layer)
- 業務邏輯層(Business Logic Layer)
- 資料訪問層(Data Access Layer)
到 資料訪問層 這塊,其實很多系統已經變成了 面向資料程式設計 ,最終做成了“資料庫管理系統”。按照傳統的三層模型,使用者介面的開發依賴Service層,而Service層又依賴著DAO,DAO對應著資料庫。大家相互依賴,業務邏輯一旦修改,就意味著要從DAO層開始修改,資料庫也跟著被修改,而往往隨著我們開發的深入,業務的模型會被不斷調整,這樣資料庫可能就要頻繁的變動。程式碼也開始變得複雜... ...
而在領域驅動設計中提出了另外一種 四層架構 ,在此之前,我想先分享《實現領域驅動設計》一書講的六邊形模型。
我們在設計系統的時候,往往過於關注資料庫,Http介面等基礎設施的設計,而忽略了我們需要關注的業務。在複雜系統中,最容易變化的也是業務形態,產品經常會要求改來改去,因為業務本身就在不斷地演進,如果我們一開始就基於資料庫作所有的設計,那麼勢必一旦遇上業務的修改,庫表肯定也需要對應先進行變化。假如我們融入六邊形架構,將資料庫和暴露的 Controller
都視為是基礎設施,先去關注業務的模型和程式碼, Class
的修改比要資料庫改起來要簡單的多。另外一方面,也大大提高了程式的可測試性:在沒有準備一堆基礎設施(資料庫,介面,非同步通知等等)情況下,可以先測試邏輯的完整性。
另外,有時候隨著業務增長有的基礎設施是會需要進行替換的,採用六邊形架構之後,這種更換的成本就會降低。另外如果出現需要使用Web Service的客戶,我們也不必糾結於之前的HTTP介面,直接開出一套新的協議程式碼供客戶使用,而不會糾結領域部分程式碼有邏輯上的缺失。
採用六邊形架構之後,我們的領域模型也會更加獨立,更精簡,在適應新的需求時修改也會更容易。在《架構整潔之道》之中提到的“整潔架構”也與“六邊形架構”大同小異。
其實這兩種架構也是 依賴倒置原則 很好的實現:
- 高層模組不應該依賴低層模組,兩者都應該依賴其抽象
- 抽象不應該依賴細節
- 細節應該依賴抽象
此時再回顧原來定義的四層架構:
- 使用者展示層
- 應用服務層
- 領域層
- 基礎設施層
他們的依賴關係如圖:
這樣我們可以將業務核心程式碼放入 領域層
之中,要應對的各個場景程式碼放入應用服務層中。將協議轉換、中介軟體和資料庫的適配都放入基礎設施層裡。在應用層與 Controller
之間的那些 VO
作為使用者展示層,以做出整潔架構。
當我們開始學習Java的時候,都知道Java是一門面向物件的語言,我們本可以將現實世界翻譯到程式碼的世界之中,但實際上我們往往在專案中只會將物件定義成 貧血模型 ,最終寫成面向過程的程式碼。如何做才能讓這個複雜的世界反應到程式碼裡呢?
讓我們再從需求說起,對於一個複雜的軟體,任何一個專案的參與者(包括初創的成員),都很難靠自己就看清整個專案的全貌,我們猶如圖中的盲人,大家可能最後對專案的理解都是不一致的。此時每一位參與者都猶如“盲人摸象”中的“盲人”,對需求(大象)只有片面的理解,於是乎,有的人覺得大象是水管的形狀,有的人覺得大象是扇子一樣的形狀,有的人說大象長得跟柱子一樣... ...
通過討論,我們會對自我的認知進行一些修正,最終大致得出一個需求的全貌。
比如我們要去識別一些系統邊界,在DDD的戰略設計中非常強調劃分 界限上下文 。比如我作為一個個體,在不同的場景中,我的身份、角色都不大相同。
猶如在上圖中,在“地鐵”、“家庭”和“公司”中,我的身份是不一樣的,但是我依舊是我,找出業務的場景,也就意味著我找到了系統的邊界。通過分析場景識別邊界來找出系統的核心領域和支撐領域,以此來最終確定系統的數量,降低系統的耦合。
我們還可以用 四色建模方法 來識別出我們系統中發生的整個流程,發現究竟是誰通過什麼方式觸發了什麼事情,最終又影響了哪些物件。
最終通過我們找出的事件,整理出一個能夠讓我們進行溝通的模型。在我們的模型被構建得相對完善之時,其實程式碼也差不多已經被構建出來了,因為這個時候再去回想 面向物件 的設計,我們發現模型即程式碼,程式碼即模型。
一直以來我都希望通過一些好的“工程實踐”來提高團隊的效率以及我們的程式碼質量,我想這也是思考這些架構的意義吧。我想用《架構整潔之道》中的一句話來做本文的總結:
軟體架構的最終目標是,用 最小 的人力成本來滿足 構建和維護 系統的需求。
正如《人月神話》裡說的一樣,軟體工程裡沒有“銀彈”,即使做了整潔架構也無法避免需求的變化和延期,只是希望當我們身處需求的困境中時,仍能給自己以更多的選擇。
參考資料&活動 :
- 《領域驅動設計》
- 《架構整潔之道》
- 《實現領域驅動設計》
- 從三明治到六邊形架構 http://insights.thoughtworkers.org/from-sandwich-to-hexagon/
- 運用四色建模法進行領域分析 https://www.infoq.cn/articles/xh-four-color-modeling
- 領域驅動戰略設計實踐 https://gitbook.cn/gitchat/column/5b3235082ab5224deb750e02
- 左耳聽風 https://time.geekbang.org/column/48
- 領域驅動設計中國峰會DDD-China