1. 程式人生 > >Python6大設計原則

Python6大設計原則

tails 邏輯 成本 單獨 根據 職責 理解 java 預測

內容總覽

六大設計原則都有哪些

一、單一職責原則
二、裏氏替換原則
三、依賴倒置原則
四、接口隔離原則
五、迪米特法則
六、開放封閉原則

內容詳解

一、單一職責原則

單一職責原則:英文名稱是Single Responsiblity Principle,簡稱是SRP。定義:應該有且僅有一個原因引起類的變更。

單一職責原則要求:一個接口或類只有一個原因引起變化,也就是一個接口或類只有一個職責,它就負責一件事情。

單一職責原則的好處:

  1. 類的復雜性降低,實現什麽職責都有清晰明確的定義;
  2. 可讀性提高,復雜性降低,那當然可讀性提高了;
  3. 可維護性提高,可讀性提高,那當然更容易維護了;
  4. 變更引起的風險降低,變更是必不可少的,如果接口的單一職責做得好,一個接口修改只對相應的實現類有影響,對其他的接口無影響,這對系統的擴展性、維護性都有非常大的幫助。

註意: 單一職責原則提出了一個編寫程序的標準,用“職責”或“變化原因”來衡量接口或類設計得是否優良,但是“職責”和“變化原因”都是不可度量的,因項目而異,因環境而異。

對於單一職責原則,接口一定要做到單一職責,類的設計盡量做到只有一個原因引起變化。

二、裏氏替換原則

裏氏替換原則(Liskov Substitution Principle,LSP),有兩種定義:

  • 第一種定義,也是最正宗的定義:If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T ,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.(如果對每一個類型為S的對象o1,都有類型為T的對象o2,使得以T定義的所有程序P在所有的對象o1都代換成o2時,程序P的行為沒有發生變化,那麽類型S是類型T的子類型。)
  • 第二種定義:Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.(所有引用基類的地方必須能透明地使用其子類的對象。)

第二個定義是最清晰明確的,通俗點講,只要父類出現的地方子類就可以出現,而且替換為子類也不會產生任何錯誤或異常,使用者可能根本就不需要知道父類還是子類。但是反過來就不行了,有子類出現的地方,父類未必就能適應。

裏氏替換原則為良好的繼承定義了一個規範,一句簡單的定義包含了4層含義:

  1. 子類必須完全實現父類的方法;
    1. 在類中調用其他類時務必要使用父類或接口,如果不能使用父類或接口,則說明類的設計已經違背了LSP原則;
    2. 如果子類不能完整地實現父類的方法,或者父類的某些方法在子類中已經發生“畸變”,則建議斷開父子繼承關系,采用依賴、聚集、組合等關系代替繼承。
  2. 子類可以有自己的個性;
  3. 覆蓋或實現父類的方法時輸入參數可以被放大;
    1. 如果父類的輸入參數類型大於子類的輸入參數類型,會出現父類存在的地方,子類未必會存在,因為一旦把子類作為參數傳入,調用者很可能進入子類的方法範疇;
    2. 子類中方法的前置條件必須與超類中被覆寫的方法的前置條件相同或者更寬松。
  4. 覆寫或實現父類的方法時輸出結果可以被縮小。
    1. 父類的一個方法的返回值是一個類型T,子類的相同方法(重載或覆寫)的返回值為S,那麽裏氏替換原則就要求S必須小於等於T,也就是說,要麽S和T是同一個類型,要麽S是T的子類。

采用裏氏替換原則的目的就是增強程序的健壯性,版本升級時也可以保持非常好的兼容性。即使增加子類,原有的子類還可以繼續執行。

三、依賴倒置原則

  依賴倒置原則(Dependence Inversion Principle,DIP),原始定義是:High level modules should not depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.

包含三層含義:

  1. 高層模塊不應該依賴底層模塊,兩者都應該依賴抽象;
  2. 抽象不應該依賴細節;
  3. 細節應該依賴抽象。

  高層模塊和低層模塊容易理解,每一個邏輯的實現都是由原子邏輯組成的,不可分割的原子邏輯就是底層模塊,原子邏輯的再組裝就是高層模塊。

在Java語言中,抽象就是指接口或抽象類,兩者都是不能直接被實例化的;細節就是實現類,實現接口或繼承抽象類而產生的類就是細節,其特點就是可以直接被實例化,也就是可以加上一個關鍵字new產生一個對象。依賴倒置原則在Java語言中的表現就是:

  1. 模塊間的依賴通過抽象發生,實現類之間不發生直接的依賴關系,其依賴關系是通過接口或抽象類產生的;
  2. 接口或抽象類不依賴於實現類;
  3. 實現類依賴接口或抽象類。

  更加精簡的定義就是“面向接口編程”--OOD(Object-Oriented Design,面向對象設計)的精髓之一。

依賴的三種寫法:

  依賴是可以傳遞的,A對象依賴B對象,B又依賴C,C又依賴D。。。。。。生生不息,依賴不止,記住一點:只要做到抽象依賴,即使是多層的依賴傳遞也無所畏懼!

  對象的依賴關系有三種方式來傳遞:

  1. 構造函數傳遞依賴對象 --- 在類中通過構造函數聲明依賴對象,按照依賴註入的說法,這種方式叫做構造函數註入。
  2. Setter方法傳遞依賴對象 --- 在抽象中設置Setter方法聲明依賴關系,依照註入的說法,這是Setter依賴註入。
  3. 在接口的方法中聲明依賴對象,這種方式也叫做接口註入。

  依賴倒置原則的本質就是通過抽象(接口或抽象類)使各個類或模塊的實現彼此獨立,不互相影響,實現模塊間的松耦合,我們怎麽在項目中使用這個規則呢?只要遵循以下的規則就可以:

  1. 每個類盡量都有接口或抽象類,或者抽象類和接口兩者都具備; --- 這是依賴倒置的基本要求,接口或抽象類都是屬於抽象的,有了抽象才可能依賴倒置。
  2. 變量的表面類型盡量是接口或者是抽象類;
  3. 任何類都不應該從具體類派生;
  4. 盡量不要覆寫基類的方法; --- 如果基類是一個抽象類,而且這個方法已經實現了,子類盡量不要覆寫。類間依賴的是抽象,覆寫了抽象方法,對依賴的穩定性會產生一定的影響。
  5. 結合裏氏替換原則使用。 --- 接口負責定義public屬性和方法,並且聲明與其他對象的依賴關系,抽象類負責公共構造部分的實現,實現類準確的實現業務邏輯,同時在適當的時候對父類進行細化。  

四、接口隔離原則

  接口分為兩種:

  1. 實例接口(Object Interface),在Java中聲明一個類,然後用new關鍵字產生一個實例,它是對一個類型的食物的描述,這是一種接口。
  2. 類接口(Class Interface),Java中經常使用的interface關鍵字定義的接口;
  3. Clients should not be forced to depend upon interfaces that they don‘t use。(客戶端不應該依賴它不需要的接口。)
  4. The dependency of one class to another one should depend on the smallest possible interface。(類間的依賴關系應該建立在最小的接口上。)

接口隔離原則是對接口進行規範約束,其包含以下4層含義:

  1. 接口要盡量小; --- 這是接口隔離原則的核心定義,不出現臃腫的接口(Fat Interface),但是“小”是有限度的,首先就是不能違反單一職責原則。
  2. 接口要高內聚; --- 高內聚就是提高接口、類、模塊的處理能力,減少對外的交互。在接口中盡量少公布public方法,接口是對外的承諾,承諾越少對系統的開發越有利,變更的風險也就越少,同時也越有利於降低成本。
  3. 定制服務; --- 一個系統或系統內的模塊之間必然會有耦合,有耦合就要有相互訪問的接口(並不一定就是Java中定義的Interface,也可能是一個類或單純的數據交換),我們設計時就需要為各個訪問者(即客戶端)定制服務。定制服務就是單獨為一個個體提供優良的服務。我們在做系統設計時也需要考慮對系統之間或模塊之間的接口采用定制服務。采用定制服務就必然有一個要求:只提供訪問者需要的方法。
  4. 接口設計是有限度的。 --- 接口的設計粒度越小,系統越靈活,這是不爭的事實。但是,靈活的同時也帶來了結構的復雜化,開發難度增加,可維護性低,這不是一個項目或產品所期望看到的,所以接口設計一定要註意適度,這個“度”如何來判斷?根據經驗和常識判斷,沒有一個固話或可測量的標準。

  接口隔離原則是對接口的定義,同時也是對類的定義,接口和類盡量使用原子接口或原子類來組裝。但是,這個原子該怎麽劃分是設計模式中的一大難題,在實踐中可以根據以下幾個規則來衡量:

  1. 一個接口只服務於一個模塊或業務邏輯;
  2. 通過業務邏輯壓縮接口中的public方法,接口時常去回顧,盡量讓接口達到“滿身筋骨肉”,而不是“肥嘟嘟”的一大堆方法;
  3. 已經被汙染了的接口,盡量去修改,若變更的風險較大,則采用適配器模式進行轉化處理;
  4. 了解環境,拒絕盲從。每個項目或產品都有特定的環境因素,別看到大師是這樣做的你就照抄。千萬別,環境不同,接口拆分的標準就不同。深入了解業務邏輯,最好的接口設計就出自你的手中!

五、迪米特法則

  迪米特法則(Law of Demeter ,LoD)也稱為最少知識原則(Least Knowledge Principle,LKP),雖然名字不同,但描述的是同一個規則:一個對象應該對其他對象有最少的了解。通俗地講,一個類應該對自己需要耦合或調用的類知道得最少,你(被耦合或調用的類)的內部是如何復雜都和我沒關系,那是你的事情,我就知道你提供的這麽多public方法,我就調用這麽多,其他的我一概不關心。

  迪米特法則對類的低耦合提出了明確的要求,其包含以下4層含義:

  1. 只和朋友交流 --- 迪米特法則還有一個英文解釋是:Only talk to your immedate friends(只與直接的朋友通信。)什麽叫做直接的朋友?每個對象都必然會與其他對象有耦合關系,兩個對象之間的耦合就成為朋友關系,這種關系的類型有很多,例如組合、聚合、依賴等。朋友類的定義:出現在成員變量、方法的輸入輸出參數中的類稱為成員朋友類,而出現在方法體內部的類不屬於朋友類。註意:一個類只和朋友交流,不與陌生類交流,不要出現getA().getB().getC().getD()這種情況(在一種極端的情況下允許出現這種訪問,即每一個點後面返回類型都相同),類與類之間的關系是建立在類間的,而不是方法間,因此一個方法盡量不引入一個類中不存在的對象,當然,JDK API提供的類除外。
  2. 朋友間也是有距離的 --- 一個類公開的public屬性或方法越多,修改時涉及的面積越大,變更引起的風險擴散也就越大。因此,為了保持朋友類間的距離,在設計時需要反復衡量:是否還可以再減少public方法和屬性,是否可以修改為private、package-private(包類型,在類、方法、變量前不加訪問權限,則默認為包類型)、protected 等訪問權限,是否可以加上final關鍵字等。 註意:迪米特法則要求類“羞澀”一點,盡量不要對外公布太多的public方法和非靜態的public變量,盡量內斂,多使用private、package-private、protected等訪問權限。
  3. 是自己的就是自己的 --- 如果一個方法放在本類中,即不增加類間的關系,也對本類不產生負面影響,就放置在本類中。
  4. 謹慎使用Serializable

  迪米特法則的核心觀念就是類間解耦,弱耦合,只有弱耦合了以後,類的復用率才可以提高。其要求的結果就是產生了大量的中轉或跳轉類,導致系統的復雜性提高,同時也為維護帶來了難度。

  迪米特法則要求類間解耦,但解耦是有限度的,除非是計算機的最小單元——二進制的0和1。那才是完全解耦,在實際的項目中,需要適度地考慮這個原則,別為了套用原則而做項目。原則只是供參考,如果違背了這個原則,項目也未必會失敗,這就需要大家在采用原則時反復度量,不遵循是不對的,嚴格執行就是“過猶不及”。

六、開放封閉原則

  開放封閉原則的定義:Software entities like classes, modules and functions should be open for extension but closed for modifications.(一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉。)其含義是說一個軟件實體應該通過擴展來實現變化。軟實體包括以下幾個部分:

  1. 項目或軟件產品中按照一定的邏輯規則劃分的模塊;
  2. 抽象和類;
  3. 方法。

  一個軟件產品只要在生命周期內,都會發生變化,既然變化是一個既定的事實,我們就應該在設計時盡量適應這些變化,以提高項目的穩定性和靈活性,真正實現“擁抱變化”。開放封閉原則告訴我們應盡量通過擴展軟件實體的行為來實現變化,而不是通過修改已有的代碼來完成變化,它是為軟件實體的未來事件而制定的對現行開發設計進行約束的一個原則。

  開放封閉原則對擴展開放,對修改封閉,並不意味著不做任何修改,低層模塊的變更,必然要有高層模塊進行耦合,否則就是一個孤立無意義的代碼片段。我們可以把變化歸納以下三種類型:

  1. 邏輯變化; -- 只變化一個邏輯,而不涉及其他模塊,比如原有的一個算法是 a*b+c ,現在需要修改為 a*b*c ,可以通過修改原有類中的方法的方式來完成,前提條件是所有依賴關系都按照相同的邏輯處理。
  2. 子模塊變化; ---一個模塊變化,會對其他的模塊產生影響,特別是一個低層次的模塊變化必然引起高層次模塊的變化,因此在通過擴展完成變化時,高層次的模塊修改是必然的。
  3. 可見視圖變化 --- 可見視圖是提供給客戶使用的界面,如JSP程序、Swing界面等,該部分的變化一般會引起連鎖反應(特別是在國內做項目,做歐美的外包項目一般不會影響太大)。如果僅僅是界面上按鈕、文字的重新排列倒是簡單,最司空見慣的是業務耦合變化,什麽意思?一個展示數據的列表,按照原有的需求是6列,突然有一天要增加1列,而且這一列要跨N張表,處理M個邏輯才能展現出來,這樣的變化是比較恐怖的,但還是可以通過擴展來完成變化,這就要看我們原有的設計是否靈活。

為什麽要采用開放封閉原則:

  每個事物的誕生都有它存在的必要性,存在即合理,那開閉原則的存在也是合理的,為什麽這麽說呢?

  首先,開閉原則是那麽地著名,只要是做面向對象編程的,甭管是什麽語言,Java也好,C++也好,或者是Smalltalk,在開發時都會提及開閉原則。

  其次,開閉原則是最基礎的一個原則,前面介紹的原則都是開閉原則的具體形態,也就是說前五個原則就是指導設計的工具和方法,而開閉原則才是其精神領袖。換一個角度理解,依照Java語言的稱謂,開閉原則是抽象類,其他五大原則是具體的實現類,開閉原則在面向對象設計領域中的地位就類似於牛頓第一定律在力學、勾股定律在幾何學、質能方程在狹義相對論中的地位,其地位無人能及。

  最後,開閉原則是非常重要的,可通過以下幾個方面來理解其重要性。

  1、開閉原則對測試的影響

  所有已經投產的代碼都是有意義的,並且都受系統規則的約束,這樣的代碼都要經過“千錘百煉”的測試過程,不僅保證邏輯是正確的,還要保證苛刻條件(高壓力、異常、錯誤)下不產生“有毒代碼(Poisonous Code)”,因此有變化提出時,我們就需要考慮一下,原有的健壯代碼是否可以不修改,僅僅通過擴展實現變化呢?否則,就需要把原有的測試過程回籠一遍,需要進行單元測試、功能測試、集成測試甚至是驗收測試,現在雖然在大力提倡自動化測試工具,但是仍然代替不了人工的測試工作。

  2、 開閉原則可以提高復用性

  在面向對象的設計中,所有的邏輯都是從原子邏輯組合而來的,而不是在一個類中獨立實現一個業務邏輯。只有這樣代碼才可以復用,粒度越小,被復用的可能性就越大。那為什麽要復用呢?減少代碼量,避免相同的邏輯分散在多個角落,避免日後的維護人員為了修改一個微小的缺陷或增加新功能而要在整個項目中到處查找相關的代碼,然後發出對開發人員“極度失望”的感概。那怎麽才能提高復用率呢?縮小邏輯粒度,直到一個邏輯不可再拆分為止。

  3、開閉原則可以提高可維護性

  一款軟件投產後,維護人員的工作不僅僅是對數據進行維護,還可能要對程序進行擴展,維護人員最樂意做的事情就是擴展一個類,而不是修改一個類,甭管原有的代碼寫得多麽優秀還是多麽糟糕,讓維護人員讀懂代碼原有的代碼,然後再修改,是一件很痛苦的事情,不要讓他在原有的代碼海洋裏遊戈完畢後再修改,那是對維護人員的一種折磨和摧殘。

  4、面向對象開發的要求

  萬物皆對象,我們需要把所有的事物都抽象成對象,然後針對對象進行操作,但是萬物皆運動,有運動就有變化,有變化就要有策略去應對,怎麽快速應對呢?這就需要在設計之初考慮到所有可能變化的因素,然後留下接口,等待“可能”轉變為“現實”。

如何使用開閉原則

  1、抽象約束

  抽象是對一組事物的通用描述,沒有具體的實現,也就表示它可以有非常多的可能性,可以跟隨需求的變化而變化。因此,通過接口或抽象類可以約束一組可能變化的行為,並且能夠實現對擴展開放,其包含三層含義:第一,通過接口或抽象類約束擴展,對擴展進行邊界限定,不允許出現在接口或抽象類中不存在的public方法;第二,參數類型,引用對象盡量使用接口或者抽象類,而不是實現類;第三,抽象層盡量保持穩定,一旦確定即不允許修改。

  2、元數據(metadata)控制模塊行為

  編程時使用元數據來控制程序的行為,減少重復開發。用來描述環境和數據的數據,通俗地說就是配置參數,參數可以從文件中獲得,也可以從數據中庫中獲得。

  3、制定項目章程

  在一個團隊中,建立項目章程是非常重要的,因為章程中指定了所有人員都必須遵守的約定,對項目來說,約定優於配置。相信大家都做過項目,會發現一個項目會產生非常多的配置文件。以SSH項目開發為例,一個項目中Bean配置文件就非常多,管理非常麻煩。如果需要擴展,就需要增加子類,並修改SpringContext文件。然而,如果你在項目中指定這樣一個章程:所有的Bean都自動註入,使用Annotation進行裝配,進行擴展時,甚至只用寫一個類,然後由持久層生成對象,其他的都不需要修改,這就需要項目內約束,每個項目成員都必須遵守,該方法需要一個團隊有較高的自覺性,需要一個較長時間的磨合,一旦項目成員都熟悉這樣的規則,比通過接口或抽象類進行的約束效率更高,而且擴展性一點也沒有減少。

  4、封裝變化

  對變化的封裝包含兩層含義:第一,將相同的變化封裝到一個接口或抽象類中;第二,將不同的變化封裝到不同的接口或抽象類中,不應該有兩個不同的變化出現在同一個接口或抽象類中。封裝變化,也就是受保護的變化(protected variations),找出預計有變化或不穩定的點,我們為這些變化點創建穩定的接口,準確地講是封裝可能發生的變化,一旦預測到或“第六感”發覺有變化,就可以進行封裝,23個設計模式都是從各個不同的角度對變化進行封裝的,我們會在各個模式中逐步講解。

Python6大設計原則