從三明治到六邊形
軟件項目的套路
如果你平時的工作是做各種項目(而不是產品),而且你工作的時間足夠長,那么自然見識過很多不同類型的項目。在切換過多次上下文之后,作為程序員的你,自然而然的會感到一定程度的重復:稍加抽象,你會發現所有的業務系統都幾乎做著同樣的事情:
- 從某種渠道與用戶交互,從而接受輸入(Native App,Mobile Site,Web Site,桌面應用等等)
- 將用戶輸入的數據按照一定規則進行轉換,然后保存起來(通常是關系型數據庫)
- 將業務數據以某種形式展現(列表,卡片,地圖上的Marker,時間線等)
稍加簡化,你會發現大部分業務系統其實就是對某種形式的資源進行管理。所謂管理,也無非是增刪查改(CRUD)操作。比如知乎是對“問題”這種資源的管理,LinkedIn是對“Profile”的管理,Jenkins對構建任務的管理等等,粗略的看起來都是這一個套路(當然,每個系統管理的資源類型可能不止一種,比如知乎還有時間線,Live,動態等等資源的管理)。
這些情況甚至會給開發者一種錯覺:世界上所有的信息管理系統都是一樣的,不同的僅僅是技術棧和操作的業務對象而已。如果寫好一個模板,幾乎都可以將開發過程自動化起來。事實上,有一些工具已經支持通過配置文件(比如yaml或者json/XML)的描述來生成對應的代碼的功能。
如果真是這樣的話,軟件開發就簡單多了,只需要知道客戶業務的資源,然后寫寫配置文件,最后執行了一個命令來生成應用程序就好了。不過如果你和我一樣生活在現實世界的話,還是趁早放棄這種完全自動化的想法吧。
復雜的業務
現實世界的軟件開發是復雜的,復雜性并不體現在具體的技術棧上。如Java,Spring,Docker,MySQL等等具體的技術是可以學習很快就熟練掌握的。軟件真正復雜的部分,往往是業務本身,比如航空公司的超售策略,在超售之后Remove乘客的策略等;比如亞馬遜的打折策略,物流策略等。
用軟件模型如何優雅而合理的反應復雜的業務(以便在未來業務發生變化時可以更快速,更低錯誤的作出響應)本身也是復雜的。要將復雜的業務規則轉換成軟件模型是軟件活動中非常重要的一環,也是信息傳遞往往會失真的一環。業務人員說的A可能被軟件開發者理解成Z,反過來也一樣。
舉個例子,我給租來的房子買了1年的聯通寬帶。可是不多過了6個月后,房東想要賣房子把我趕了出來,在搬家之后,我需要通知聯通公司幫我做移機服務。
如果純粹從開發者的角度出發,寫出來的代碼可能看起來是這樣的:
public class Customer {
private String address;
public void setAddress(String address) {
this.address = address;
}
public String getAddress() {
return this.address;
}
}
</code></pre>
中規中矩,一個簡單的值對象。作為對比,通過與領域專家的交流之后,寫出來的代碼會是這樣:
public class Customer {
private String address;
public void movingHome(String address) {
this.address = address;
}
}
</code></pre>
通過引入業務場景中的概念movingHome,代碼就變得有了業務含義,除了可讀性變強之外,這樣的代碼也便于和領域專家進行交流和討論。Eric在領域驅動設計(Domain Drvien Design)中將統一語言視為實施DDD的先決條件。
層次架構(三明治)
All problems in computer science can be solved by another level of indirection, except of course for the problem of too many indirections. – David Wheeler
上文提到,業務系統對外的呈現是對某種資源的管理,而且,現實世界里的業務系統往往要對多種資源進行管理。這些資源還會互相引用,互相交織。比如一個看板系統中的泳道、價值流、卡片等;LinkedIn中的公司,學校,個人,研究機構,項目,項目成員等,它們往往會有嵌套、依賴等關系。
為了管理龐大的資源種類和繁復的引用關系,人們自然而然的將做同樣事情的代碼放在了統一的地方。將不同職責的事物分類是人類在處理復雜問題時自然使用的一種方式,將復雜的、龐大的問題分解、降級成可以解決的問題,然后分而治之。
比如在實踐中 ,展現部分的代碼只負責將數據渲染出來,應用部分的代碼只負責序列化/反序列化、組織并協調對業務服務的調用,數據訪問層則負責屏蔽底層關系型數據庫的差異,為上層提供數據。這就是層級架構的由來:上層的代碼直接依賴于臨近的下層,一般不對間接的下層產生依賴,層次之間通過精心設計的API來通信(依賴通常也是單向的)。
以現代的眼光來看,層次架構的出現似乎理所應當、自然而然,其實它也是經過了很多次的演進而來的。以JavaEE世界為例,早期人們會把應用程序中負責請求處理、文件IO、業務邏輯、結果生成都放在servlet中;后來發明了可以被Web容器翻譯成servlet的JSP,這樣數據和展現可以得到比較好的分離(當然中間還有一些迂回,比如JSTL、taglib的濫用又導致很多邏輯被泄露到了展現層);數據存儲則從JDBC演化到了各種ORM框架,最后再到JPA的大一統。
如果現在把一個Spring-Boot寫的RESTful后端,和SSH(Spring-Struts-Hibernate)流行的年代的后端來做對比,除了代碼量上會少很多以外,層次結構上基本上并無太大區別。不過當年在SSH中復雜的配置,比如大量的XML變成了代碼中的注解,容器被內置到應用中,一些配置演變成了慣例,大致來看,應用的層次基本還是保留了:
- 展現層
- 應用層
- 數據訪問層
在有些場景下,應用層內還可能劃分出一個服務層。

前后端分離
隨著智能設備的大爆發,移動端變成了展現層的主力,如何讓應用程序很容易的適配新的展現層變成了新的挑戰。這個新的挑戰驅動出了前后端分離方式,即后端只提供數據(JSON或者XML),前端應用來展現這些數據。甚至很多時候,前端會成為一個獨立的應用程序,有自己的MVC/MVP,只需要有一個HTTP后端就可以獨立工作。

前后端分離可以很好的解決多端消費者的問題,后端應用現在不區分前端的消費者到底是誰,它既可以是通過4G網絡連接的iOS上的Native App,也可以是iMac桌面上的Chrome瀏覽器,還可以是Android上的獵豹瀏覽器。甚至它還可以是另一個后臺的應用程序:總之,只要可以消費HTTP協議的文本就可以了!
這不得不說是一個非常大的進步,一旦后端應用基本穩定,頻繁改變的用戶界面不會影響后端的發布計劃,手機用戶的體驗改進也與后端的API設計沒有任何關系,似乎一切都變的美好起來了。
業務與基礎設施分離
不過,如果有一個消費者(一個業務系統),它根本不使用HTTP協議怎么辦?比如使用消息隊列,或者自定義的Socket協議來進行通信,應用程序如何處理這種場景?
這種情況就好比你看到了這樣一個函數:
httpService(request, response);
作為程序員,自然會做一次抽象,將協議作為參數傳入:
service(request, response, protocol);
更進一步,protocol可以在service之外構造,并注入到應用中,這樣代碼就可以適配很多種協議(比如消息隊列,或者其他自定義的Socket協議)。
比如:
public interface Protocol {
void transform(Request request, Response response);
}
public class HTTP implements Protocol {
}
public class MyProtocol implements Protocol {
}
public class Service {
public Service(Protocol protocol) {
this.protocol = protocol;
}
public void service(request, response) {
//business logic here
protocol.transfrom(request, response);
}
}
</code></pre>
類似的,對于數據的持久化,也可以使用同樣的原則。對于代碼中諸如這樣的代碼:
persisteToDatabase(data);
在修改之后會變成:
persistenceTo(data, repository);
應用依賴倒置原則,我們會寫出這樣的形式:
public class DomainService {
public BusinessLogic(Repository repository) {
this.repository = repository
}
public void perform() {
//perform business logic
repository.save(record);
}
}
</code></pre>
對于Repository可能會有多種實現。根據不同的需求,我們可以自由的在各種實現中切換:
public class InMemoryRepository implements Repository {}
public class RDBMSRepository implements Repository {}
這樣業務邏輯和外圍的傳輸協議、持久化機制、安全、審計等等都隔離開來了,應用程序不再依賴具體的傳輸細節,持久化細節,這些具體的實現細節反過來會依賴于應用程序。
通過將傳統內置在層次架構中的數據庫訪問層、通信機制等部分的剝離,應用程序可以簡單的分為內部和外部兩大部分。內部是業務的核心,也就是 DDD (Domain Driven Design)中強調的領域模型(其中包含領域服務,對業務概念的建立的模型等);外部則是類似RESTful API,SOAP, AMQP ,或者數據庫,內存,文件系統,以及自動化測試。
這種架構風格被稱為六邊形架構,也叫端口適配器架構。
六邊形架構(端口適配器)
六邊形架構最早由 Alistair Cockburn 提出。在DDD社區得到了發展和推廣,然后IDDD(《實現領域驅動設計》)一書中,作者進行了比較深入的討論。

圖片來源: slideshare
簡而言之,在六邊形架構風格中,應用程序的內部(中間的橙色六邊形)包含業務規則,基于業務規則的計算,領域對象,領域事件等。這部分是企業應用的核心:比如在線商店里什么樣的商品可以打折,對那種類型的用戶進行80%的折扣;取消一個正在執行的流水線會需要發生什么動作,刪除一個已經被別的Job依賴的Stage又應該如何處理。
而外部的,也是我們平時最熟悉的諸如REST,SOAP,NoSQL,SQL,Message Queue等,都通過一個端口接入,然后在內外之間有一個適配器組成的層,它負責將不同端口來的數據進行轉換,翻譯成領域內部可以識別的概念(領域對象,領域事件等)。
內部不關心數據從何而來,不關心數據如何存儲,不關心輸出時JSON還是XML,事實上它對調用者一無所知,它可以處理的數據已經是經過適配器轉換過的領域對象了。
六邊形架構的優點
- 業務領域的邊界更加清晰
- 更好的可擴展性
- 對測試的友好支持
- 更容易實施DDD
要新添加一種數據庫的支持,或者需要將RESTful的應用擴展為支持SOAP,我們只需要定義一組端口-適配器即可,對于業務邏輯部分無需觸碰,而且對既有的端口-適配器也不會有影響。
由于業務之外的一切都屬于外圍,所以應用程序是真的跑在了Web容器中還是一個Java進程中其實是無所謂的,這時候自動化測試會容易很多,因為測試的重點:業務邏輯和復雜的計算都是簡單對象,也無需容器,數據庫之類的環境問題,單元級別的測試就可以覆蓋大部分的業務場景。
這種架構模式甚至可能影響到團隊的組成,對業務有深入理解的業務專家和技術專家一起來完成核心業務領域的建模及編碼,而外圍的則可以交給新人或者干脆外包出去。
在很多情況下,從開發者的角度進行的假設都會在事后被證明是錯誤的。人們在預測軟件未來演進方向時往往會做很多錯誤的決定。比如對關系型數據庫的選用,對前端框架的選用,對中間件的選用等等,六邊形架構可以很好的幫助我們避免這一點。
小結
軟件的核心復雜度在于業務本身,我們需要對業務本身非常熟悉才可能正確的為業務建模。通過統一的語言我們可以編寫出表意而且易于和業務人員交流的模型。
另一方面模型應該盡可能的和基礎設施(比如JSON/XML的,數據庫存儲,通信機制等)分離開。這樣一來可以很容易用mock的方式來解耦模型和基礎設施,從而更容易測試和修改,二來我們的領域模型也更獨立,更精簡,在適應新的需求時修改也會更容易。
這里有一段 很微小的代碼 ,有興趣的同學可以看看。
來自:http://icodeit.org/2017/08/from-sandwich-to-hexagon/