天天看點

架構師修煉 III - 掌握設計原則

架構師扮演着技術方案的“法官”角色,針對各種沖突作出準确的判斷。在架構設計的世界中“沒有最好的設計,隻有更好的設計”同時“沒有絕對的正确決策,也沒有絕對的錯誤判斷”,架構師的隻是在辯證中尋找“合适”的方法作出“客觀”的判斷,究竟這些判定的依據就是 — 設計原則。

關于軟體的設計原則有很多,對于設計原則的掌握、了解、實踐及升華是架構師的一項極為之必要的修煉。 記得在12年前第一次閱讀《靈活開發》時,五大基本設計原則就深深地植入到我的腦海中一直影響至今,我也由此獲益良多。設計原則當然不止隻有五種,最主要的面向對象的設計原則有以下這些:

  • 單一職責原則 (SRP) - 就一個類而言,應該僅有一個引起它變化的原因
  • 開-閉原則 (OCP)- 軟體實體(類,子產品,函數等)應該是可以擴充的,但是不可以修改
  • 裡氏替換原則 (LSP)- 子類必須能夠替換它們的基類型
  • 依賴倒置原則 (DIP)- 抽象不應該依賴于細節。細節應該依賴于抽象。
  • 接口隔離原則 (ISP)- 不應該強迫客戶依賴于它們不用的方法。接口屬于客戶,不屬于它所在的類層次結構。
  • 重用釋出等階原則 (REP)- 重用的粒度就是釋出的粒度。
  • 共同封閉原則 (CCP)- 包中的所有類對于同一類性質的變化應該是共同封閉的。一個變化若對一個包産生影響,則将對該包中的所有類産生影響,而對于其他的包不造成影響。
  • 共同重用原則(CRP)-  一個包中所有類應該是共同重用的。如果重用了包中的一個類,那麼就要重用包中的所有類。
  • 無環依賴原則(ADP)- 在包的依賴關系圖中不允許存在環。
  • 穩定依賴原則 (SDP)- 朝着穩定的方向進行依賴。
  • 穩定抽象原則(SAP)- 包的抽象程度應該和其穩定程度一緻。
  • 合成/聚合 複用原則(CARP)- 要盡量使用合成/聚合 ,盡量不要使用繼承
  •  …..

當然面向對象的設計原則遠遠不止這些,設計原則是伴随着開發語言的發展應用和軟體開發經驗的累加總結得出的經驗彙總,随着語言的演變、開發方法的進步還會不斷地衍生和進化出更多的的設計原則。應用設計原則可以避開很多的設計中的陷阱與誤區,但在應用設計原則的同時需要緊記一點:設計原則本質上是一些經驗條框,是設計的導盲杖而不要讓它們成為束縛設計思想的牢籠。

每個架構師在經曆長期的實踐後也會慢慢建立屬于自己的設計原則。多年來我也總結出了一些設計原則,并将上面這些這種可用于代碼設計的原則歸納為:“代碼設計原則”,另外一些應用于意識與設計過程中的原則稱為“意識-行為原則”。以下我将會分别講述我對這些設計原則的了解與運用的經驗。

意識 - 行為原則

  意識決定行為,很多的設計失誤并不單純源自于對設計原則的把握不足,而更多可能源自于架構師在意識指導上的錯誤, 是以在開始設計之前應該先建立正确的思想與意識引導。以下的這些意識-行為原則是我從很多次的跌倒中總結出的一些心得,将期作為原則是為了時刻引導自己不會在類似問題中犯錯。

堅持創新原則

  首先談談模闆式設計,我相信模闆對于每一位開發人員和設計人員來說都是非常好的東西,因為它可以“快速”建構出“成熟”的代碼、結構或UI。“拿來主義”在業界盛極不衰,對于架構師而言模闆也有這種功效,在設計的過程中我們會經常遇到很多必須而不重要的“雞肋”子產品,沒有它們系統會變得不完整,而它們的存在并不能為系統增加任何的“特色功能”,如:使用者管理、角色管理或系統設定等。常見做法是,直接采用第三方子產品或是從已有的其它項目中複用類似的子產品,你是這樣的嗎 ?至少我是經常這樣做的,因為我們的中國式項目通常是“驗收驅動”,能通過驗收、成熟可用就好。如果整個項目都隻是由各類模闆化的子產品所構成,那麼這個項目其實不需要架構師,因為不存在任何設計,所有的工作隻是一種“融合”(Fusion)。可能這樣說會有很多人會吐槽說這是一種“資源整合”能力,從“趕項目”的角度來說這無可口非,但從技術含量與本質上說确實不存在任何設計成分,這類拼裝性或是“複制”性的項目隻需要項目經理配備幾個進階程式員就能完成了。

我曾在“表達思維與駕馭方法論”一文中提到與銷售的溝通方法,其中就有一條:“至少說出系統的三個特色”,這個表述對銷售具有市場意義以外 , 其實對于架構師是起到一個重要的提醒作用同時也是在建立一種設計原則:

  架構設計中模闆的拼裝是不可避免的,重要的是必須加入屬于你的特色設計

很難有人記得住整個軟體的設計師,而卻很容易記住某項極具特色功能的設計者。“特色” 是架構師在軟體中所留下的一種重要的印記,也是在團隊中配備架構師的意義所在。設計出完全可被模闆化重用的設計是一功力,而當中小型企業内出現這樣的設計之日就是架構師離開企業之時,或許這也是當下中國架構師之殇。保持特色保住飯碗,你懂的。

固守本質原則 

  唯一不變的就是變化本身 —  Jerry Marktos《人月神話》
  不變隻是願望,變化才是永恒 —  Swift

看到這兩句經典是不是猜到我想就“變化”二字來一次老生常談 ?其實不然,這兩個字在業内估計也讨論了20多年了,也說爛了。我之是以引用這兩位大師的名言隻是想無時無刻提醒自己要了解身邊的每一個變化,對他們的源頭産生興趣,進而深入了解。世界上不會有無緣無故的愛,也沒有無緣無故的恨一切皆有根源,那是 “本質”。我們來将 “本質” 與 “變化” 這兩個哲學性的問題應用到軟體開發的範疇内來看一個軟體産品的疊代:

  • 使用者的需求在變 - 他們需要增加更多的功能,要求更高品質的使用者體驗。
  • 代碼在變 - 不斷的重構、測試,持續內建,讓代碼變得容讀,穩定。
  • 老闆的想法在變 - 因為市場需求在變,需要為軟體加入更多的特色滿足市場。
  • 架構在變 - 采用更新式的技術體系,獲得更高效的生産力,更為穩定、安全的運作環境。

而唯一不變的是:軟體的核心。正如:Windows 變了N個版本最後還是操作平台,Office 衍生了多代後若然在處理文檔檔案 。

  變化是表像,不穩定且可定制的;本質是核心,必須穩定,可擴充而不可修改;被固定的變化則可納入核心。

  架構應從本質入手,一切複雜的事物都應可被分解為簡單的原理和構成,本質之外的内容皆可變化。我們來舉例說明,設計一個電子商務網站,其核心就可被分解為 “購物車” 與 “訂單狀态跟蹤”這是不可變的除非大衆的整體購物行為發生了本質上的改變,為了增加使用者體驗我們選用美觀舒适的界面套件如BootStrap,如果進一步提升使用者體驗則可以采用SPA的架構讓客戶在Web上獲得Native式的使用體驗;為了讓使用者使用不同的支付方式,我們就需要定義支付網關接口(引入變化)支援已有的支付平台,也為将來“可能”出現的支付平台留有擴充。為了增強網站對使用者的粘性,我們就需要增加社群子產品,并采用雲存儲或是其它的BigData技術以支撐大資料量的運轉;....  最後,一切的本質仍然不變,電商網站,變的是擴充性、易用性、伸縮性等等。架構師可以向其中添加的功能太多太多,但必須固守本質才能讓整個産品不會成為一個由高技術打造出來的怪物,在增加新功能時參考 “代碼商人”原則的指引。

“代碼商人” 原則

永遠不要投資未來,絕不設計沒有回報的功能

不知道你是否擁有類似的經曆:

  • 在與客戶的交流中,你的老闆和經理在不斷地向客戶描繪“未來”圖景,而在其中包含了很多幾乎是客戶沒有需要的特色 ?
  • 在你設計整體架構時,有一種沖動讓你很想将某項由靈感觸發對于系統“将來”的擴充需要很有用的功能或子產品加入其中呢 ?
  • 在你的代碼裡面有多少個方法或類是可以被删除,但你認為他們可以用于“以後”擴充而“手下留碼”的呢 ?
  • 你是否曾與經理或項目組長為了是否增加某個很有可能成為特色且被你實作出來的功能争論不休呢 ?

  衡量标準的尺子掌握在架構師手中,如果設計中出現林林總總的這些“未來功能”您會如何來對待呢 ?是直接砍掉還是将其包裝成為“特色”呢 ?此時架構師不單單是需要作為一名技術人員的角度考慮這個功能是否在将來可用,而更多的是需要考慮“成本”。每個功能甚至每行代碼都需要付出“人-月”成本,一旦成本失控,軟體就會化身“人狼”吞掉你的項目,而最後也隻能後悔沒有找到“銀彈”。每個“未來”功能如何不能對現有項目帶來即時性的回報,必須砍掉!即使這個功能有如何的美妙、高深或是在将來具有非凡的意義,還是将它放入“研究室”成為其它項目的技術儲備吧。站在商人的立場:每一分錢的成本投入,都需要有足夠的利益回報。

  未來永遠是美好的、豐滿的同時也是浮雲,而現實卻往往是充滿骨感。在架構或代碼中透支未來極少數可獲得回報,因為這些“投資”都具有不可預見性隻是一些嘗試,在産品中除了“市場政策”需要外的這類過分投資就得有陷入“維護未來”的心理覺悟。新的功能、未來的特色更應該收集起來,作為一下版本中可選項,通過詳細的市場研究再考慮加入到産品中。當然,對于大型軟體企業這個原則基本上是多餘的,因為很多成熟的軟體企業對需求的控制極其嚴格與規範。但如果你所在的企業還沒有這樣的管理意識,或具有超脫性的設計自由,那麼這條原則是非常重要的,我們是用代碼換錢的人,更少的代碼換更多的錢才是我們最基本的生存需要。

重構優先原則

在沒有代碼的時候就應該重構,重構是寫出優雅代碼的方法而不單純是修改代碼的理論。

駱駝與帳篷的故事

  在風沙彌漫的大沙漠,駱駝在四處尋找溫暖的家。後來它終于找到一頂帳篷,可是,帳篷是别人的(也許你的處境跟它一樣)! 

  最初,駱駝哀求說,主人,我的頭都凍僵了,讓我把頭伸進來緩和暖和吧!主人可憐它,答應了。過了一陣子,駱駝又說,主人,我的肩膀都凍麻了,讓我再進來一點吧!主人可憐它,又答應了。接着,駱駝不斷的提出要求,想把整個身體都放進來。 

  主人有點猶豫,一方面,他害怕駱駝粗大的鼻孔;另一方面,外面的風沙那麼大,他好像也需要這樣一位夥伴,和他共同抵禦風寒和危險。于是,他有些無奈地背轉身去,給駱駝騰出更多的位子。等到駱駝完全精神并可以掌握帳篷的控制權的時候,它很不耐煩地說,主人,這頂帳篷是如此狹小以緻連我轉身都很困難,你就給我出去吧

  這是一個很有寓意故事,如果将其比喻為開發過程也很有意思。對于“發臭”甚至“腐爛”代碼我們會馬上說“重構”,但重構是否能解決一切問題 ?你是否試過重構失敗呢 ?重構在什麼情況下是不可用的呢 ?如果這些問題在你心中是沒有準确答案的話, 我建議可以重新去閱讀一次《代碼重構》一書。我認為重構不單純是一種開發期與代碼回顧期所使用的方法,而是一種設計與編碼的思想指導!在設計期就應運用重構中的原則,那是否就可以“防腐”呢 ?答案顯然是确定的。重構的往往不單純是代碼,而是開發人員、設計人員的思想,不執行甚至沒有代碼規範、随意命名、随意複制/粘貼、随意調用這些都必須被杜絕。我并不是指在設計重構就不需要重構,隻是這樣做的意義可以大量減少由于發現“臭”代碼而去重構的成本 。

  這也可以說是一個團隊性的開發原則,在項目之始就得有統一的編碼規範(直接使用官方規範),并将重構中的基本代碼重構方法也納入規範中,在開發過程中強制執行規範,對任何可能“腐化”的代碼絕對的“零”容忍,痛苦隻是一時,但好處卻是長久的。

代碼設計原則

開放-封閉原則 

開放封閉原則又稱 開-閉原則 Open-Closed Principle (OCP) 

軟體實體(如類,子產品,函數等)應該是可以擴充的,但是不可以修改。

  OCP是一個極為之出名的設計原則,簡單的一句話就概括了可時該“開放”可時該“封閉”。這句話看起來很簡單,一看似乎也會覺得自己領悟了什麼,仔細咀嚼卻覺得内中深意無限,到底應怎樣了解這句話且将其應用于設計中呢 ? 我參考了不少國内的資料對此原則的總結,感覺就是霧裡看花,沒有辦法找到最為貼切的解釋。

我想分幾個方面來诠釋這個原則:

從類設計的角度

  在類設計的應用中開-閉原則是一種對類的“多态”控制原則。開閉原則在基類或超類的設計中由為重要, 可以簡單地理為對 成員對象的作用域 和 可“重載”成員 的控制指引原則。按 “裡氏替換原則” 基類成員通常對于子類都應該可見,也就是說基類成員的作用域的最小作用範圍應該是 protect , 如果出現大量的 private 成員時就應該考慮将private 成員們分離成其它的類,因為些成員都不适用于其子代而違反了“替換原則”,而更适用“合成/聚合原則“。

  在運用 virtual 關鍵字時需甚重考慮,除了針對某些特殊的設計模式如 ”裝飾“模式需要大量 virtual 的支援以外,在沒有必要的情況下盡量避免。定義可重寫的成員為子類預留了”改變行為“的餘地,但同時也是為子類違反”替換原則“埋下了地雷。當子類中出現大量重寫成員的時候就得考慮該子類是否還應該繼承于此類族,因為子類在大量地違反”替換原則“時就意味着它滿足了被分離出類族的條件。同理,在C#内一但需要在子類内部實作基類接口時也需要作出同樣的考慮。

 注:裡氏替換原則是開-閉原則的一種重要補充,在類設計中一般是同時使用。

從子產品設計的角度

  子產品設計的“開-閉原則”是側重于對接口的控制。而這個在整個架構中也尤為重要,因為子產品間的“開-閉”是直接影響系統級的耦合度。子產品間的開閉需要“衡量成本”,并不是将所有的細節都開放使用子產品具有極強的可擴充性就會有很高的重用度。首先要看了解幾點:

開放性與維護成本成正比關系

  接口的開放必須帶有使用說明,這會增加團隊開放的溝通成本同時一但接口發生改變将可能帶來額外的“說明性重構”成本。在某些情況下我們很容易被“高擴充性”所引誘将很多“可能”被複用的功能通過擴充接口暴露出來。當這種高擴充性的誘惑主導了設計師的思維,随着子產品的增多項目的變大、慢慢地設計師就會進入自己所建立的“注釋惡夢”中。

開放性與耦合度成正比關系

  子產品的開放性接口是具有耦合傳導效應的,控制子產品間的耦合度就能在很大程度上控制了系統的耦合度。子產品間的依賴性越小,耦合度越低才更易于變化盡量将耦合度集中在某一兩個子產品中(如:Facade 模式),而不是分散在各子產品間。耦合度高的子產品自然而然地成為“核心”子產品,而其實的“外部”子產品則需要保持自身的封閉性,這樣的設計就很多容易适對未知的變化。

由這兩個正比關系結合對實作成本的控制上我們做出兩個最為簡單可行的推論:

推論1:“正常情況下請保持封閉,沒有必要的情況下絕不開放”。

推論2:“集中開放性,讓子產品間保持陌生”

開-閉原則從理論上來談會有很多内容,但實作起來卻很簡單, 就以C#為例控制子產品開放性的最簡單辦法就是控制作用域:internal , public。

3.從函數/方法設計的角度

我為認為OCP用到極至的情況就是應用于方法級,衆所周知:參數越少的方法越好用。開-閉原則可以簡單地了解為參數的多寡與返會值的控制

在此我更想談談“開-閉原則”在C#中的應用。首先在方法設計上,C# 給了設計人員與開發人員一個極大的空間,到了4.5我們甚至可以使用async 方法來簡單控異步方法,那麼先來總結一下C#的方法參數的種類。

  • 固定參數:public void methodName(string a, out b, ref c);
  • 動态參數:public void methodName(string a, string b=“defautlString”) 
  • 可變參數:public void methodName(params string[] a);
  • 表達式參數(方法注入):public void methodName(Func<string> func, Action act);
  • 泛型參數:public void methodName<T>( T a) where a : class;

在C#中我們則需要從“注入”這方面來思考和充分發揮語言自身的特性,以達到簡化代碼,增強易讀性的效果。 這裡談的“注入”主要指兩個方面,一 是 “代碼注入”,二是 “類型注入”。

“代碼注入”就是向方法傳入“代理”類就是在方法内部開辟出某一“可擴充”的部分以執行未知、可變的功能 ,那麼我們就可以對相對“封閉”的方法增強其“開放”性。

通過泛型方法的使用,我們可以在對類型“開放”的情況下對類型的通用操作相對地“封閉”起來,這樣可以在很大程度上利用泛型複合取代類繼承,降低類的多态耦合度。

裡氏替換原則(LSP) 

  凡是基類适用的地方,子類一定适用

裡氏代換原則 (Liskov Substitution Principle LSP)面向對象設計的基本原則之一。 裡氏代換原則中說,任何基類可以出現的地方,子類一定可以出現。 LSP是繼承複用的基石,隻有當衍生類可以替換掉基類,軟體機關的功能不受到影響時,基類才能真正被複用,而衍生類也能夠在基類的基礎上增加新的行為。裡氏代換原則是對“開-閉”原則的補充。實作“開-閉”原則的關鍵步驟就是抽象化。而基類與子類的繼承關系就是抽象化的具體實作,是以裡氏代換原則是對實作抽象化的具體步驟的規範。

在前文”開-閉原則“關于類設計應用部分已經基本叙述過”替換原則“的用法。 這個原則,我一直是反向了解的,這樣就非常容易運用,我是這樣使用的:

  • 凡是出現大量子類不适用的成員,子類就應該脫離繼承關系
  • 基類中凡是出現大量虛成員,該類就失去成為基類的條件

參考:Agile Design Principles: The Liskov Substitution Principle 

依賴倒轉原則(DIP) 

   要依賴抽象,不要依賴具體。

  DIP 就像LSP一樣,原文與譯文其實都非常坑爹,這裡我就不直接引入原文了,因為我希望每個讀這篇文章的朋友都能了解并應用這些原則而不是在玩文字遊戲。DIP 用最為簡單的表述就是:“面向接口程式設計”。子類可以引用父類方法或成員,而父類則絕對不能調用任何的子類方法或成員。一但上層類調的方法調用了子類的方法就會形成依賴環,一般上編譯器會“放過”依賴環認為這不屬于邏輯錯誤,但具有依賴環的類結構是無法序列化的(在C#中會直接抛出環狀引用的異常)。

通俗點:“規矩是祖宗定的,子孫隻能執行和完善”,用這個口決就可以完全掌握此原則 。

在過去(10年前)開發工具還比較落後,這是原則十分重要,而如今可以借助VS.net去找到出這種設計錯誤,也可以直接使用IoC 和 DI 就會自然而充分地尊守此原則 。

接口隔離原則 (ISP)  

  使用多個專門的接口比适用單一的接口要好

  架構師在邏輯世界就是神,設計軟體的過程就是創造邏輯世界,每一個接口就是這個世界中的一種規則,類則是實作規則的做法,執行個體就是執行規則的人。 在實作工作中,我們會經常遇到這樣的現象:一個PM可能同時在跟進好幾個項目,或是一個PM要同時充當架構師、PM、程式員甚至售前的角色,這些苦B們是公司内最累的人,同時也是失敗率最高的群體,為什麼? 答案顯而易見:人的精力是有限的,專注于某一件事才能真正有成果。同理,在邏輯世界也是一樣的,當接口要承載多種的任務,被衆多不同的類所調用時就會出現“接口過載”或者”接口污染“,實作這些接口的類将會産生很高的耦合度,進而代碼會變得難以閱讀,難以了解,也難以變化。分離接口就是隔離了客戶(接口的使用者),隔離客戶就自然降低耦合度。

  一個完美的世界就應該是專人專項,讓擅長的人做其擅長的事,在現實不可能但邏輯世界卻可以。那麼在設計中如何來把握這種原則呢 ?很簡單,當一個接口上的方法被多個類調用時就要警覺了,如果這些方法間沒有依賴關系,甚至是不同類别(在做不同的事)的方法那麼就得考慮使用ISP原則将接口分離成兩個獨立的接口,使接口的耦合度從1..n 降低至 1..1. 

合成/聚合 複用原則(CARP)

  要盡量使用合成/聚合 ,盡量不要使用繼承

  複用原則是一個很容易被忽略而又極其重要的原則,這個原則具有非常深遠的架構意義。對于小型項目(類庫規模小)即使違反此原則也不會帶來什麼危害,但當建構大規模的類庫(數百甚至數千個類)時,這個原則就可以防止出現“繼承失控”、過度膨脹、無法重構等的風險,也決定了整個結構的可重用性和可維護性。在定義中它隻是一句簡單的話,但從“繼承”、“合成”與“聚合”就引出了一系列的内容,涵蓋多種設計模式和附帶多個更小層級的應用原則。

(注:關于合成/聚合的好處請去百度吧,關于“白箱複用”與“黑箱複用”都被轉爛了)

 首先要正确的選擇合成/複用和繼承,必須透徹地了解裡氏替換原則和Coad法則。Coad法則由Peter Coad提出,總結了一些什麼時候使用繼承作為複用工具的條件。 

Coad法則:

隻有當以下Coad條件全部被滿足時,才應當使用繼承關系:

  1. 子類是超類的一個特殊種類,而不是超類的一個角色。區分“Has-A”和“Is-A”。隻有“Is-A”關系才符合繼承關系,“Has-A”關系應當用聚合來描述。 
    • “Is-A”代表一個類是另外一個類的一種;
    • “Has-A”代表一個類是另外一個類的一個角色,而不是另外一個類的特殊種類。 
  2. 永遠不會出現需要将子類換成另外一個類的子類的情況。如果不能肯定将來是否會變成另外一個子類的話,就不要使用繼承。 
  3. 子類具有擴充超類的責任,而不是具有置換掉(override)或登出掉(Nullify)超類的責任。如果一個子類需要大量的置換掉超類的行為,那麼這個類就不應該是這個超類的子類。 (注:在C# 中 含有 new 的方法、屬性和内部實作單一基類接口就相當于Nullify)
  4. 隻有在分類學角度上有意義時,才可以使用繼承。不要從工具類繼承。 

對于一個老手Coad法則隻是一種總結,很容易了解與運用,但如果你是一個架構新手Coad法則就很是坑爹(我的了解力很低,當年我就被坑了很久)!是以我想另辟蹊徑從其它角度來嘗試解釋這個原則。

繼承控制

繼承是面向對象的一種重要構型,複用原則隻告訴我們“盡量不使用繼承”而不是将繼承魔鬼化,在很多場景下,小結構繼承是非常常見與易讀的。隻是,我們需要了解繼承的子代的增加是以整個類結構的複雜度增加n次方在遞增,随着子代層級的增多“類家族”結構的變化就越來越難。其實,我們可以找一些自已手上的例子來看看,如果有3代以上繼承關系的類,看看最小的子孫類與基類之間是否已經有點“面目全非”?這一點與人類的繁衍與繼承是很類似的。再深入一點就是如果向最頂層的基類進行擴充,是則能完全适用“替換原則”呢 ?更改高層級結構時是否有“揮舞大刀”般的沉重感 ? 對是否有勇氣對穩定的祖代類重構 ?

推論:“盡可能避免出現三代以外的繼承關系,否則應考慮合成/聚合”

“合成”與“聚合”從字面意義上去了解是我一直以來都無法正确了解的内容。可能是我國文水準實在太低的緣故吧,對 Composite 和 Aggregation 兩個單詞我反而能在維基百科上找到準确的定義。

合成  ( Composite )   - 值聚合 (Aggregation by value)

我的通俗定義:合成的過程是在類的構造過程中(構造函數或外部的構造方法)在運作期将值或其它類執行個體組裝到合成類内(通過變量或屬性Hold住)

如:

public class Keyboard{}
public class Mouse{}
public class Monitor{}
 
public class Computer
{
    private Keyboard keyboard;
    private Mouse mouse;
    private Monitor monitor;
 
    public Computer() 
    {
         this.keyboard=new Keyboard();
         this.mouse=new Mouse();
         this.monitor=new Monitor();
    }
}      

由這個例子可見,所謂的“值(Value)”通過構造函數合成為 “Computer”的内部成員,有如将各個功能單一的部件裝配成為一個功能強大的産品。所有的依賴都被“關在”構造函數内,如果将依賴外置就可以運用工廠(Factory Pattern)和合成模式(Composite Pattern)進行演變。

public class Item{};
 
public class Keyboard:Item{}
public class Mouse:Item {}
public class Monitor:Item{}
public ComputerFactory 
{
   public Item Keyboard() { return new Keyboard(); }
   public Item Monitor() { return new Monitor(); }
   public Item Mouse() { return new Mouse(); }
}
 
public class Computer
{
    public List<Item> Items{get;set;}
    
    public Computer(ComputerFactory factory) 
    {
        this.Items.Add(factory.Keyboard());
        this.Items.Add(factory.Mouse());
        this.Items.Add(factory.Monitor()); 
    }
}       

通過簡單的演變,就可以将Computer 1-3的耦合變成 1-1 的耦合,所有的依賴都集中到ComputerFactory上,隻需要繼承ComputerFactory建立更多的工廠傳入Computer類就可以生産出各種各樣的Computer執行個體,而無需更改Computer的任何代碼,這就是所謂的“黑箱複用”。

思考:試試用Builder模式改寫上面的例子,會有不同的效果。

這裡隻是有3個部件,但如果将部件變成30個或者更多時改變的也隻是 “合成的構造者” ,應對再複雜的場景:樹型合成結構也隻是将構造者演變為遞歸式構造。由此可見到“合成”原則的運作對大量類組合的強大之處。

聚合  ( Aggregation )  - 引用聚合(Aggregation by reference)

聚合在面向對象的實作上是一個極為簡單的代碼,說白了就是:對象屬性。以上面第一個範例說明 (不繼承Item基類)

public class Computer
{
   public Mouse Mouse{ get;set; }
   public Monitor Monitor{ get; set; }
   public Keyboard Keyboard {get;set;}
}
 
public class Host
{
    public static void Main()
    {
         var computer=new Computer()
         {
              Mouse=new Mouse(),
              Monitor=new Monitor(),
              KeyBoard=new KeyBoard()
         };
    }
}      

  聚合類中Hold住的是執行個體化類的引用,不單是值。聚合類的意義在于将引用依賴集中一處,從某意義上說這個Computer類也是一個Facade 模式。這種方式常見于大規模對象模型的入口類,如Office的 Application 對象,這樣的設計可以便于開發者“尋找”類的引用。同時也可以用作 上下文的設計 如:.net中的System.Web.HttpContext。值得注意的是:聚合類是需要慎用的,對于類本身是收斂類引用耦合,同時聚合類也具有耦合傳導的特性,由其是構造函數。就拿EF說事吧,我們用EF通路資料庫都需要這樣的代碼:

public void OrderManager
{
    public List<Order> GetOrder()
    {
        using (var ctx=new DbContext( )
        {
            //…
        }
    }
}      

當這個new 在代碼各處出現時就壞菜了!構造引用的耦合度每調用一次就增加一分,當遍布整個通路層甚至系統時 DBContext就是一個不可變更的超巨型耦合惡性良性腫瘤!要解決這個問題可以采用單件模式自構造或是用IoC、DI将構造移到一個集中的地方,防止構造耦合散播。 

小結

  如果你是一位.net 體系的開發人員,隻要你打開vs.net的代碼檢查規則你就會發現一個新的世界,一個基于原則/規範 的世界,如果你的代碼能80%地通過vs.net中最進階别的代碼檢查準則,那麼事實上你的代碼已經是非常優質。内中的每一條代碼檢查準則都值得我們細細地去品味與學習。

繼續閱讀