原文:https://herbertograca.com/2019/08/12/documenting-software-architecture/
這篇文章是軟件架構編年史(譯)的一部分,這部編年史由一系列關于軟件架構的文章組成。在這一系列文章中,我將寫下我對軟件架構的學習和思考,以及我是如何運用這些知識的。如果你閱讀了這個系列中之前的文章,本篇文章的的內(nèi)容將更有意義畔塔。
我們學習了如何編碼,我們構建了一些很酷的應用鸯屿,然后我們學習了架構以及它們?nèi)绾伪WC應用可以維護多年...
但是我們還是需要向別人(新來的開發(fā)澈吨、產(chǎn)品負責人、投資人...)解釋應用是如何工作的寄摆,我們還需要做更多...我們需要文檔谅辣。
我們有哪些可供選擇的文檔工具來表達整個應用的構建塊以及應用如何工作?婶恼!
本文我將介紹以下這些選擇:
- UML
- 4+1 架構視圖模型
- 架構決策記錄
- C4 模型
- 依賴圖
- 應用地圖
UML
我們可以用 UML 繪制各種圖桑阶,我們將這些圖分成兩大類:
- UML 行為圖(譯注,http://tool.uml.com.cn/ToolsEA/UserGuide/model_domains/behavioraldiagrams.html)
- UML 結構圖(譯注勾邦,http://tool.uml.com.cn/ToolsEA/UserGuide/model_domains/structuraldiagrams.html)
這里我不會介紹每種類型的細節(jié)蚣录,一篇文章不可能講清楚,而且這些圖的類型的介紹文檔實在是太多了检痰。要想深入了解每種類型包归,你可以點擊上面這些連接,它們會跳轉到 Visual Paradigm guides 的對應介紹铅歼,或者可以讀一下這篇博客。(譯注换可,可以在這里找到各種 UML 圖的中文介紹椎椰,http://tool.uml.com.cn/ToolsEA/UserGuide/model_domains/whatisuml.html)
總而言之成洗,UML 很酷很有意思档叔,表達力很強婆跑,我們可以簡單地描繪我們的思路痴鳄,并和同事討論橙垢。
然后背犯,用 UML 記錄一個完整的應用會用到不同類型的圖玻墅。而且装诡,如果我們只想用一張類圖來表達完整應用就是在自找麻煩吞歼。
一個適合 UML 類圖的例子是用來記錄設計模式:
來自: https://java-design-patterns.com/patterns/strategy
很好圈膏,這實際上很不錯!它可以表代類篙骡、接口稽坤、使用以及繼承關系丈甸、數(shù)據(jù)和行為。這種類圖也簡潔易讀尿褪,因為圖很小睦擂,創(chuàng)建起來也快。
然而杖玲,下面這個例子卻沒什么用...它非常大顿仇,因而令人困惑并且很難懂。而且摆马,繪制這樣一張類圖非常耗時臼闻,當我們完成繪制的時候就已經(jīng)過時了,因為在繪制的同時多半代碼已經(jīng)發(fā)生了變化今膊。
來源: https://knowhow.visual-paradigm.com
所以些阅,我們可以并且應該使用 UML,但是應該只在某些情況下使用斑唬,如:描述模式市埋,應用每一小部分的細節(jié),或者沒有太多細節(jié)的粗粒度應用試圖(用的不是類圖)恕刘。(譯注缤谎,除了使用一些重量級軟件來繪制 UML 圖之外,可以使用PlantUml 來繪制褐着。PlantUml 是一個開源工具坷澡,使用代碼來描述 UML 圖,通過工具來生成真正的圖形含蓉。)
但是問題沒有徹底解決频敛,我們怎樣記錄一個完整的應用?馅扣!
4+1 架構視圖模型
4+1 架構視圖模型由 Philippe Kruchten 創(chuàng)造斟赚,并在他 1995 年的論文 《Architectural Blueprints—The “4+1” View Model of Software Architecture》里公開發(fā)表。
這種將軟件應用架構可視化出來的方法基于五種不同的應用視圖/視角差油,告訴我們每種試圖應該是用哪種圖來記錄拗军。
-
邏輯/結構視圖
關注系統(tǒng)提供的功能以及代碼設計如何提供這些功能; -
實現(xiàn)/開發(fā)者視圖
描繪代碼的靜態(tài)組織結構蓄喇,如組件发侵、模塊和包; -
進程/行為視圖
聚焦系統(tǒng)的運行時行為妆偏,系統(tǒng)進程如何通信刃鳄,以及并發(fā)、同步楼眷、性能等等問題铲汪; -
部署/物理視圖
說明應用的物理組織結構熊尉,就是“硬件上運行的是什么代碼”; -
用例/場景視圖
利用少量幾個用例來說明完整的架構掌腰,用例就是交互的序列狰住。部分架構在這些用例上進行演進。
需要注意的是齿梁,4+1 架構視圖模型并不要求我們使用前面提到的全部這些圖催植,甚至不會用到所有的視圖。我們總是要理解工具勺择,根據(jù)我們的需要使用該用的功能创南。
架構決策記錄
架構決策記錄(ADR,Architecture Decision Record)實際上記錄的并不是應用程序架構的當前或未來狀態(tài)省核,而是關于導致這些狀態(tài)的原因稿辙。這些原因特別重要,它們的目的是告訴他人和未來的自己气忠,為什么架構是這個樣子的邻储。
ADR 就是一條架構決策的日志條目,這些已經(jīng)做出的決策導致了架構現(xiàn)在的狀態(tài)或者未來將要變成的狀態(tài)旧噪。它們包含了這些描述架構的圖背后的原因吨娜。
首先,我們需要了解一些制品:
- 架構顯著的需求(Architecturally-Significant Requirement淘钟,ASR):對軟件系統(tǒng)的架構的影響可以度量的需求宦赠;
- 架構決策(Architecture Decision,AD):解決一個重要需求的軟件設計選擇米母;
- 架構決策記錄(Architecture Decision Record 勾扭,ADR):記錄作出的架構決策以及上下文和結果的文檔;
- 架構決策日志(Architecture Decision Log 铁瞒,ADL):一個特定項目(或者組織)創(chuàng)建和維護的全部 ADR 集合尺借;
- 架構知識管理(Architecture Knowledge Management,AKM):囊括上述所有概念更高層次的范疇精拟。
我見過一些創(chuàng)建 ADR 的模板,其中一些有點東西虱歪,于是我創(chuàng)建了自己的模板蜂绎。你也可以,也許應當創(chuàng)建符合自己或團隊需要的模板笋鄙。
我認為就模板而言最重要的是簡單师枣,可以用一些文件來幫助充實并協(xié)助作出務實和公正的決定。
在討論或者決策后記錄一份文檔萧落,并不是 ADR 的最佳實踐践美。最佳實踐是在討論開始的時候就把它當做記錄思路/提案的 RFC(Request For Comments)洗贰,我們提出的這些思路/提案需要團隊/部門其它成員的輸入/觀點/批準。其目的實際上是用它來開始討論陨倡,進行頭腦風暴敛滋,做出可能的最佳決策,并將提案文檔本身作為決策日志條目(ADR)兴革。從一開始就編寫 ADR 并不意味著不可變绎晃,相反隨著討論的展開它表虛更新/改進。把所有考慮到的方案以及利弊都寫下來杂曲,我覺得這一點特別重要庶艾,這樣才能激發(fā)討論和明確的決定。
我設計的文檔如下:
https://docs.google.com/document/d/1Xe5erulKsdaha3uwU6tNWsAWxjQK-Rcrr_iJeOCXuSQ/edit?usp=sharing
你可以隨意復制這份文檔擎勘。
如果你想進一步探討這個話題咱揍,推薦Joel Parker Henderson 關于 ADR 的 github 倉庫(譯注,也可以參考 Phodal 的兩篇文章:【譯文】架構決策記錄(Architecture Decision Records)棚饵、使用 adr 輕松創(chuàng)建 “程序員友好” 的輕量級架構決策記錄)煤裙。
C4 模型
C4 模型是 Simon Brown 發(fā)明的,是我目前看到的關于軟件架構文檔的最好思路蟹地。我會快速地用自己的語言來闡述主要的思路积暖,但使用的還是他的圖例。
其思路是用四種不同粒度(或者“縮放”)層級來記錄軟件的架構:
- 第一級:系統(tǒng)上下文圖
- 第二級:容器圖
- 第三級:組件圖
- 第四級:代碼圖
第一級:系統(tǒng)上下文圖
這是最粗粒度的圖怪与。它的細節(jié)很少但其主要目標是描述應用所處的上下文夺刑。因此,這幅圖中只有一個方塊代表整個應用分别,其它圍繞著應用的方塊代表了應用要進行交付的外部系統(tǒng)和用戶遍愿。
第二級:容器圖
現(xiàn)在,我們將應用放大耘斩,也就是上一級圖中的藍色方塊沼填,在這一級它對應的是下圖中的虛線框。
在這個粒度級別括授,我們將看到應用得容器坞笙,一個容器就是一個應用中技術上獨立的一小部分,例如一個移動 App荚虚,一個 API 或者一個數(shù)據(jù)庫薛夜。它還描述了應用使用的主要技術和容器之間的通信方式。
第三級:組件圖
組件圖展示的是一個容器內(nèi)的組件版述。在 C4 模型上下文里梯澜,每個組件就是應用的一個模塊,不光是領域維度的模塊(如賬單渴析、用戶...)還包括純粹的功能模塊(如 email晚伙、sms...)吮龄。因此這個層級的圖向我們展示了一個容器的主要齒輪和齒輪之間的嚙合關系。
第四級:代碼圖
這是最細粒度的圖咆疗,目的是描述一個組件內(nèi)部的代碼結構漓帚。在這個層級,我們使用的是表示類級別制品的 UML 圖民傻。
要了解更多信息胰默,可以閱讀 Simon Brown 自己的說明:這里以及這里,或者看看他關于 C4 模型的演講漓踢。(譯注牵署,可以參考我的同事仝鍵的文章“可視化架構設計——C4 介紹”。)(譯注喧半,前面提到的 PlantUml 可以自己定義擴展圖形奴迅,C4 模型上述的前三種圖已經(jīng)有了開源的擴展 https://github.com/RicardoNiepel/C4-PlantUML,使用非常簡單高效挺据。第四種代碼圖本來就是 PlantUML 支持的 UML 類圖)取具。
還有什么問題?扁耐!
我認為 C4 模型是一種非常好的記錄應用架構的方式暇检,它可以幫我們很好地理解應用在某個層次上的架構。但我覺得還是不夠婉称,盡管搞清楚這個問題花了不少時間块仆。
這些圖有三處限制:
- 除了一些例外情況,比如 Simon Brown 的structurizr王暗,這些圖需要人工繪制悔据,而不能自動生成,也不能直接從代碼中生成俗壹,這意味著它們體現(xiàn)的可能并不是實際代碼科汗,而是我們目前對代碼的理解;
- 這些圖并不能幫助我們發(fā)現(xiàn)應用程序代碼庫中存在的問題绷雏,比如混亂的代碼關系和糟糕的結構头滔,這些問題影響了模塊化和封裝,這對任何工程產(chǎn)品都是至關重要的涎显;
- 這些圖不能幫助我們從整體上理解代碼庫:應用程序的齒輪可以做什么拙毫,它們之間如何交互。
我找到了另外兩類圖來幫我們解決這些問題棺禾。
依賴圖
依賴圖可以有效地告知我們代碼庫中不同類型代碼之間存在的依賴。
有一點特別重要峭跳,這些圖是直接從代碼自動生成的膘婶,不然這種圖只能反映我們認為的代碼的樣子缺前,如果理解真的絲毫不差,我們也就不需要這種類型的文檔了悬襟。
此外衅码,我們能借助這種依賴分析能力,當預設的依賴關系規(guī)則被破壞的時候停止構建脊岳,這一點可能比這些圖本身更為重要逝段。用來生成依賴圖的工具也應該可以當做測試工具使用,包括在 CI 流水線上使用割捅,就像單元測試那樣奶躯,組織不必要的依賴性進入生產(chǎn)環(huán)境,這維護并強化了模塊化亿驾,反過來也能幫助實現(xiàn)較高的變更頻率嘹黔,進而適應特性開發(fā)的快節(jié)奏。
對這種圖來說莫瞬,我覺得有三種不同類別對不同的依賴類別進行斷言儡蔓。
下面的這些例子都是用我的個人項目(explicit-architec-php)通過 deptrac生成的,這是我的一個實驗項目疼邀。你可以在倉庫根目錄下中找到生成配置喂江。
不過有一點請注意,我自己添加了一些顏色旁振,讓這些圖例在這篇文章里更容易讀懂获询。不同的顏色代表了應用的不同層級,和我之前文章中配圖的層級一致:
層級依賴圖
這種圖的目的是可視化规求,并且確保每個層級的代碼只能依賴內(nèi)部或下面的層級筐付。
因此,我們可以看到下面的圖例重阻肿,例如瓦戚,最外面層級之一的基礎設施層可以依賴其他任何層。反過來丛塌,最中心的領域?qū)咏辖猓荒芤蕾囅旅娴膶訉樱?SharedKernel-Domain(它也是領域的一部分)和 PhpExtension(其代碼被當成語言自身的一部分使用)赴邻。
用deptrac 生成的 https://github.com/hgraca/explicit-architecture-php 的層級依賴圖
類依賴圖
層級依賴圖分析的是層級之間的依賴印衔,但在層級之還有一些依賴關系不允許出現(xiàn)。
類依賴圖則有助于分析代碼倉庫中不同類型的類之間的依賴關系姥敛,尤其是那些屬于同一層級的類的依賴關系奸焙。
例如,如果我們希望事件可以被序列化以便于放到隊列中,我們可能希望它們不要包含實體与帆,因為使用 ORM 來對實體進行反序列化和持久化可能會出問題了赌。事件依賴于服務也是沒有意義的。使用這種類型的依賴圖玄糟,或者更準確地說勿她,使用這種測試依賴關系的工具,我們可以很容易地檢測到這些問題阵翎,防止它們進入生產(chǎn)環(huán)境逢并。
用deptrac 生成的 https://github.com/hgraca/explicit-architecture-php 的類依賴圖
組件依賴圖
組件是領域維度的模塊,這樣的模塊既包含應用層也包含領域?qū)庸馈R粋€組件(例如“賬單”)可以包含全部有關的用例和領域邏輯砍聊。
組件可以和 DDD 的限界上下文與/或微服務對應起來,這意味著它們必須完全從物理上和時間上解耦箱沦。如果我們的單體應用包含的是完全解耦的組件辩恼,(在代碼維度)它就能很容易的轉換成微服務架構。
而且谓形,如果能在其它非領域維度的模塊上也應用同樣的解耦要求灶伊,我們就能保證任何模塊都可以輕松進行替換。
組件依賴圖的木不礙事保證應用組件和模塊之間的解耦寒跳。
注意聘萨,在下面這個圖例中,同一個層級中的模塊(顏色相同的節(jié)點)是互相不知道對方的存在的童太,至少直接的依賴是不存在的米辐。
尤其重要的一點是兩個組件(User 和 Blog,圖中央較深藍色的兩個橢圓)是解耦的书释。如果應用采用了微服務架構翘贮,這兩個組件應該是微服務。
用deptrac 生成的 https://github.com/hgraca/explicit-architecture-php 的組件依賴圖
應用地圖
大約一年前爆惧,我意識到這些文檔選項還有一些遺漏的地方:所有這些圖只能告訴我們組成應用的是哪些構建塊狸页,哪些構建塊之間需要交互以及它們之間的關聯(lián),但是這些圖不能告訴我們 構建塊的作用是什么扯再,也不能告訴我們 它們之間是如何交互的芍耘,以及是 何時交付的。這要求我們既要從用戶視角理解應用熄阻,也要從開發(fā)者視角理解代碼庫斋竞。前面這些圖無法告訴我們應用中有哪些用例,也無法告訴我們事件是那些用例觸發(fā)的秃殉,也無法告訴我們這些事件的后果是什么坝初。如果我們把這些圖拿給產(chǎn)品負責人看浸剩,他會覺得大部分圖對他沒有任何幫助。
所以我想到了一種信的文檔圖脖卖,我把它稱為應用地圖乒省,它可以替代 C4 模型組件圖。
應用地圖的目標就是成為名副其實的應用地圖畦木,定義出其中的“城市”(組件)、“當?shù)氐牡缆贰保ㄓ美┰曳骸ⅰ案咚俟贰保ㄊ录┑鹊取?/p>
組件和模塊之間的區(qū)別是十籍,模塊是應用程序的任何模塊化片段,而組件是應用程序的領域維度的模塊唇礁。因此勾栗,雖然 ORM 是應用程序的模塊,但它不是組件盏筐,因為它只關心技術围俘。另一方面,“賬單”模塊是一個組件琢融,因為它關心的是領域界牡。
應用地圖首先要定義出應用的組件,即領域維度的模塊漾抬,如“賬單”宿亡、“用戶”、“公司”纳令、“訂單”挽荠、“產(chǎn)品”等。對一個簡單的博客應用來說平绩,我們可以劃出兩個組件圈匆,“用戶”(User)和“博客”(Blog):
在每個組件內(nèi)部,我們定義出可以發(fā)給它們的命令捏雌≡咀“用戶”組件可以創(chuàng)建和刪除用戶,而“博客”組件可以創(chuàng)建和刪除帖子腹忽,還可以創(chuàng)建帖子的評論来累。
接下來,我們列出每個組件相關的全部服務窘奏。這些服務之所以和組件相關嘹锁,是因為它們要么觸發(fā)一個事件,要么被另一個組件直接使用着裹。這一點很重要领猾,因為應用地圖應該把組件之間的連接及其含義和導致的任何副作用可視化出來,因此,我們需要展現(xiàn)將組件和其他組件串聯(lián)起來的服務及其名稱(服務命名應該代表它們的功能)摔竿。
服務列完之后面粮,每個組件還要列出所有的事件監(jiān)聽器,哪怕實際上這些監(jiān)聽器沒有被用過继低,這樣我們就可以檢測到并按需修復或者刪除未使用的代碼熬苍,這一點很方便。
我所說的監(jiān)聽器就是一個類袁翁,其公共方法由唯一一種的事件類型觸發(fā)柴底,它們關注的是某一個事件。
我們還要列出組件的事件訂閱者粱胜,這和列出監(jiān)聽器的目的一樣柄驻。事件訂閱者和事件監(jiān)聽器類似,但它的公共方法由不同的事件觸發(fā)焙压,它們專注于一個復合的任務,一個類監(jiān)聽不同的框架事件涯曲,以控制何時開始野哭、提交或回滾事務請求,就是一個事件訂閱者的例子掀抹。
現(xiàn)在虐拓,我們已經(jīng)在地圖中列出了所有的組件及其功能。這張地圖非常有價值傲武,它能告訴我們或任何非技術人員蓉驹,每個組件可以做什么。
但是揪利,它仍然沒有告訴我們所有這些功能是之間是如何關聯(lián)的态兴,比如如“用戶創(chuàng)建博客文章會產(chǎn)生什么結果?”
要體現(xiàn)出這些關聯(lián)疟位,第一步是列出特定功能被觸發(fā)時組件上會發(fā)生什么瞻润。
在下面的這幅圖例中,我們可以看到刪除帖子(“DeletePost”)將觸發(fā) PostService 的DeletePost() 方法甜刻,此外绍撞,當監(jiān)聽器監(jiān)聽到了用戶已被刪除的通知事件時,也會觸發(fā)這個方法得院。這告訴我們傻铣,我們的應用刪除帖子可以是用戶直接命令導致的結果,也可以是帖子作者被刪除導致的結果祥绞。
我們可以看到用戶組件在創(chuàng)建帖子時非洲,其作者自動訂閱了帖子的主題(標簽)鸭限。
現(xiàn)在,我們有了組件內(nèi)部的流的相關信息两踏,但是我們?nèi)匀蝗鄙倏缃M件的流的相關信息败京,因此我們要加上被觸發(fā)和被監(jiān)聽的事件:
例如,我們可以看到:
- 刪除用戶將觸發(fā)將刪除用戶帖子的事件梦染;
- 創(chuàng)建帖子將同時觸發(fā)作者訂閱帖子主題的時間和提高作者等級的事件赡麦;
- 任何用例只要刪除帖子都會觸發(fā)降低作者等級的事件。
有了地圖上的這些信息帕识,我們就可以導航了隧甚。任何技術人員或非技術人員都可以清楚地看到,任何應用用例觸發(fā)時會發(fā)生什么渡冻。這可以幫助我澄清我們的代碼,和對應用行為的想法忧便。
(譯注族吻,如果熟悉事件風暴,我們可以發(fā)現(xiàn)珠增,上述這些組件圖和依賴非常想我們在事件風暴過程中不斷梳理出一些領域概念超歌,包括事件、命令蒂教、聚合巍举、API,慢慢浮現(xiàn)出來的限界上下文地圖/服務地圖凝垛。這個工具可以幫我們將事件風暴的結成果進行電子化)懊悯。
然而,這種圖用在大型應用中時仍然或存在與前面提到的關系圖相同的問題:
- 這份圖需要花費大量的精力和時間來完成梦皮,而維護其簡單的時效性也要大量投入;
- 最后這幅圖也會變得很大炭分,上面有很多文本,可讀性也不是最好的剑肯。
要解決第一個問題捧毛,我們需要能夠隨時從代碼中生成應用地圖。這樣創(chuàng)建應用地圖不費吹灰之力让网,維護也不需要什么成本呀忧,想要創(chuàng)建的時候可以立即創(chuàng)建。
要解決第二個問題溃睹,我們需要能夠選擇性的生成部分應用地圖而账。例如,提供我們想要分析的用例的名稱丸凭,只生成與給定用例相關的部分地圖福扬。
所以腕铸,我們需要一個工具,但這個工具...現(xiàn)在...還不存在铛碑!
真的不存在嗎狠裹?!
最近我開始創(chuàng)造這個工具汽烦,目前只有組件內(nèi)部的流這部分功能還沒完成涛菠,但是已經(jīng)可以列出所有命令、服務撇吞、監(jiān)聽器俗冻、訂閱者和事件。這個工具仍然還處于非常早期的階段牍颈,不光是因為缺少組件內(nèi)部的這些信息迄薄,還因為它分析的代碼庫還不夠靈活,但我目前工作的公司的代碼庫可以用它生成這樣的圖:
https://gitlab.com/hgraca/app-mapper生成的(不完整的)應用地圖示例
如果你對這個項目感興趣煮岁,可以在這里找到它讥蔽。不過要請注意,這個仍然是非常初級画机,只是驗證了我的想法冶伞,而且我已經(jīng)有幾個月沒做這個了。如果你覺得有價值步氏,也有時間可以貢獻响禽,請聯(lián)系我,我會全力支持你幫你創(chuàng)建任務荚醒,讓你把這個工具提升到更高的水平芋类。