架構整潔之道導讀(二):續
關於元件聚合張力圖的討論
週三的午休時間,我在ThoughtWorks北京辦公室分享了一場《架構整潔之道導讀》。當談到分享元件聚合原則的時候,很多同事表示難以理解。究其緣由,是我們無法將元件違反原則的後果對應到真實專案的問題上,這就導致原則和實踐之間的不一致。討論的過程異常激烈,但是很遺憾地最終並沒有得到一個服眾的結論。所以為了進一步澄清這些爭議點,我決定專門組織一場針對元件聚合原則張力圖的討論會。在吳大師的鼓動下,時間定在下週四晚上的8點半,與會人員大多是諮詢團隊的技術教練,也有我們專案上的客戶。
在這場長達兩個半小時的討論會上,沒想到首先出現爭議的點居然是元件的定義。
元件是軟體部署的最小單元,是整個軟體系統在部署過程中可以獨立完成部署的最小實體。
對於這樣的定義, ofollow,noindex">大魔頭 提出了質疑:library(庫)並不能獨立部署。但凡出現明顯的邏輯漏洞的時候,我們最好的方式是拋開譯文回去看原文。
Components are the units of deployment. They are the smallest entities that can be deployed as part of a system.
閱讀原文之後,我們發現“元件是軟體部署的最小單元。”這句話翻譯得並沒有太大問題,但是第二句就有損原意了,原意是說 可以作為系統的一部分被部署的最小實體 ,而沒有強調部署過程這種動態的概念,否則就和前一句是同義反復。所以這個定義裡面並沒有說元件可以獨立部署。後面提到元件可以被連結到一個獨立可執行檔案或者歸檔檔案,又或者,可以被打包成.jar、.dll或者.exe檔案,並以動態載入的外掛形式實現 獨立部署 。
解讀元件的定義
來自原文:
Components can be linked together into a single executable. Or they can be aggregated together into a single archive, such as a .war file. Or they can be independently deployed as separate dynamically loaded plugins, such as.jar or .dll or .exe files.
來自討論:
20:56:56 From tianjie : These dynamically linked files, which can be plugged together at runtime, are the software components of our architectures.
聯絡上下文理解之後,我們知道:元件可以被設計成獨立部署的,但是並不是所有的元件都是可以獨立部署的。這是要澄清的,不然討論聚合原則的時候容易出現偏差。
吳大師接著解釋說,元件應該是個邏輯單元,而不是物理單元。強制某個程式碼模組就是一個物理的部署單元是不合適的。另外,鮑勃大叔在介紹架構邊界時,也表明了一樣的觀點:架構的邊界並不是服務的邊界。
解讀REP原則
我按照自己的思路解釋過REP、CCP和CRP原則之後,討論的焦點很快聚集到REP原則的解讀和實踐意義上。
吳大師 認為REP原則如果簡單解讀成沒有釋出過程就不能複用,它就和CCP、CRP原則的排斥力量不均衡,無法形成穩定的三角關係,那麼這個張力圖就顯得有點雞肋。
尚奇 受到CAP(分散式系統基本原理,一致性,可用性和分割槽容錯性)原則的啟發提出了另一個解讀方向。他說,CAP原則在分散式系統的實踐裡,都會先站住P原則,然後在C和A中權衡。那麼在REP、CCP和CRP三角關係裡,REP原則就相當於這裡的P原則,必須先滿足然後再去取捨CCP和CRP。
大魔頭理解REP的意思是可複用性就是元件是獨立可複用的。假如回到沒有Maven這些工具,沒有依賴管理的年代,如果我們所依賴的包還依賴其它第三方包,那麼這個包就不能叫做獨立可複用。
21:13:04 From YangYun : 我倒是理解REP的意思是你釋出出來的一個可重用的包就是獨立可重用的,你不能讓我必須帶著別的jar包才能用它。
21:14:04 From YangYun : The granule of reuse is the granule of release
他接著說,假如有兩個提供同樣功能的包,其中一個沒有第三方的依賴,而另一個有,那我當然選擇前者。
技術教練Sara舉出了一個相對複雜但是很有啟發性的例子。
21:46:35 From Qian Ping : 假設專案包含sub module ABC
- 如果ABC單純sub module沒有打成jar,又互相直接複用了,就是違反了REP
- 如果每個sub module,打成jar,互相複用的時候是通過對方特定版本的jar(如snapshot版本),就是符合REP
- 如果符合REP了,而所有sub module是跟隨整個專案一起升級版本,就是符合CCP因為他們是一體一起釋出的
- 這時假如A依賴B和C,我這次單純想改C,他們一起升版本了。但其實B的Jar完全沒有變化,這個對B來說就是一個不必要的釋出,B又貌似應該分離出去,但如果它分離出去了,就又離REP和CCP遠了
對於最後一句的表述,她澄清道:
之前有遇到一個情況,比如元件A,然後它裡面需要用到一個common library, lib裡面其實包含了比如3個sub module(1/2/3),全部都是A需要複用的, 這時候如果要改1/2/3裡面任意的東西,都會一起升級lib,然後在A裡面對應升級版本。
後來,有一些新元件B,它只需要用到common lib裡面的3,不需要1/2,於是3一直被改和打包版本。 此時1/2會跟著升版本號,但其實1/2內容本身是完全沒有變化的,只是版本號升了。
這個場景中引入了兩個元件A和B分別依賴common library的某些模組。在我們討論一個元件依賴時,面臨的約束要簡單很多,但是複用的初衷就是給多個元件去依賴,所以這個假設是很有價值。
Sara分析的思路如下:
如果分離出去,等於我有兩個common lib(1/2 和 3), 對於B來說,B只需要3這麼一個lib是比較完美的,反正改了3再改B就好了。
但對於A來說,它就需要同時升級1/2的lib和3的lib,等於要3個釋出,而它原來只需要2個釋出(1/2/3 + A),所以離CRP遠了,同時它也要分別維護兩個lib分別的版本升級,所以CCP也比原來差了。
在她的分析下,我們發現CRP和CCP不單是互相排斥的,還有可能兩者都無法滿足。造成這種結果的原因在於1/2/3模組形成的這個common library對於A元件而言都符合CCP和CRP原則,但是對於B元件而言,是不滿足REP和CRP原則的,因為每次想要依賴3模組,就得全部依賴1/2/3整個common library(複用困難)。反之,如果我們將3從1/2/3中拆出來成為獨立的元件,那就幾乎宣告對於A元件而言勢必違反CCP和CRP原則,但是B元件卻獲得了符合REP和CRP原則的好處。
她接著補充道:
其實後來說起對應微服務的時候有另外一個想法,就是比如說我係統裡面多個元件需要用計提(Mark to market)這麼一個功能,說白了就是一條公式,那通常可以有幾個做法
- 直接把這個公式複製到要用的元件,code level的複用,沒有版本 -> REP bad, CCP bad, but CRP not bad (因為要更改時候釋出次數還是一樣的)
- 把公式寫到一個common lib裡面再進行復用 -> REP good, CCP good, CRP bad(多釋出一次)
- 把公式放在一個獨立service -> REP good, CCP bad(因為要維護多一個服務), CRP good
這個觀點就上升到不同層次的複用性上,可以算是對元件聚合原則的普適性的探索。
當話題再次被聚焦到複用性時,技術教練 MoMo 提出一個觀點:我們現在討論就是可複用元件應該遵循的原則,而REP是對複用粒度的定義。至於那些那些常年採用SNAPSHOT(Java專案裡Maven常用的開發版本號),沒有釋出概念的元件,就不該納入複用的考慮範圍內,那些也就不是REP的反模式。
與此同時, 閻王 指出了一個翻譯上的失誤。元件粘合張力圖中REP原則的簡短描述是“為複用性而組合”,而原文其實是"Group for reusers",翻譯過來應該是為了複用者而組合,複用性的英文是 Reusability。所以為了複用者釋出,考慮的就是對外部的承諾。
tension diagram
外部資料
大魔頭在加班寫方案和討論的間隙,快速查閱了一些資料,比如wiki上對於REP原則的定義:
21:45:05 From YangYun : Reuse-release Equivalence Principle (REP)
REP essentially means that the package must be created with reusable classes – “Either all of the classes inside the package are reusable, or none of them are”. The classes must also be of the same family. Classes that are unrelated to the purpose of the package should not be included. A package constructed as a family of reusable classes tends to be most useful and reusable. - wiki百科裡
在wiki的定義裡,可以看到REP原則包含CRP和CCP原則的成分,如此看來,這三大原則並不符合 MCME 分類原則,就連鮑勃大叔在書中也是模稜兩可的態度——REP維護共同的大主題,元件中的類和模組也必須緊密相關,這基本是CCP和CRP的簡版描述。
然後大魔頭查詢到“粒度”這個詞在軟體設計中詳細定義,這是對REP原則定義(軟體複用的最小粒度等同於其釋出的最小粒度)的分解和再認知。
21:57:03 From YangYun : http://condor.depaul.edu/dmumaugh/OOT/Design-Principles/granularity.pdf
granularity
21:58:29 From YangYun : https://fi.ort.edu.uy/innovaportal/file/2032/1/design_principles.pdf
design principles
這些觀點和學術建議很有代表性,值得大家反覆揣摩和思考。
反模式
軟體工程師一般有個“正難則反”的習慣。原則較抽象,但是模式很具體,反模式更能指導實踐。接下來,大家開始討論哪些是違反了REP原則的反模式。
首當其衝的就是 git submodule
,在某些專案中,這種通過原始碼劃分模組並共享的方式還是挺常見的。因為共享的是程式碼,所以每次共享程式碼更新,勢必要讓依賴方重新編譯,釋出和部署。這種做法對於複用是痛苦的。
其次是常年使用SNAPSHOT版本的某些專案。這些專案的特點一般都是某個產品團隊底下,內部團隊之間有複用的要求。缺點其實也很明顯,常年SNAPSHOT等於沒有版本和釋出的流程。使用者並不知道SNAPSHOT中哪些是穩定的,哪些是修改的,拿到的版本到底是最新的還是遺留的,我需要的功能在這個功能有包含,還是你包含了太多我不需要的升級。這種也是複用痛苦的。
REP原則小結
綜合以上兩個例子以及其它討論,我們得出了一個好玩的結論:軟體工程發展到現在,REP原則已經是基本的要求,它的存在有可能是鮑勃大叔年代感
的體現。
於 2018-11-12