什麼是整潔的架構

看完了clean code -- 代碼整潔之道,那麼接下來就應該讀讀其姊妹篇:clean architecture -- 架構整潔之道。不過對我而言,代碼是實實在在的,看得見,摸得着;而架構雖然散發着光芒,但好像有點虛,彷佛認知、思考還比較少。本文主要記錄《clean architecture》的主要內容以及本身的一點思考。html

本文地址:http://www.javashuo.com/article/p-dbccraxw-dm.htmljava

架構的存在乎義

clean architecture的做者是一位從事軟件行業幾十年的架構大師,參與開發了各類不一樣類型的軟件,在職業生涯中發現了一個規律:那就是,儘管幾十年來硬件、編程語言、編程範式發生了翻天覆地的變化,但架構規則並無發生變化。python

The architecture rules are the same!mysql

我想讀過clean code以後,應該都達成了如下共識程序員

getting it work is easy
getting it right is hard
right make software easy to maintain、changeweb

上升到架構層面來講,問題一樣存在,並且更加明顯,由於架構的影響面遠大於代碼。做者舉了一個例子,展現了隨着代碼量增長、團隊人員增長、release版本增長,致使的新增代碼代價的激增以及程序員生產力的降低。
sql

從能夠看到,隨着時間的推移,每一行代碼的代價(成本)都在逐漸上升。數據庫

從另外一個角度來看
編程

單個程序員的產出隨着 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
無環依賴原則:就是在組件依賴關係圖中不該該存在環。

上圖中右下角InteractorsAuthorizerEntities三個組件之間就造成了環裝依賴。環裝依賴的問題是,環中的任何一個組件的修改都會影響到環中的任何組件,致使很難獨立開發部署。另外,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

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

相關文章
相關標籤/搜索