天天看點

SoftwareModel - 避免耦合的設計原則 如果代碼耦合太高,對于BUG修改,新功能增加,測試,都是一個噩夢。解耦合手段之一:DRY原則解耦合手段之二:Single Responsibility Principle  解耦合手段之三:Open/Closed Principle解耦合手段之四:Law of Demeter解耦合手段之五:Dependency Inversion Principle解耦合手段之六:并發解耦合手段之七:Liskov代換原則

如果代碼耦合太高,對于BUG修改,新功能增加,測試,都是一個噩夢。

我們公司還是有很多牛人的,知識我不知道而已,下面是我一同僚的部落格上的文章,關于解耦合手段,每讀一篇,我會把他連結到這裡。

解耦合手段之一:dry原則

Don't Repeat Yourself

CPD, PMD

解耦合手段之一:DRY原則

Posted on January 28, 2009 by terryyin 一直信誓旦旦的要總結一下軟體系統解耦合的手段。等到真的要寫了,才發現這可不是件容易的事情。所謂解耦合,也就是解開系統内各子產品間不必要或不合理的依賴關系,讓各個子產品内部結構緊湊,整體架構簡化進而易于了解和維護,友善擴充。目标就是“高内聚,低耦合”。軟體系統中的耦合,說遠了可能是軟體開發團隊的組織結構和管理造成的,說近了也可能是你鍵盤上的Ctrl+C、Ctrl+V造成的。我打算想到哪裡就寫到哪裡,一次寫一個方法。 首先想到的自然是DRY原則了。DRY原則,全稱是 Don’t Repeat Yourself 原則,有人翻譯做“一次且僅此一次”原則,其實就是“不要重複”。這句話也是著名的《程式員修煉之道》中的核心原則之一。 資訊不應重複。因為重複的資訊給将來的改動帶來困難,使得系統更複雜,并且可能導緻将來的不一緻性。比方說有一段代碼我寫好了,然後發現另外一個地方也要用,于是我就把它拷貝過去,結果就造成了代碼上的重複。等到下次我要改的時候就要改兩個地方。也可能下次不是我改了,而改的那個人不知道另一段相同内容、相同目的的代碼的存在,隻改了一個地方。比這個例子還要糟糕許多的真實情形每天都在我們的軟體開發組織裡發生着。 比方說我們常聽說的所謂“詳細設計文檔”往往就是對代碼的重複。一批又紅又專的程式員寫了代碼,然後又把代碼中所寫的内容記錄在文檔裡,雖然他們不知道為啥要這樣做(當然,他們也可能先寫的文檔,再寫代碼,那隻會更糟)。後來來了一批不那麼紅,不那麼專的程式員,改了代碼沒補文檔。再後來大家就不知道該信文檔還是該信代碼了。如果那批又紅又專的程式員知道, 代碼不光是寫給編譯器看的,也是寫給人看的,就不會把精力放在不該放的地方了。聽到這個,總會有更紅更專的老程式員跳出來大叫:“咄!不做設計就寫代碼,成何體統!”沒人說不讓你做設計,且不說萬惡的BDUF(Big Design Up-Front),就說簡單的設計吧,你把它寫在白闆上,畫在紙上不是也一樣麼。然後把它用代碼清晰的描述出來。最後别忘了把白闆擦了,把那張紙吃掉,省得留下來害人。 還有,測試文檔往往是對自動化測試腳本的重複;需求分析文檔往往是對驗收測試的重複。。。。。。 為了發現和剪除代碼中的重複,有專門的CPD(Copy-Paste Detector)工具可以用。我用過一個叫 PMD的開源軟體,很不錯,支援Java, C/C++, PHP。把它用在我們的一些代碼上,有時你會發現幾十處,每處上百行的重複代碼。我想,這足夠說明問題的了。 幾乎軟體中所有的問題都和重複有關,而軟體設計中用到的各種原則,最後其實也就是用來減少重複的。《Clean Code》裡面說:“Duplication may be the root of all evil in software.”這話說得可能大了點,看你如何來了解了。像我那位狡猾的以色列朋友在聽到有人誇他們猶太人都很有經濟頭腦時常說的: “All generalizations are wrong, including this one.”

SoftwareModel - 避免耦合的設計原則 如果代碼耦合太高,對于BUG修改,新功能增加,測試,都是一個噩夢。解耦合手段之一:DRY原則解耦合手段之二:Single Responsibility Principle  解耦合手段之三:Open/Closed Principle解耦合手段之四:Law of Demeter解耦合手段之五:Dependency Inversion Principle解耦合手段之六:并發解耦合手段之七:Liskov代換原則

解耦合手段之二:Single Responsibility Principle

在面向對象的方法中,Robert C. Martin引入了Single Responsibility Principle(SRP),即單一職責原則。就是說:所有的對象都應該有單一的職責,它提供的所有的服務也都僅圍繞着這個職責。用Uncle Bob自己的話來說就是:“永遠不要讓一個類存在多個改變的理由”(There should never be more than one reason for a class to change.)

解耦合手段之二:Single Responsibility Principle 

在面向對象的方法中,Robert C. Martin引入了Single Responsibility Principle(SRP),即單一職責原則。就是說:所有的對象都應該有單一的職責,它提供的所有的服務也都僅圍繞着這個職責。用Uncle Bob自己的話來說就是:“永遠不要讓一個類存在多個改變的理由”(There should never be more than one reason for a class to change.)

多個職責擠在一個類當中就是耦合。例如一個矩形類即要負責幾何計算,又要負責在圖形界面中畫出這個矩形,這就違反了SRP。為什麼這樣耦合性就高了呢?因為類内部的“内聚性”差了。如果一個類裡面所有的方法都用到了這個類所有的成員變量,我們說這個類的内聚性是最高的。如果是一些方法用到了一半的成員變量,而另一些方法用到了另一半,那麼這顯然是兩個不相幹的職責被擠到了一個類裡,這個類的内聚性是最差的。遵守SRP原則就能夠使我們提高類的内聚性,進而降低系統的耦合性。 那麼怎麼來定義職責呢?Uncle Bob的定義就是“改變的理由”。如果你能想到一個類存在多個使其改變的原因,那麼這個類就存在多個職責。例如上面提到的矩形類,要改變圖形界面中矩形的外觀要改動它,要改變矩形的幾何運算也要改到它。是以這個矩形類要被拆成兩個。 這樣就會使得每個類都很小,是的,就是要類很小。可能有人會擔心這麼多很小的類會不會用起來很麻煩。用個Uncle Bob的例子吧:你是希望工具箱裡隻有幾個大抽屜,所有的工具都放在一起,還是希望有很多小抽屜,上面都标好了裡面工具的名稱? 即使不是面向對象的語言,我們還是可以使用面向對象的思想,把資料和它所對應的操作關聯起來。函數,類,子產品以至于整個系統,這些不同層面代表了對業務邏輯不同層面的抽象。SRP從職責,或者是“改變的理由”的角度為類的抽象粒度提供的判斷的标準。 最後提前說一下,所謂SRP隻不過是偉大的Separation of Concerns原則的一個引伸原則,SoC才是一切解耦合的不二法門。等我把SoC的引伸原則一個一個的列舉完了,再來談它自己吧。

解耦合手段之三:Open/Closed Principle

這又是一個面向對向的方法。Open/Closed Principle,可以譯成“開/關原則”,“開-閉原則”或者“開放封閉原則”,是說“一個軟體實體應當對擴充開放,對修改關閉( Software entities should be open for extension,but closed for modification.)。”這裡所說的“實體”可以是函數、類、子產品等等。

解耦合手段之三:Open/Closed Principle

Posted on January 30, 2009 by terryyin 這又是一個面向對向的方法。Open/Closed Principle,可以譯成“開/關原則”,“開-閉原則”或者“開放封閉原則”,是說“一個軟體實體應當對擴充開放,對修改關閉( Software entities should be open for extension,but closed for modification.)。”這裡所說的“實體”可以是函數、類、子產品等等。 舉個我所見過的比較差的例子吧。我們做的産品是由很多單闆組成的分布式系統,為了适應不斷提高的資料傳輸要求,其接口闆經常要更新換代,不斷更新。每次有新的接口闆硬體以後,這塊闆上的幾個程式子產品都需要進行修改,甚至其它單闆上的程式也要改動很多處。這種改動往往是在函數級别上的。系統的這種高耦合性給我們帶來了很多麻煩,即費時又容易出問題。顯然,我們的系統設計一定程度上違反了OCP,因為這個系統的子產品都對修改開放,對擴充封閉。如果遵守了OCP,如果一段程式自身的功能沒有BUG的話是不會被打開修改的,并且如果接口沒有發生變化也不會對接口兩端都進行改動的。 還是在Uncle Bob的著作《Agile Software Development Principles, Patterns, and Practice》(PPP)中有對于這個原則的較長的描述。實作OCP原則,往往要用到繼承和抽象接口。 這篇文章 是和《PPP》中的内容一樣的。 通常如果我們隻是為了滿足眼前的功能所寫出的代碼是不會自然而然的滿足OCP和前面提到的SRP的,這就需要我們不斷的對代碼進行重構(Refactoring)。Martin Fowler在他的著作《重構-改善即有代碼的設計》(Refactoring Improving the Design of Existing Code)一書中講到了許多“Bad smells of code”——代碼中的壞味道。如果你“聞”到你的代碼中有他提到的“發散式變化(divergent change)”或者“散彈式修改(shotgun surgery)”那麼你的代碼可能違反了SRP或者OCP,需要重構。

解耦合手段之四:Law of Demeter

迪米特法則(Law of Demeter),又稱“最少知識原則”(Principle of Least Knowledge),也是主要針對面向對象思想的,可以簡單的概括為“talk only to your immediate friends”。

解耦合手段之四:Law of Demeter

Posted on January 31, 2009 by terryyin 迪米特法則(Law of Demeter),又稱“最少知識原則”(Principle of Least Knowledge),也是主要針對面向對象思想的,可以簡單的概括為“talk only to your immediate friends”。

具體來說,在面向對象的方法中,一個方法“M”和一個對象“O”隻可以調用以下幾種對象的方法:

1. O自己

2. M的參數

3. 在M中建立的對象

4. O的直接元件對象

Law of Demeter來源于1987年荷蘭大學的一個叫做Demeter的項目。Craig Larman把Law of Demeter又稱作“不要和陌生人說話”。在《程式員修煉之道》中講LoD的那一章叫作“解耦合與迪米特法則”。可以看出迪米特法則是非常流行的解耦合手段之一。關于迪米特法則有一些很形象的比喻:

如果你想讓你的狗狗跑的話,你會對狗狗說還是對四條狗腿說?

如果你去店裡買東西,你會把錢交給店員,還是會把錢包交給店員讓他自己拿? 讓店員自己從錢包裡拿錢?這聽起來有點荒唐,不過在我們的代碼裡這幾乎是見怪不怪的事情了: class Clerk {

Store store;

void SellGoodsTo(Client client) { money = client.GetWallet().GetMoney();//店員自己從錢包裡拿錢了!

store.ReceiveMoney(money);

} }; 在《Clean Code》一書中,作者找到了Apache framework中的一段違反了LoD的代碼: final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath(); 這麼長的一串對其它對象的細節,以及細節的細節,細節的細節的細節……的調用,違反了迪米特法則,增加了耦合,使得代碼結構複雜、僵化,難以擴充和維護。

在《重構》一書中的各種“Bad smells of code”中有一種“smell”叫做“Feature Envy”(依戀情結),形象的描述了一種違反了LoC的情況。Feature Envy就是說一個對象對其它對象的内容更有興趣,也就是說老是羨慕别的對象的成員、結構或者功能,大老遠的調用人家的東西。這樣的結構顯然是不合理的。我們的程式應該寫得比較“害羞”。不能像前面例子中的那個不把自己當外人的店員一樣,拿過客人的錢包自己把錢拿出來。“害羞”的程式隻和自己最近的朋友交談。這種情況下應該調整程式的結構,讓那個對象自己擁有它羨慕的feature,或者使用合理的設計模式(例如Facade和Mediator)。

店員的例子如果是這樣就會好一點: money = client.GetMoney();//客戶自己從錢包裡拿錢

或者根本不用那麼麻煩:

void SellGoods(Money money) { store.ReceiveMoney(money); }

這一法則不僅僅局限于計算機領域,在其他領域也同樣适用。據說美國人就在航天系統的設計中采用這一法則。

解耦合手段之五:Dependency Inversion Principle

Dependency Inversion,又被稱為控制反轉(Inversion of Control)。這種原則又被戲稱為“好萊塢原則”——“Don’t call us, we’ll call you.”。Uncle Bob是這樣描述DIP(Dependency Inversion Principle)的:

A. 高層子產品不應該依賴于低層子產品,二者都應該依賴于抽象。

B. 抽象不應該依賴于細節,細節應該依賴于抽象。

解耦合手段之五:Dependency Inversion Principle

Posted on February 4, 2009 by terryyin 通常,我們認為上層子產品理所當然的依賴于下層子產品的服務,例如業務子產品對資料庫的依賴,通信子產品對網絡的依賴。按照這種邏輯,如果要設計一款電視的話,也許會是這樣: class TVSet{ private Program program = LocateProgramFromRadiobBroadcast(); void Play() { Show(Program); } }; 因為電視依賴于節目源,是以電視自己建立了節目源,或者說自己來定位節目源。在上面的例子中是定位一個無線廣播節目。但這樣的電視有個問題,它隻能播廣播節目。生産廠商要對電視進行測試也要依賴于電視台的廣播。是以,我們的電視通常都不是這樣的,而是專門有接口,由使用者提供節目源: class TVSet{ void Play(Program program) { Show(Program); } }; 這樣,不論是測試用的簡單信号發生器,還是DVD,電腦都可以通過電視播放了。 這種依賴關系的倒置被稱為Dependency Inversion,又被稱為控制反轉(Inversion of Control)。這種原則又被戲稱為“好萊塢原則”——“Don’t call us, we’ll call you.”。Uncle Bob是這樣描述DIP(Dependency Inversion Principle)的:

A. 高層子產品不應該依賴于低層子產品,二者都應該依賴于抽象。

B. 抽象不應該依賴于細節,細節應該依賴于抽象。

依賴倒置使得系統的耦合性降低,子產品的可重用性提高,并能提高系統的可測性和可用性。尤其是對單元測試有至關重要的作用,因為這樣就可以用mock object來代替真實的對象。 實作依賴倒置的技術被稱為“依賴注入”(Dependency Injection)。通常,當一個子產品需要一種服務的時候,它要麼直接持有對此服務的索引,要麼通過一個服務定位器(ServiceLocator)來獲得對此服務的索引。而采用依賴注入的方法,則是從外界傳入對此服務的索引。例如通過傳入參數,或者回調函數來設定服務。 Martin Fowler把依賴注入的方法分為三類:

  • Interface injection
  • Setter injection
  • Constructor injection

甚至有很多架構工具可以支援我們友善的在代碼中實作Dependency Injection,例如google Guice。

解耦合手段之六:并發

一般的程式都是順序執行的,在這種情況下,程式的執行和執行的時間是互相依賴的或者說是耦合的。當這種耦合成為一種阻礙時我們就需要并發(Concurrency)。

"Concurrency is a decoupling strategy. It helps us decouple what gets done from when it gets done."

–Clean Code, Robert C. Martin

解耦合手段之六:并發

Posted on February 8, 2009 by terryyin 什麼?并發也是一種解耦合的手段麼? 是的。 一般的程式都是順序執行的,在這種情況下,程式的執行和執行的時間是互相依賴的或者說是耦合的。當這種耦合成為一種阻礙時我們就需要并發(Concurrency)。 "Concurrency is a decoupling strategy. It helps us decouple what gets done from when it gets done." – Clean Code, Robert C. Martin 一個很常見的錯誤觀點是并發總是能夠提高性能。這在大多數情況下,尤其是單核系統中往往是不成立的,就像1+1+1+1+1和1*4都等于4一樣。 我能想象到的采用并發的理由總結一下有以下幾點:

  • 解開what和when的耦合以後,程式的結構更容易被了解。例如用一個大的循環來處理所有使用者的聊天請求,就不如用不同的線程來處理每個使用者的請求那麼清晰,而且也更友善擴充對使用者的服務。
  • 需要提高響應輸入的速度,或及時的輸出中間結果。
  • 需要利用多核,多CPU,多個機算機甚至網絡的計算能力。

并發就需要用到多線程或者多程序。而在多線程或多程序程式設計的時候我們往往又需要或不得不把其它耦合性帶回到程式裡去。例如對共享資料的操作需要互斥,有些業務邏輯要求有順序,同時還要避免可能發生的死鎖。是以,結果可能是我們引入了更多的耦合性。例如前面提到的聊天程式。因為每個請求都很簡單,處理得很快,是以即使是上千使用者一起使用單一線程的服務也是感覺不到有什麼問題。然而采用多線程處理就要引入很多其它負載,反而可能降低性能或引入更多問題。但如果每個使用者的服務内容很複雜,單一線程又會變得很難了解或很難處理。 這個時候,合理的選擇串行和并發以及并發的手段就成為降低耦合性的關鍵。如果我們不得不選擇并發,那麼提高每個程序的内聚性仍是降低整體耦合性的有效手段。 現在有很多并發語言,例如傳統的電信行業中使用的SDL,還有新的語言如Erlang, Stackless Python。它們往往會提供高效的程序排程機制,友善的程序間通信手段以及合理的互斥方法。但無論使用什麼語言,在多麼先進的架構下工作,并發程式設計都是非常複雜的,并且和串行的程式設計所需要用到的設計政策往往是完全不同的。

解耦合手段之七:Liskov代換原則

子類不應要求比基類更高的precondition。

子類不應弱化基類的postcondition。

解耦合手段之七:Liskov代換原則

Posted on March 11, 2009 by terryyin 周二,美國計算機協會宣布,來自麻省理工學院的女教授芭芭拉·利斯科夫(Barbara Liskov)獲得本年度圖靈獎以及25萬美元的獎金,她的貢獻是讓計算機程式更加可靠、安全和易于使用。( 新聞) Barbara Liskov 是美國第一位獲得計算機博士學位的女性。她的研究為子產品化程式設計和面向對向程式設計的産生奠定了基礎。除此之外,Barbara的另一個為人們所熟知的供獻是她定義了 Liskov substitution principle ,我們且稱之為“Liskov代換原則”。它為子類型化或者說面向對向中的“繼承”定義了重要的原則。 Liskov代換原則的原文是挺難看懂的: Let q(x) be a property provable about objectsx of typeT. Thenq(y) should be true for objectsy of typeS whereS is a subtype ofT. 幸好有Bob大叔做好事,早就寫了 一篇相對容易了解一點的文章來解釋Liskov Substitution Principle:

使用指向基類(超類)的引用的函數,必須能夠在不知道具體派生類(子類)對象類型的情況下使用它們。

舉一個例子: 有一個基類Rectangle(矩形)。按常理來說,正方形(Square)也是一個矩形,那麼Square類可以從Rectangle類繼承而來。而事實是這樣的麼?假設有一個函數: void g(Rectangle& r) { r.SetWidth(5); r.SetHeight(4); assert(r.GetWidth() * r.GetHeight()) == 20); } 很顯然,這個函數對于傳入的基類Rectangle變量來講是正确的。然而如果傳如入的變量是Square類型就不正确了。是以,Square與Rectangle之間的繼承關系違反了Liskov代換原則。 那麼為什麼會這樣呢?從常識(或幾何定義)的角度來看,正方形是一個矩形;然而從行為的角度看,正方形不是一個矩形!因為正方形的行為與矩形有所不同。如果一定要讓正方形繼承于矩形,那麼正方形的行為必須修改矩形的行為。可以看出,這同時也違反了“ 開閉原則 ”。 Liskov代換原則與“契約式設計(Design by Contract)”也有很緊密的聯系。在Design by Contract的模式下對于一個類的方法有precondition(先決條件)和postcondition(後置條件)兩個概念。Precondition是為了使用這個方法而必須事先滿足的條件;Postcondition是這個方法必須保證它被使用以後成立的條件。由Liskov代換原則我們可以得到: .子類不應要求比基類更高的precondition。 .子類不應弱化基類的postcondition。 例如上面的Rectangle類裡SetWidth方法的一個postcondition是矩形的Height不應發生變化。這個postcondition在Square中就不能滿足。 發現中文維基中沒有Liskov代換原則的條目,把它補上: http://zh.wikipedia.org/wiki/Liskov代換原則

SoftwareModel - 避免耦合的設計原則 如果代碼耦合太高,對于BUG修改,新功能增加,測試,都是一個噩夢。解耦合手段之一:DRY原則解耦合手段之二:Single Responsibility Principle  解耦合手段之三:Open/Closed Principle解耦合手段之四:Law of Demeter解耦合手段之五:Dependency Inversion Principle解耦合手段之六:并發解耦合手段之七:Liskov代換原則

繼續閱讀