对一些架构设计原则的反思
在架構設計的領域,?們總結出了很多原則。這些原則的?語?都很簡略,容易傳播。但是提出這些原則的?,往往不會告訴你,為什么應該是這樣的原則。哪怕說了背景,過了?段時間,聽的?可能已經不知道原則提出?的初衷。?且這些原則,粗看起來是很有道理,可是在實踐中,卻往往不是這么回事,那么就淪為?靈雞湯了。在看這些原則的時候,每個?都要形成??的判斷能?,不要?云亦云才好。以下是個?對?些設計原則的思考,不?定正確,期望能夠引發讀者??的思考,形成讀者??的判斷。
?
KISS 原則
KISS(Keep It Simple, Stupid) 原則,翻譯成中?就是“保持簡單、愚蠢”。這是?句沒有主語的話,猜想主語應該是指設計師,并且這個“It”應該指的是設計師所設計的?標系統。這條原則?意應該是告訴設計師在設計時要保持系統的“Simple and Stupid”。這個原則仔細分析?下,有兩個?問題:
?先,“Simple and Stupid”的判斷原則是什么,怎樣才算是“Simple and Stupid”?這是這個原則中最讓?迷惑的地?,?般?下意識給出的是??所認知的“Simple and Stupid”,這未必是提出這條原則的?所理解的“Simple and Stupid”。比如讓?個體操隊員來做?個后空翻,對于他來說,這是“Simple and Stupid”。可是讓?個沒經過體操訓練的普通?做?個后空翻,這絕對不“Simple and Stupid”,可能會摔斷脖?出?命的。
也就是說“Simple and Stupid”是因??異的,對于技術?平不同的?,他會的或者他能夠熟練掌握的技術才是“Simple and Stupid”。?個?平很?的設計師,看到?個系統,他可以給出他設計的“Simple and Stupid”?案,可是如果實現的?水平很糟糕、技術?準達不到,勉強去實施的話,有可能會實現不出來的,哪怕做出來了也?定是問題多多,弄不好要搞出?事情。因此,要設計?個系統,要根據實施團隊的?平,做適合他們的架構設計,這樣才可以算得上是“Simple and Stupid”。
其次,?個系統的?的是為了給?戶來使?的。?個設計師設計了?個“Simple and Stupid”的系統,那么這個系統對?戶來說就?定好?嗎?如果?便了設計師,或者說?便了實施?員,說不定會影響使?系統的?戶呢?恰恰是讓?戶?起來“Simple and Stupid”的系統,才是?個好?的系統。那么給系統的設計師?個“Simple and Stupid”的原則,到底是在幫助誰?損害誰?符合業務?標嗎?許多公司的設計師在設計系統的時候,不斷的和業務團隊沖突,為了保持設計師所持的“Simple and Stupid”理念,很容易會降低?戶的體驗,導致最后??也?點都不“Simple and Stupid”。
可見,這個設計原則?到軟件?業是有問題的,我們不能夠為了“Simple and Stupid”去設計,不能夠設計?個系統時,依照“Simple and Stupid”為?標去設計。因為設計?個系統的?的是為了更好的完成系統所服務的?戶的訪問,?不是為了設計者或者實施者。考慮設計者和實施者的時候,是在這個完成?戶訪問的前提下,怎么做到低成本的、可持續的迭代。因此,如果?個設計者不能夠理解?戶的需求,不能夠理解?戶通過不同的訪問?命周期來達到??的?的,是無法設計好?個系統的。
只有通過對?戶的業務?命周期、訪問?命周期進?分析,根據流量的壓?不同,進?合理的樹狀拆分,也因此形成不同的系統,那么這些所形成的系統?定是內聚的,邊界?定是清晰的,也?定是“Simple and Stupid”。也就是說,只有從業務上去分析、去拆分,才能夠得到?個“Simple and Stupid”的結構,這是?個副產品,?不是?標。如果依照這個原則為?標去設計,則可能會破壞業務本身的整體。
這個原則本身是從軍??業出來的,說明“Simple and Stupid”含義是有軍?背景的,有軍??業??的標準,讀者感興趣的話可以去研究?下。不去了解?個概念的歷史,盲?的直接引?到軟件?業來,很容易吃虧。?且其中“Stupid”的含義是為了形容系統組件的修理維護簡單程度,不知道這是不是這個原則提出者的“Simple and Stupid”的本意。按照這個意思,如果所設計的系統根本不允許或不需要考慮維護或者修理的話,還需要考慮“Simple and Stupid”嗎?反過來,我們回頭去審查?個系統的時候,如果發現所設計的系統對于?戶訪問的拆分不夠清晰,不是樹狀結構的時候,那么?定是不夠“Simple and Stupid”的,倒是可以作為?個架構審查的判斷點,幫助改進設計,但是不能夠作為設計的依據。
?
SOLID 原則
SOLID 原則,據 WikiPedia 所說是由 Robert C. Martin 總結的?向對象設計的原則。這個名字其實是以下?個原則的?字母簡寫:
-
Single responsibility principle;
-
Open/closed principle;
-
Liskov substitution principle;
-
Interface segregation principle;
-
Dependency inversion principle。
“Single responsibility principle” ,翻譯成中?是“單?職責原則”。這也是?個缺乏主語的話,但推斷應該是指所設計的系統吧,這個系統應該是單?職責的。可是這個“職責”的“單?”,如何來判定呢?不同的?會有不同的認知。據作者原?所給出的參考?章所描述的:“This principle was described in the work of Tom DeMarco and Meilir Page-Jones . They called it cohesion”,原本叫做 Cohesion,翻譯成中?是“內聚”。說成內聚很容易理解,但是作者給出的解釋是“A class should have only one reason to change”,就很難理解了。據給出的例?,說的是?個保齡球的游戲編程,原本 Game 類有兩個責任,?個是負責跟蹤當前幀,?個是負責計算分數,最后把這兩個責任分別給了兩個不同的類。作者給出這個拆分的理由是,“Because each responsibility is an axis of change”,意思是“因為每個職責是?個變化的維度”。猜想作者想表達的意思是兩個正交的維度,拆開可以互不影響的意思。
原本現實?活中打保齡球,可以??算分,也可以讓別?幫忙算。為什么可以拆分開來,這是因為打保齡球的核??命周期是打球,算分只是?個游戲規則,沒有這個規則,保齡球也可以打的,因此這個分數計算規則可以拆分出來。并且保齡球游戲產?的結果是計算分數的輸入,這兩個步驟是打保齡球游戲的兩個連續的?命周期活動,因此非核??命周期可以拆分出去,形成樹狀結構。Game 的原本功能沒變,只不過其中?個步驟的實現分離出去,通過?法調?的?式回歸了?已。這樣 Game 的職責更專注,分數計算也更專注,修改時可以互不影響,確實叫“內聚”比較好。
可是?旦改成“單?職責”,意思就變化了。后?又把詳細解釋的內容從“an axis of change”改成“one reason to change”,意思進?步不同了。“an axis of change”指的是?個維度,?“one reason to change”指的是?種理由,?者很難等同,應該是有很?的爭議的。
那么怎么樣才算“單?”呢?這個是沒有確定的標準的。Game 包含打球和算分兩個步驟,難道 Game 就不“單?”了嗎?保齡球要打球和算分的話,這是?個“單?”的運動,放在?起并不算職責不單?,這樣做并不錯。但是后續修改和維護的角度來看,如果分數計算規則要頻繁的修改,但不希望動 Game 的話,分數計算可以拆分出來,這是?種架構拆分,但并不是因為“單?職責”的緣由才拆分的。那么打球和分數計算分離開來了后,難道分數計算職責就“單?”了嗎?不?定,如果分數計算有很多不同規則,還可以把規則做架構拆分,分數計算職責也并不“單?”呀。
因此,我們說“單?”職責,并不能表述“內聚”的含義。?且“單?”是?個相對的詞語,要看針對什么來說是“單?”的。?個事情分兩個步驟,并不能說這個事情不“單?”,這是?個事情,是單?的。把這兩個步驟分開后,由兩個?來分別執?,對于這兩個?來說,各?的職責是單?的,但是不能因此否認原來這個事情就不“單?”了,因為這兩個?各?“單?”職責的完成,組成了原本的那個“單?”的事情。其實從原作者的本意來看,不過是想表述“內聚”?已,“內聚”這個詞最貼切。
從“單?”的思路去看,最近出現的 CQRS(Command Query Responsibility Segregation),“命令查詢責任分離”,把命令和查詢拆分開來,分開后這個職責“單?”吧,可是這個做法卻完全破壞了業務本身的內聚。還是前?那個保齡球的例?,想象?下算分和查詢分數是不同的類,那么?旦算分的規則發?變化,那么查詢分數的規則不?定能夠跟著算分來變化,Bug 就很容易出現。如果這兩個類分為兩個不同的?來維護的話,?旦出現問題的話,這兩個?就可以沒完沒了的扯?了,責任也很難分清楚,需要?量的溝通成本,最后?定會?團糟,這就是破壞了業務本身的內聚所帶來的后果。CQRS 這個做法往往是?于數據讀寫的場景,?于提升讀或寫的性能,只有當讀、寫時不存在業務邏輯的時候、僅僅是做讀寫通道的拆分的時候才?的。
Open/closed principle ,也就是“開 / 閉原則”。作者總結了這個原則的發明者 Bertrand Meyer 的觀點“Software Entities (Classes, Modules, Functions, etc.)should be open for extension, but closed for modification.”,作者對這句話的理解是,?個模塊同時要能夠適應新的變化,還要不允許修改,這是相沖突的。為解決這個沖突,作者得出?個?案,那就是?抽象類來解決這個問題。?抽象類則會帶來許許多多的其他問題,但仍然無法完全做到對修改關閉。
為什么會出現這個原則呢?從這個原則出現的時期來看,是在 1980 年代提出來的,猜想是因為軟件?程不夠發達,開發?員懼怕改變所導致的。為什么要對修改關閉呢?因為害怕修改所帶來的連鎖反應。在早期的開發實踐中,沒有完善的版本控制、依賴管理等的幫助,無法承擔頻繁的修改所帶來的對項?的沖擊。另???,可能是想要去修改別?的代碼或者類庫,許多?如此的解釋。或許也有可能也不得?知。如果能夠去修改別?的代碼或者類庫,說明??已經有源代碼了,??做好版本控制即可,可以??維護。如果想讓??的修改還要兼容別?的后續升級,無論是??修改還是繼承,都會遇到兼容的問題,這種情況最好的辦法是去請求原作者來修改,保持權責對等是成本最低的。??修改的話,就要做好??維護的準備,并且后續升級的代價會比較?。可是為什么要去修改別?的代碼或類庫呢?如果是功能不滿?,就不要去?別?的代碼,換?個別的類庫或者??寫?個?如果是有 bug,就去請求原作者去修復,盡量不要??動?修改?或者??參加進去成為?個貢獻者也可以。總之要保持代碼創造者對其代碼的權責對等。
隨著現代開發理念的發展,越來越多的?看到了抽象、繼承的壞處,越來越多的?采?組合的?式來協作,其實抽象類可以看成是組合的?種特殊情況。?且隨著代碼的變化越來越頻繁,擁抱變化反?成為了?個風?。只要代碼中的類做到了“內聚”,只要業務代碼能夠做到內聚、訪問通道做到不重?,那么要重?的只會是業務代碼,這樣修改的范圍會?很多,同時依靠版本與依賴管理,完全可以避免修改所產?的影響。因此這個“開 / 閉原則”,也需要重新再看待,理性使?。使?開閉原則,就意味著?量的抽象類、?量的繼承,意味著內聚的喪失,意味著要付出耦合的代價。
Liskov substitution principle ,中?是“?氏代換原則”。前?的“開 / 閉原則”導致了抽象與繼承,“?氏代換原則”則是繼承的進?步體現,也最終形成了多態的特性。作者總結了發明者 Barbara Liskov 的話“Function that use pointers or references to base class must be able to use objects of derived classes without knowing it”,意思就是?個功能如果引?的是某個?類,如果實際傳的是該?類的?個?類的話,這個功能本身的?為不會發?變化。這個原則是很多程序員喜歡抽象的?個理論來源,這?試著分析?下。
?氏代換原則的本意,應該是對開閉原則的拓展,?來實現開閉原則。只有能夠??類來代換,才能夠符合開閉原則。但是總不能夠每次修改都創建?個?類吧?也因此可以看到,開閉原則也是?個無奈之舉。正確的做法是針對修改創建不同的版本,針對不同的版本來進?構建、發布。
但是有了這個代換的辦法,結果?家倒是不?來遵守開閉原則了,?是?來盡可能的抽象,結果把本來應該內聚在?個類中的?法和屬性,分散到許多不同的?類中去了,這是很?的?個弊病。記得以前 Java 認證考試就專門考繼承時的變量初始化,許多?掉進這個坑?。并且這種情況非常容易造成?產事故,因為這種錯誤只有在運?時才能夠發現,還不好排查,往往修改?類時,?類的 bug 就出現了。沒有做到內聚的后果是很嚴重的。
另外?個問題是?氏代換的時候,比如?類中有 Instrument.play()?法,可以??類 Piano.play(),Violin.play() 來代換,雖然引??類時可以?作的很正常,但是 play() 出來的聲?卻是不確定的,因此也不能說?為沒有發?變化,只能說都能夠 play,但是 play 的結果是不?樣的。但是當實際業務很復雜,不光要 play,后續還要調整具體的樂器的話,這個抽象就比較麻煩了,因為不同樂器調整各式各樣不同,然后就發現原來的抽象不夠?,要費很???去進?步抽象。慢慢的在業務的變化下,抽象就變成?團糟了,最后連??也看不懂代碼了。
可是何必要花??去抽象呢?直接引?實際的樂器就好了。除非能夠做到?次抽象能夠適應以后所有的變化,否則還是??實實的?對實際情況吧,哪怕有多個樂器需要選擇,寫個 if-else 就好了,沒??代碼,并且還是可測試的,并且錯誤是編譯期可以發現的,以后修改、擴展也容易。為了這??代碼,引入那么多抽象,破壞“內聚”不說,?氏代換時,?般都是運?時才能確定的,反?導致運?時探查問題的麻煩,同時,代碼也很難閱讀,沒?敢去修改,影響?活質量。
Interface segregation principle ,即“接口隔離原則”。這個原則相當于是預設了調?者與被調?者兩?的前提,對于調?者來講,被調?者的接口數量應該最?化。這個原則其實就是通道訪問的隔離。在訪問通道上,不同的客戶端,不可以使?同樣的訪問通道,因為會導致它們之間的訪問互相影響,這是很簡單的道理。比如?個居民?區的車道和??道必須要分離,否則兩者通道混雜的話,?定會出事情的,?會很容易有?命危險,產?額外的問題。
可是為什么會變成?個?接口呢?恰恰就是為了要重?這個接口,以便讓各種不同的調?者來訪問。所以訪問通道上的重?是萬萬不可的,因此也會導致服務端會變成?個?接口,從?慢慢會變成團隊之間的糾紛點,故障點。
Dependency inversion principle ,即“依賴倒置原則”。作者舉了?個 Copy 的例?,本來是把字符從鍵盤 copy 到打印機,后來增加了需求要 copy 到磁盤,因此要重?Copy 的?法,以后可能還要有多個設備要讀寫,不希望 Copy 程序依賴設備,因此引入了?個抽象類,放在 Copy 程序和具體的設備之間。
其實這個例?的業務背景只是利?Copy 程序作了?個字符傳遞的通道,這個通道本身是沒有任何邏輯的。作者強制性的把不同設備之間的通道綁在了?起,相當于是共享了設備之間 copy 的訪問通道,在業務上來說,這是不符合通道獨?的原則的,因為這些不同的通道可能是屬于不同的業務與不同的?戶,他們之間的需求后續可能會不?致,很有可能因為某個通道的修改,導致其他的通道受到影響。
?這個例?僅僅是為了在編譯期間不依賴于設備?已,為了這個?的?花這么?的代價,有什么?的意義嗎?最終運?期間對設備的依賴是逃不掉的。其實訪問通道依賴于設備是沒有關系的,因為通道沒有邏輯,不需要測試。所以這種通道的共享是?個過度設計,根本沒有必要。?旦 copy 有??的邏輯、?且這個邏輯可能還與通道有關的話,那么這么多通道混雜在?起,反?額外的增加了復雜度。并且這個 Copy 程序根本沒有必要重?,因為沒有邏輯就沒有重?的價值。如果要從鍵盤寫到磁盤,不如重新寫?個 CopytoDisk 更簡單,因為?戶可能不?樣,?起來也更簡單,也更獨?。
作者這個 Copy 程序例?的場景可能舉的不好,但是依賴倒置也是有??的場景的,不是什么時候都需要。依賴倒置的做法,無非是在兩個步驟之間增加?個節點,其實就是作了?個架構拆分,形成了?個新的訪問通道,形成了樹狀的結構。這個依賴叫“倒置”也不太對,只不過變成了依賴中間增加的那個節點,避免了直接依賴?已,但是間接依賴還是在的。真正正確的架構拆分,其依賴?定是樹狀的,從拆分的起點開始往樹的下層依賴,不會出現下層依賴上層的情況,甚至不會出現兄弟節點之間的依賴,因為他們都是從頂層拆分下來的,是?個訪問通道上的不同節點。
?旦發?架構拆分,就意味著要管理這個新增加節點的?命周期,也意味著額外的成本。只有相依賴的兩?會對對?的?作造成影響的時候,才需要通過拆分增加?個節點,以便讓兩?可以獨?的?作,互不影響。并且增加的這個節點不?定是 Abstract 的,也可以是?個實體。?且建議?實體,不要?Abstract,因為依賴于 Abstract 意味著依賴?個繼承樹,成本太?。?通俗的話說,盡量去?對正規公司,不要去依賴?包公司,層級越少,溝通越少,效率越?。所以,不要?開始就去架構拆分,要根據當時所?對的情況,合理的采?。
所以,對于 SOLID 原則,第?個其實是說內聚,只是“單?職責”的提法不好。第?、第三個說的是繼承的問題,這是?向對象語?的特性。繼承會有很多的坑,會破壞內聚,也不?定合適。第四、第五個,其實說的是訪問通道的問題,只要做好訪問通道的隔離就不會有問題。如果?家從這些原則字?上的意思去理解,怕是要?入誤區了。
?
業務內聚與訪問通道內聚
當然,很多?也會提到“?內聚、低耦合”的原則。這個“?、低”的說法不夠嚴謹。只要某個業務的?命周期活動不在?個類中確定,那么這個類就沒有形成內聚,反之就是做到了內聚。只要做到內聚,就沒耦合了,就只有依賴關系,?且這個依賴是?個樹狀的結構;只要沒做到內聚,肯定耦合了,沒有?低之分,最后都會帶來麻煩,區別在于帶來麻煩的多少?已。所以?個應?要么沒有內聚,只有耦合,要么只有內聚,沒有耦合,只有其中?個情況。
可是要做到業務的內聚,卻離不開業務訪問通道的隔離,這個原則我把它稱作“訪問通道不重?”原則。觀察?者的關系可以發現,重?業務與重?訪問通道?者,只能夠選?個。因為重?訪問通道會導致業務無法內聚、也就無法重?;重?業務則會導致訪問通道無法重?。如果想兩者都達成,那么最后的結果?定是只成功的重?了訪問通道,?業務內聚則?定會被破壞。
為什么會是這樣呢?因為?個事物對物理空間的占有是獨享的,?訪問通道則是事物跨越物理空間的通路。必須確保?戶對?個事物的訪問通道是獨享的,才能夠保證這個訪問通道是內聚的。如果不同類型?戶的共享同?個訪問通道,就意味著訪問通道不再是獨占的了,這就是對訪問通道內聚的破壞,最終這個訪問通道就變成?個不確定的通路,內步沖突不斷、阻礙重重,?定會反應到對業務內聚的破壞。
比如?們做公共的交通?具,往往不允許帶寵物,這就是要遵守寵物和?類的訪問通道內聚原則,因為寵物和?類在狹窄的空間?共存,會產出非常多額外、不必要的沖突。所以我們說“內聚”,絕對不能只提業務的內聚,訪問也是?種獨特的業務,也需要達到內聚的原則。也就是說,“訪問通道不重?”原則其實說的就是“訪問通道的內聚”。
如果做好了業務的內聚,并隔離不同類型客戶端對業務的訪問通道,形成訪問通道的內聚,基本上程序就不會太差,代碼就會很穩定。有了這個基礎,再根據運營過程中所產?的瓶頸點,有針對性的做業務架構拆分或訪問通道架構拆分就很容易了。做為?個架構設計師或者程序員,如果不把“內聚”放在最重要的位置,最終?定會被需求給淹沒的。
因此,架構設計的的核?原則就是“內聚”,任何架構原則都不能違反此原則。這個“內聚”包括兩部分:“業務內聚”,“業務訪問通道內聚”。所以,對于我們遇到的任何?個架構原則都可以這樣去判斷:如果發現它違反了“業務內聚”原則,我們都要三思,因為會導致業務分散、無法重??如果它違反“業務訪問通道內聚”原則,也就是“業務訪問通道不重?”原則,我們也要三思,不要去追求訪問通道重?。
“訪問通道內聚”原則是軟件?業普遍忽視的,這個原則太不起眼,也太容易被破壞,?家都忽視了。“訪問通道內聚”的缺失?定會導致“業務內聚”原則的破壞,導致業務無法重?。于是,系統就開始陷入困境了。
總結
以上是生活随笔為你收集整理的对一些架构设计原则的反思的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 超级大汇总!200多个最好的机器学习、N
- 下一篇: 那些让你起飞的计算机基础知识