原文:https://herbertograca.com/2018/07/07/more-than-concentric-layers/
這篇文章是軟件架構編年史(譯)的一部分,這部編年史由一系列關于軟件架構的文章組成偎箫。在這一系列文章中溉苛,我將寫下我對軟件架構的學習和思考,以及我是如何運用這些知識的娇唯。如果你閱讀了這個系列中之前的文章寂玲,本篇文章的的內容將更有意義。
在我之前的這篇系列文章(譯)中拓哟,我發(fā)表了一幅信息圖,展示了用來理解代碼單元類型之間聯(lián)系的心智地圖流纹。
然而漱凝,我始終覺得有一部分內容并沒有得到很好的體現(xiàn),但我不知道應該如何更好地將它展示出來茸炒。這個部分就是共享內核壁公。
而且,我還發(fā)現(xiàn)了一些新內容紊册,在這篇博客中我將記錄下所有的內容湿硝。
觀察上一篇博客中發(fā)表的信息圖润努,我們發(fā)現(xiàn)共享內核位于圖的中心,看起來好像在領域層內部痢畜,疊在代表領域上下文的同心圓之上鳍侣。
盡管將共享內核放在這個位置倚聚,但我要表達的并不是共享內核會依賴其余部分的代碼,它也不是領域層內部的另一個層次惑折。
什么是共享內核?!
共享內核由 DDD 之父 Eric Evans 定義白热,它是多個限界上下文之間共享的代碼粗卜,由開發(fā)團隊決定:
[...] 兩個團隊同意共享的領域模型的子集。當然攻臀,和模型子集一起共享還包括代碼的子集,還有和這部分模型有關的數(shù)據(jù)庫設計百匆。這部分明確要共享的內容有著特殊的狀態(tài)呜投,而且在沒有和其他團隊達成一致的情況下不應該修改。
Shared Kernel, Ward Cunningham 的 DDD wiki
所以基本上雕拼,它可能是任何類型的代碼:領域層代碼粘招、應用層代碼、庫辑甜,隨便什么代碼袍冷。
然而胡诗,在我的這份心智地圖里,我將它當做一些特定類型的代碼的子集骇陈。在我的心智地圖里瑰抵,共享內核包含的是領域層和應用層的代碼,這些代碼會在限界上下文之間共享婿崭,讓這些上下文可以互相通信习贫。
這意味著,例如颤绕,一個或多個限界上下文觸發(fā)的事件可以在其它的限界上下文里被監(jiān)聽到。需要和這些事件一起共享的還有它們用到的所有數(shù)據(jù)類型物独,例如:實體 ID氯葬、值對象、枚舉官研,等等闯睹。事件不應該直接使用像實體這樣的復雜對象,因為將它們序列化到隊列中或是從隊列中反序列化時都會遇到一些問題始花,所以共享的代碼不應該太寬泛孩锡。
當然,如果我們手中的是一個由不同語言開發(fā)的微服務組成的多語言系統(tǒng)浇垦,共享內核必須是描述性的語言斩披,格式是 json、xml、yaml 或者其它厕倍,這樣所有的微服務都能理解贩疙。
因此,共享內核就完全和其余的代碼以及組件完全解耦了组民。這樣很好悲靴,因為這意味著盡管組件耦合了共享內核,但組件之間不再耦合耸三。共享代碼可以被清晰地識別出來,并輕松地提取到一個獨立的庫中憨颠。
如果我們決定將一個限界上下文從單體中分離出來并提取成一個微服務积锅,這也會很方便。我對共享代碼了然于心淫茵,可以輕松地將共享內核提取到一個庫中蹬跃。而這個庫即可以安裝到單體中蝶缀,也可以安裝到微服務中。
所以回顧一下翁都,在我的心智地圖中柄慰,應用核心依賴共享內核,共享內核包括了在限界上下文之間共享的領域層和應用層代碼藏研。
當語言不夠用時…
這樣概行,我們得到了包含同心圓層次的應用代碼,而應用核心依賴著支撐所有這些代碼的共享內核业踏。
我們還可以說所有代碼都依賴它們所使用的編程語言涧卵,但我們對這樣顯而易見的事實熟視無睹。
然而我要拋出這個事實伐脖,原因是還有一個問題“當語言結構不夠用時我們該怎么辦晓殊?认烁!”。顯然我們會自己創(chuàng)造語言結構嘹承,來填補這些語言的瑕疵撼港。隨后我會提出下一個重要的問題“我們如何傳遞這些代碼之所以存在地背后原理?我們把它放在哪里?我們如何清晰地表達使用它的時機懒震?使用方法个扰?”
我見過做法的是,我自己也是這樣做的,將代碼放在一個名為 Utils 或 Commons 的包里土榴。但它最終會變成一個筐子赫段,當我們不知道代碼該放在哪兒時,就會一股腦兒全扔進這個筐子里!不同類型的代碼额获,不同用途的代碼抄邀,還有不同用法(包裝在適配器里使用,或是直接使用...)的代碼最后都被扔在這里奥喻,這個包沒有概念性的含義读宙,沒有一致性,沒有內聚性,一點也不明確蔫耽,到處充斥著歧義。
我想拋棄 Utils 和 Commons 包黑毅!
每一個包都必須在概念上內聚钦讳!何時使用包以及如何使用包必須是明確的潮秘!不能有任何歧義!
因此买猖,舉個例子玉控,如果我們的應用有一些特殊的和 CLI 交互的方式高诺,我們可以把相關代碼放在“Acme/App/Infrastructure/Cli/SpecialCli”命名空間下,而不是放在“Acme/Util/SpecialCli”命名空間下牡拇。前一個名字告訴我們這個包和 CLI 有關,它是“Acme”公司的應用“App”的“Infrastructure”(基礎設施)的一部分剔蹋。既然它是應用基礎設施的一部分,也就是說應用核心中還應該有一個端口矫付,它必須遵循這個端口。
另一種情況是而叼,如果我們發(fā)現(xiàn)包是這種語言自己應該/能夠具備的一些東西,我們可以把它放在可以體現(xiàn)出這層含義的命名空間之下脱篙,比如“Acme/PhpExtension/SpecialCli”。這個名字告訴我們這個包應該被看作是語言自身的一部分,因此其中的代碼應該被其它代碼像語言結構一樣直接使用笔喉。但是作谭,如果其它公司依賴這個包時折欠,顯然不會傻傻地直接依賴它,而是為它創(chuàng)建端口/適配器,這是更妥善的處理方法斤葱,這樣他們可以換掉它。然而,如果這個包是我們自己來負責幔烛,我們就可以決定把它當成語言的一部分來對待饿悬,因為出現(xiàn)尋找替代品的風險的幾率要小得多狡恬。這里總有一些權衡。
另一個我們可以認為是語言的一部分的例子是 PHP 中的 UUID兔乞。我可以不把它想成是語言的一部分熊榛,因為每隔一段時間就會有一個新版本并造成一些維護的負擔玄坦;但它卻是一個通用、廣泛和一致的概念喻喳,足以成為語言的一部分。
所以慷丽,為什么不創(chuàng)建一個 UUID 的實現(xiàn)纲熏,把它當成 PHP 自身的一部分來使用呢勺拣?就像我們使用 DateTime 對象一樣宣脉?只要實現(xiàn)還在我們的掌控之中谈跛,我覺得沒有什么壞處。
那么 Doctrine Query Language (DQL) 呢? (Doctrine 是 Hibernate 在 PHP 中的移植) 我們能把 DQL 當成 SQL嫂沉、Elasticsearch QL 或 Mongo QL 嗎?
總結
所以總結一下,我在宏觀層面發(fā)現(xiàn)了四種主要的代碼類型蜀漆,而且我認為在代碼組織中將它們清晰地展現(xiàn)出來确丢,是我們避免最終形成大泥球的關鍵。
于我而言,不能質疑的真相是架構就在那里,唯一的問題是:它在不在我們掌控之中益缠?基公!。
因此胰伍,讓我們清晰地組織好代碼骂租,來幫助我們表達架構渗饮,全盤或者部分遵循我的心智地圖互站,遵循你自己的心智地圖胡桃,或者遵循其他人的心智地圖都可以标捺,但是請使用一致的推理思路來組織代碼亡容,讓項目可以使用結構和代碼組織來清晰地表達架構闺兢。