天天看點

什麼是整潔的架構

看完了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: 緊急,但不總是特别重要

architecture:重要,但從來不緊急

了解過時間管理或者目标管理的話,都知道重要但不緊急的事情反而是需要我們特别花時間去處理的。

而架構設計就是讓我們在支撐功能的同時,保證系統的可維護性、可擴充性。

軟體開發和修房子一樣,在實施角度來看都是從low-level到high-level的過程,比如房子是由磚塊(brick)到房間(room),再由房間到房子(house)。作者的類比如下

software

building

programming paradigms

brick

module rule(solid)

room

component rule

house

在我看來,clean code中強調的變量名、函數、排版更像是軟體開發中最基礎的機關,不同的programming paradigms遵循的思想是不同的,但代碼品質(整潔代碼)是獨立于程式設計語言的。

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):依賴導緻原則,依賴接口而不是實作(高層不需要知道底層的實作)

module級别的SRP很容易和函數的單一職責相混淆。函數的單一職責是一個函數隻做一件事 -- 這件事通過函數名就可以看出來。而SRP則是指一個module僅僅對一個利益相關者(actor)負責,隻有這個利益相關者有理由修改這個module。

違背SRP,會導緻不相關的邏輯的意外耦合,如下面這個例子

什麼是整潔的架構

Employee這個類裡面包含了太多的功能:

<code>save</code>是給CTO調用

<code>CalculatePay</code>是給CFO使用

而COO則關心<code>reportHours</code>。

問題在于,<code>CalculatePay</code>也依賴<code>ReportHours</code>,如果CFO因為某些原因修改了<code>ReportHours</code>,那麼就會影響到COO。

這個例子也表明,一個類是對什麼東西的抽象并不是最重要的,而在于誰使用這個類,如何使用這個類。

解決方法之一是使用Facade模式,如下所示

什麼是整潔的架構

Facade模式保證了對外暴露同樣的三個接口,但其職責都委托給了三個獨立的module,互不影響。

對于繼承而言,子類的執行個體理論上是滿足基類的所有限制的,比如Bird extend Animal,那麼Animal的所有行為bird都應該滿足。

但上面也描述過,類的有效性取決于類的使用方式,并不能用人類的認識去判斷。比如正方形是否應該繼承自長方形(square is a rectangle?),按照正常人的認知來說肯定是的,但對于某些使用方式就會存在問題, 比如下面這個函數

上述的代碼表明,<code>g</code>函數的編寫者認為存在一種限制:修改rectangle的長不會影響寬。但 這個對于squre是不成立的,是以square違背了某種(隐式的)契約,這個契約是關于如何使用rectangle這個類的。

如何傳達這個契約呢,有兩種方式,第一是單元測試;第二是DBC(design by contract)。

詳見讨論: 你會怎樣設計長方形類和正方形類?

接口隔離原則解決的是“胖”接口問題,如下圖所示:

什麼是整潔的架構

<code>OPS</code>所提供的三個接口是給三個不同的actor使用的,但與SRP要解決的問題不同,在這裡并不存在因公用代碼導緻的耦合。真正的問題是 Use1對<code>op1</code>的使用導緻OPS的修改,導緻User2 User3也要重新編譯。

解決方法是引入中間層,如下所示

什麼是整潔的架構

當然,靜态語言之間的源碼依賴才會導緻 recompilation and redeployment; 而對于動态語言(如python)則不會有這個問題。

不過,不要依賴你不需要的東西,這個原則總是好的。

DIP(Dependency inversion principle)是架構設計中處理依賴關系的核心原則,其反轉的是依賴關系。比如一個應用可能會使用到資料庫,那麼很自然的寫法就是

什麼是整潔的架構

Business rule依賴Database的問題在于,database的選擇是一個細節問題,是易變的,今天是mysql,明天就可能會換成Nosql,這就導緻Business rule也會收到影響。是以需要依賴反轉,就是讓database去依賴Business rule

什麼是整潔的架構

Business rule依賴抽象接口,而database實作了這個抽象接口,接口一般是穩定的,是以即使替換DB的實作,也不會影響到Business rule。

這也提供了某種暗示:對于java C++等靜态類型語言,import include應該隻refer to 接口、抽象類,而不是concrete class。

OCP是下面兩個短語的縮寫

open for exrension: 當應用的需求變更時,我們可以對子產品進行擴充,使其滿足新需求

close for mofifacation: 對子產品進行擴充時,無需改動子產品的源代碼或者二進制檔案

很容易想到,有兩種常見的設計模式能實作這樣的效果,就是Strategy與Template Method。

什麼是整潔的架構

要實作OCP,至少依賴于SRP與DIP,前者保證因為不同原因修改的邏輯不會耦合在一起,後者則保證是邏輯上的被使用者依賴使用者,從Strategy模式的實作也可以看出。

其實我覺得OCP應該是比其他幾個module rule抽象層級更高的原則,甚至高于後面會提到的component rule,軟體要可維護性、可擴充性強,那麼就最好不要去修改(影響)已有的功能,而是添加(擴充)出新的功能。這是不證自明的。

什麼是component呢,component是獨立開發、獨立部署的基本單元,比如一個.jar、.dll,或者python的一個wheel或者egg。

component rule主要解決兩個問題,第一是哪些module可以形成一個component,即component cohesion,元件的内聚問題;另一個則是不同的component之間如何協作的問題,即component coupling

哪些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更小一些。

元件之間要互相協作才能産生作用,協作就會導緻依賴。

比如元件A使用到元件B(元件A中的某個類使用到了元件B中的某個類),那麼元件A就依賴于元件B。在這樣的依賴關系裡面,被依賴者(元件B)的變更會影響到依賴者(元件A),在Java,C++這樣的靜态類型語言裡面,就展現為元件A需要重制編譯、釋出、部署。

架構設計的一個重要原則,就是減少由于元件之間的依賴導緻的rebuild、redeploy,這樣才能減低開發、維護成本,最大化程式員的生産力。

ADP: Acyclic Dependencies Principle

無環依賴原則:就是在元件依賴關系圖中不應該存在環。

什麼是整潔的架構

上圖中右下角<code>Interactors</code>,<code>Authorizer</code>,<code>Entities</code>三個元件之間就形成了環裝依賴。環裝依賴的問題是,環中的任何一個元件的修改都會影響到環中的任何元件,導緻很難獨立開發部署。另外,<code>Database</code>元件本身是依賴<code>Entities</code>的,現在<code>Entities</code>在一個環中,那就相當于<code>Database</code>依賴整個環。也就是說,對外而言一個環中的所有元件事實上形成了一個更大的元件。

如何解環呢?

一種方法是使用依賴倒置原則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規則形成的元件,再加上元件之間的耦合、依賴關系,就形成了一個架構,接下來就讨論什麼是整潔的架構。

一個好的架構需要支援一些功能

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

理清楚元件之間的依賴關系,可以幫助我們推遲有關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指令為<code>WikiPage</code>, 如之前所述,DB是一個detail,是不穩定元件,而且直接使用一個DB會引入許多工作量,對測試也不夠友好。于是作者在開發期用了一個<code>MockWikiPage</code>,直接傳回預定義資料給business rule使用;過了一年之後,業務功能不滿足mock的資料,使用了基于記憶體的<code>InMemoryPage</code>;最終發現基于檔案存儲的<code>FileSystemWikiPage</code>是比<code>MySqlWikiPage</code>更好的選擇。

回到架構這個話題上來,作者認為什麼樣的架構是整潔的呢,盡在下圖:

什麼是整潔的架構

這是一個分層架構,從外環到内環,軟體的層級逐漸升高,也如之前所說

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.

在上圖中,出現了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對象

clean architecture

靈活軟體開發(Agile Software Development: Principles, Patterns, and Practices)

架構整潔之道導讀(一)

本文版權歸作者xybaby(博文位址:http://www.cnblogs.com/xybaby/)所有,歡迎轉載和商用,請在文章頁面明顯位置給出原文連結并保留此段聲明,否則保留追究法律責任的權利,其他事項,可留言咨詢。