什麼是整潔的架構
- 2019 年 10 月 24 日
- 筆記
看完了clean code — 代碼整潔之道,那麼接下來就應該讀讀其姊妹篇:clean architecture — 架構整潔之道。不過對我而言,代碼是實實在在的,看得見,摸得着;而架構雖然散發著光芒,但好像有點虛,似乎認知、思考還比較少。本文主要記錄《clean architecture》的主要內容以及自己的一點思考。
本文地址:https://www.cnblogs.com/xybaby/p/11729354.html
架構的存在意義
clean architecture的作者是一位從事軟件行業幾十年的架構大師,參與開發了各種不同類型的軟件,在職業生涯中發現了一個規律:那就是,儘管幾十年來硬件、編程語言、編程範式發生了翻天覆地的變化,但架構規則並沒有發生變化。
The architecture rules are the same!
我想讀過clean code之後,應該都達成了以下共識
getting it work is easy
getting it right is hard
right make software easy to maintain、change
上升到架構層面來說,問題同樣存在,而且更加明顯,因為架構的影響面遠大於代碼。作者舉了一個例子,展示了隨着代碼量增加、團隊人員增加、release版本增加,導致的新增代碼代價的激增以及程序員生產力的下降。
從可以看到,隨着時間的推移,每一行代碼的代價(成本)都在逐漸上升。
從另一個角度來看
單個程序員的產出隨着 release急劇 下降,即使為了一個小小的feature,也不得不到處修修改改,容易牽一髮而動全身
moving the mess from one place to the next
這樣的經歷,我想大家都有或多或少的同感,尤其在項目後期,或者團隊人員幾次輪換之後,代碼就變得難以維護,以至於沒有人敢輕易改動。出現這樣的問題,不能僅僅歸咎於code — code這個層面關注的是更為細微具體的東西(比如命名、函數、注釋),更多的應該是設計出了問題,或者說架構出了問題。
因此說,軟件架構的目標是為了減少構造、維護特定系統的人力成本
The goal of software architecture is to minimize the human resources required to build and maintain the required system.
behavior vs architecture
行為和架構是軟件系統的兩個價值維度,行為是指軟件開發出來要解決的問題,即功能性需求;而架構則算非功能性需求,比如可維護性、擴展性。很多程序員迫於各種壓力,可能覺得只要實現功能就行了;殊不知,非功能性需求也是技術債務,出來混,遲早是要還的。
怎麼看待二者的關係呢,這裡祭出放之四海而皆準的艾森豪威爾矩陣:
behavior: 緊急,但不總是特別重要
architecture:重要,但從來不緊急
了解過時間管理或者目標管理的話,都知道重要但不緊急的事情反而是需要我們特別花時間去處理的。
而架構設計就是讓我們在支撐功能的同時,保證系統的可維護性、可擴展性。
design level
軟件開發和修房子一樣,在實施角度來看都是從low-level到high-level的過程,比如房子是由磚塊(brick)到房間(room),再由房間到房子(house)。作者的類比如下
software | building |
---|---|
programming paradigms | brick |
module rule(solid) | room |
component rule | house |
在我看來,clean code中強調的變量名、函數、排版更像是軟件開發中最基礎的單位,不同的programming paradigms遵循的思想是不同的,但代碼質量(整潔代碼)是獨立於編程語言的。
module rule(solid)
module(模塊)一般的定義即單個源文件,更廣義來說,是一堆相關聯的方法和數據結構的集合。
關於這部分,在clean architecture中講得並不是很詳細,於是我結合了《敏捷軟件開發》(Agile Software Development: Principles, Patterns, and Practices)一書一起學習。
SOLID是一下幾個術語的首字母縮寫
- SRP(Single responsibility principle):單一職責原則,一個module只有一個原因修改
- OCP(Open/closed principle):開放-關閉原則,開放擴展,關閉修改
- LSP(Liskov substitution principle):里氏替換原則,子類型必須能夠替換它們的基類型
- ISP(Interface segregation principle):接口隔離原則,你所依賴的必須是真正使用到的
- DIP(Dependency inversion principle):依賴導致原則,依賴接口而不是實現(高層不需要知道底層的實現)
SRP
module級別的SRP很容易和函數的單一職責相混淆。函數的單一職責是一個函數只做一件事 — 這件事通過函數名就可以看出來。而SRP則是指一個module僅僅對一個利益相關者(actor)負責,只有這個利益相關者有理由修改這個module。
違背SRP,會導致不相關的邏輯的意外耦合,如下面這個例子
Employee這個類裏面包含了太多的功能:
save
是給CTO調用CalculatePay
是給CFO使用- 而COO則關心
reportHours
。
問題在於,CalculatePay
也依賴ReportHours
,如果CFO因為某些原因修改了ReportHours
,那麼就會影響到COO。
這個例子也表明,一個類是對什麼東西的抽象並不是最重要的,而在於誰使用這個類,如何使用這個類。
解決方法之一是使用Facade模式,如下所示
Facade模式保證了對外暴露同樣的三個接口,但其職責都委託給了三個獨立的module,互不影響。
LSP
對於繼承而言,子類的實例理論上是滿足基類的所有約束的,比如Bird extend Animal,那麼Animal的所有行為bird都應該滿足。
但上面也描述過,類的有效性取決於類的使用方式,並不能用人類的認識去判斷。比如正方形是否應該繼承自長方形(square is a rectangle?),按照正常人的認知來說肯定是的,但對於某些使用方式就會存在問題, 比如下面這個函數
def g(Rectangle &r) { r.setW(5); r.setH(2); assert(r.area() == 10); }
上述的代碼表明,g
函數的編寫者認為存在一種約束:修改rectangle的長不會影響寬。但 這個對於squre是不成立的,因此square違背了某種(隱式的)契約,這個契約是關於如何使用rectangle這個類的。
如何傳達這個契約呢,有兩種方式,第一是單元測試;第二是DBC(design by contract)。
詳見討論: 你會怎樣設計長方形類和正方形類?
ISP
接口隔離原則解決的是「胖」接口問題,如下圖所示:
OPS
所提供的三個接口是給三個不同的actor使用的,但與SRP要解決的問題不同,在這裡並不存在因公用代碼導致的耦合。真正的問題是 Use1對op1
的使用導致OPS的修改,導致User2 User3也要重新編譯。
解決方法是引入中間層,如下所示
當然,靜態語言之間的源碼依賴才會導致 recompilation and redeployment; 而對於動態語言(如python)則不會有這個問題。
ISP is a language issue, rather than an architecture issue.
不過,不要依賴你不需要的東西,這個原則總是好的。
DIP
DIP(Dependency inversion principle)是架構設計中處理依賴關係的核心原則,其反轉的是依賴關係。比如一個應用可能會使用到數據庫,那麼很自然的寫法就是
graph LR App-->MySql
Business rule依賴Database的問題在於,database的選擇是一個細節問題,是易變的,今天是mysql,明天就可能會換成Nosql,這就導致Business rule也會收到影響。所以需要依賴反轉,就是讓database去依賴Business rule
graph LR App-->DB_Interface Mysql-->DB_Interface
Business rule依賴抽象接口,而database實現了這個抽象接口,接口一般是穩定的,因此即使替換DB的實現,也不會影響到Business rule。
這也提供了某種暗示:對於java C++等靜態類型語言,import include應該只refer to 接口、抽象類,而不是concrete class。
OCP
OCP是下面兩個短語的縮寫
- open for exrension: 當應用的需求變更時,我們可以對模塊進行擴展,使其滿足新需求
- close for mofifacation: 對模塊進行擴展時,無需改動模塊的源代碼或者二進制文件
很容易想到,有兩種常見的設計模式能實現這樣的效果,就是Strategy與Template Method。
要實現OCP,至少依賴於SRP與DIP,前者保證因為不同原因修改的邏輯不會耦合在一起,後者則保證是邏輯上的被使用者依賴使用者,從Strategy模式的實現也可以看出。
其實我覺得OCP應該是比其他幾個module rule抽象層級更高的原則,甚至高於後面會提到的component rule,軟件要可維護性、可擴展性強,那麼就最好不要去修改(影響)已有的功能,而是添加(擴展)出新的功能。這是不證自明的。
component rule
什麼是component呢,component是獨立開發、獨立部署的基本單元,比如一個.jar、.dll,或者python的一個wheel或者egg。
component rule主要解決兩個問題,第一是哪些module可以形成一個component,即component cohesion,組件的內聚問題;另一個則是不同的component之間如何協作的問題,即component coupling
component cohesion
哪些module或者類應該放在一起作為獨立部署的最小實體呢,取決於以下幾個規則
REP:THE REUSE/RELEASE EQUIVALENCE PRINCIPLE
The granule of reuse is the granule of release.
復用/發佈等同原則:即軟件復用的最小粒度等同於其發佈的最小粒度。
這是從版本管理的角度來思考軟件復用的問題,通過版本追蹤系統發佈的組件包含了每個版本修改的bug、新增的feature,才能讓軟件的使用者能夠放心的選擇對應的版本,達到軟件復用的效果。
CCP:THE COMMON CLOSURE PRINCIPLE
共同閉包原則:如果一些module因為同樣的原因做修改,並且改變次數大致相同,那麼就應該放在一個component裏面。這個是其實就是將單一職責原則(SRP)應用到component這個level
This minimizes the workload related to releasing, revalidating, and redeploying the software
可見,CCP的目標是較少發佈、驗證、部署的次數,那麼是傾向於讓一個component更大一些。
CEP:THE COMMON REUSE PRINCIPLE
共同復用原則: 總是被一起複用的類才應該放在一個component裏面。這個是接口隔離原則(ISP)在component level的應用
Thus when we depend on a component, we want to make sure we depend on every class in that component
與CCP的目標不同,CEP要求總是一起複用的類才放在一起,那麼是傾向於讓一個component更小一些。
component coupling
組件之間要相互協作才能產生作用,協作就會導致依賴。
比如組件A使用到組件B(組件A中的某個類使用到了組件B中的某個類),那麼組件A就依賴於組件B。在這樣的依賴關係裏面,被依賴者(組件B)的變更會影響到依賴者(組件A),在Java,C++這樣的靜態類型語言裏面,就體現為組件A需要重現編譯、發佈、部署。
架構設計的一個重要原則,就是減少由於組件之間的依賴導致的rebuild、redeploy,這樣才能減低開發、維護成本,最大化程序員的生產力。
ADP: Acyclic Dependencies Principle
無環依賴原則:就是在組件依賴關係圖中不應該存在環。
上圖中右下角Interactors
,Authorizer
,Entities
三個組件之間就形成了環裝依賴。環裝依賴的問題是,環中的任何一個組件的修改都會影響到環中的任何組件,導致很難獨立開發部署。另外,Database
組件本身是依賴Entities
的,現在Entities
在一個環中,那就相當於Database
依賴整個環。也就是說,對外而言一個環中的所有組件事實上形成了一個更大的組件。
如何解環呢?
一種方法是使用依賴倒置原則DIP,改變依賴順序
另一種方法是抽象出新的通用component
SDP: Stable Dependencies Principle
穩定依賴原則
Any component that we expect to be volatile should not be depended on by a component that is difficult to change. Otherwise, the volatile component will lso be difficult to change
其實就是說,讓易變(不穩定)的組件去依賴穩定的組件。這裡的穩定性指變更的成本,如果一個組件被大量依賴,那麼這個組件就沒法頻繁變更,事實上也就變得穩定(或者說僵化)了。
比如在邏輯上,應用層相對UI是可穩定的,UI發生修改的變大大得多,但如果應用層依賴UI,那麼為了穩定,UI的修改也得非常小心謹慎。
解決的方案也是依賴反轉原則
SAP: Stable Abstractions Principle
穩定抽象原則
A component should be as abstract as it is stable.
越穩定應該越抽象,穩定意味着會被依賴,如果不抽象,那麼一旦修改,影響巨大。這個時候就可以考慮OCP,對於穩定的模塊,要關閉修改,開放擴展,而抽象保證了便於擴展。
按照component cohesion規則形成的組件,再加上組件之間的耦合、依賴關係,就形成了一個架構,接下來就討論什麼是整潔的架構。
architecture
一個好的架構需要支持一些功能
- The use cases and operation of the system.
- The maintenance of the system.
- The development of the system.
- The deployment of the system.
但很多時候,很難搞清用戶要怎麼使用系統,要怎麼運維、如何部署。而且,隨着時間推移,這一切都在變化中,說不定今天是集中式部署,明天就要服務化,後天還要上雲。如何應對這些可能的變化,同時又不過度設計,有兩條可遵循的原則:
- well-isolated components
- dependency rule
上一章節已經提到,應該讓不穩定的組件去依賴穩定的組件,那麼什麼組件穩定,什麼組件不穩定呢。
穩定的應該是業務邏輯,policy、business rule、use case。不穩定的應該是業務邏輯的周邊系統,detail、UI、db、framework
keep option open with boundary
理清楚組件之間的依賴關係,可以幫助我們推遲有關detail的決定
The longer you leave options open, the more experiments you can run, the more things you can try, and the more information you will have when you reach the point at which those decisions can no longer be deferred.
書中作者列舉了自己開發Fitnesse的例子。
項目開始之初,作者就知道需要一個持久化的功能,可能就是一個DB。
遵循依賴倒置原則,DB應該依賴於business rule,所以作者在這二者之間引入了一個interface,如下所示
上圖中紅色的boundary line其實就是兩個組件的分割,可以看到Database Interface和Business Rules在同一個組件中。通過依賴翻轉,database事實上成為了business rule的一個插件(plug-in),既然是插件,那麼就很方便替換。
在Fitnesse中,作者將這個DatabaseInterface命令為WikiPage
, 如之前所述,DB是一個detail,是不穩定組件,而且直接使用一個DB會引入許多工作量,對測試也不夠友好。於是作者在開發期用了一個MockWikiPage
,直接返回預定義數據給business rule使用;過了一年之後,業務功能不滿足mock的數據,使用了基於內存的InMemoryPage
;最終發現基於文件存儲的FileSystemWikiPage
是比MySqlWikiPage
更好的選擇。
clean architecture
回到架構這個話題上來,作者認為什麼樣的架構是整潔的呢,盡在下圖:
這是一個分層架構,從外環到內環,軟件的層級逐漸升高,也如之前所說
- high level policy
- low level detail
那麼clean architecture的dependency rule就是:外環(low level)依賴內環(high level)
Source code dependencies must point only inward, toward higher-level policies.
entity vs rule
在上圖中,出現了Entities和Use case這兩個並沒有怎麼強調的概念,二者都屬於Business rule的範疇
Entity:An Entity is an object within our computer system that embodies a small set of critical busin
比如說在一個銀行借貸系統中,Loan就是一個entity,包含一系列屬性如principle、rate以及相關操作applyInterest等等,這是業務邏輯的核心,也稱之為Critical Business Rules
Use case:A use case is a description of the way that an automated system is used
比如說貸款前的風控系統,如何做風控,跟具體實現有較大關係,因此也稱之為 application-specific business rules
不難看出,Use cases依賴於Entities, 相比而言,Entities更加穩定,所以處在環的最中間。
一個典型場景
重點在於上圖的右下角, Controller、 Presenter都是第三層的實體,依賴第二層的Use case,上圖展示了數據的流向,且沒有違背依賴關係。
下面這個Java web系統更加詳細、清楚
這個系統架構值得仔細揣摩、學習,在這裡值得注意的是:
- controller、presenter 與use case的依賴、交互關係
- use case實現Input接口,聲明output接口(Presenter實現)
- 交互使用的data structure,並沒有在各個layer之間傳遞Data對象
references
- clean architecture
- 敏捷軟件開發(Agile Software Development: Principles, Patterns, and Practices)
- 架構整潔之道導讀(一)