面向對象設計原則


目錄

  • 一、開放封閉原則
  • 二、里式替換原則
  • 三、依賴倒置原則
  • 四、接口隔離原則
  • 五、單一職責原則

一、開放封閉原則

概念理解

開放封閉原則是指在進行面向對象設計中,設計類或者程序應該遵循兩點:對擴展開放和對修改關閉。這樣,一個模塊在實現的過程中,就可以在不修改原來的模塊(修改關閉)基礎上,擴展器功能(擴展開放)。

 

  • 擴展開放。指的是某個模塊的功能是可擴展的,則該模塊是擴展開放的。軟件系統的功能上的可擴展性要求模塊是擴展開放的。
  • 修改關閉。指的是某模塊被其他模塊調用,如果該模塊的源代碼不允許修改,則該模塊修改關閉的。軟件系統的功能上的穩定性,持續性要求模塊是修改關閉的。

開閉原則的實現方法

為了滿足開閉原則的對修改關閉,對擴展開放原則,應該對軟件系統中的不變的部分加以抽象

實現原則:

  • 把不變的部分抽象成不變的接口,這些不變的接口可以應對未來的擴展。在python中可以使用abc模塊來建立抽象類,以及對應的接口。
  • 接口的最小功能設計原則。根據這個原則,原有的接口要么可以應對未來的擴展,不足的部分可以通過定義新的接口來實現。
  • 模塊之間的調用通過該抽象接口進行,這樣即使實現層發生變化,也無需修改調用方的代碼。

優點:

  • 提高系統的可復用性。
  • 提高系統的可維護性。

開閉原則的相對性

軟件系統的構建是一個需要不斷重構的過程,在這個過程中,模塊的功能抽象,模塊與模塊之間的關系,都不會從一開始就非常清晰明了。所以構建100%滿足開閉原則的軟件系統是非常困難的,這就是開閉原則的相對性。

但是在設計過程中,通過對模塊功能的抽象(接口定義),模塊之間的關系的抽象(通過接口調用),抽象與實現的分離(面向接口的程序設計)等,可以盡量接近滿足開閉原則。

二、里式替換原則

所有引用父類的地方必須能夠透明地使用其子類的對象。

概念理解

只要父類能出現的地方子類就可以出現,而且替換為子類也不會產生任何錯誤或異常,使用者不需要知道是父類還是子類。但是,反過來就不行了,有子類出現的地方,父類未必就能適應。

滿足條件:

  • 不應該在代碼中出現if/else之類對子類類型進行判斷的條件(因為父類出現的地方,子類就可以出現,因此不需要判斷)。
  • 子類應當可以替換父類,並出現在父類能夠出現的任何地方,代碼也能正常工作。

體現原則:

  • 類的繼承原則。如果用子類替換掉父類之后,出現運行錯誤,那么該子類不應該繼承父類。或者,應該重新設計它們之間的關系。
  • 動作正確性保證。這樣,從另一個方方面保證了凡是符合LSP原則設計的類的擴展,不會對已有的系統引入新的錯誤。

里式替換原則為我們是否應該使用繼承提供了判斷的依據,不再是簡單地根據兩者之間是否有相同之處來決定是否使用繼承。

里式替換原則的引申意義:子類可以擴展父類的功能,但是不能改變父類原有的功能。

表現幾點:

  • 子類可以實現父類的抽象方法,但是不能覆蓋父類的非抽象方法。
  • 子類可以增加自己特有的方法。
  • 當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬松。
  • 當子類的方法實現父類的抽象方法時,方法的后置條件(即方法的返回值)要比父類更嚴格。

優點:

  • 約束繼承泛濫,是開閉原則的一種體現。
  • 加強程序的健壯性,同時變更時也可以做到非常好地提高程序的維護性、擴展性。降低需求變更時引入的風險。
class User:
    def show_name(self):
        pass

classVIPUser(user):
    def show_name(self):
        pass

def show_user(u):
    res = u.show_name()
        
VIPUser這個類繼承了User,它重寫了父類的show_name,這個時候就必須保證子類的show_name方法的參數和返回值。不然當使用show_user方法調用的時候會出現問題。
示例

重構違反LCP的設計:

如果兩個具體的類A,B的關系違反了LSP的設計,(假設是從B到A的繼承關系),那么根據具體的情況可以在下面的兩種重構方案中選擇一種:

  • 創建一個新的抽象類C,作為兩個具體類的父類。然后將A、B的共同行為移動到C中來解決問題。
  • 從B到A的繼承關系更改為關聯關系。

在進行設計的時候,我們盡量從抽象類繼承,而不是從具體類繼承。

如果從繼承等級樹來看,所有葉子結點應該是具體類,而所有樹枝節點應該是抽象類或者接口。當然這只是一個一般性的指導原則,使用的時候還要具體情況具體分析。

在很多情況下,在設計初期我們類之間的關系不是很明確,LSP則給了我們一個判斷和設計類之間關系的基准:需不需要繼承,以及怎樣設計繼承關系。

三、依賴倒置原則

高層模塊不應該依賴於底層模塊,二者都應該依賴於抽象。

抽象不應該依賴於細節,細節應該依賴於抽象。

針對接口編程,不要針對實現編程。

概念理解

依賴:在程序設計中,如果一個模塊a調用了另一個模塊b,我們稱模塊a依賴模塊b。

高層模塊和底層模塊:在一個應用程序中,我們有一些低層次的類,這些類實現了一些基本的或者初級的操作,我們稱之為底層模塊,另外有一些高層次的類,這些類封裝了某些復雜的邏輯,並且依賴於低層次的類,這些類我們稱之為高層模塊。

依賴倒置:面向對象程序設計相對於面向過程程序設計而言,依賴關系被倒置了。因為傳統的結構化程序設計中,高層模塊總是依賴於底層模塊。

問題提出:
Robert C. Martin在書中給出了Bad Design的幾個特征:

  • 系統很難改變,因為每個改變都會影響其他很多部分。
  • 當你對某個地方做一個修改的時候,系統的看似無關的其他部分都不工作了。
  • 系統很難被另外一個應用重用,因為很難將要重用的部分從系統中分離出來。

這其中導致Bad Design的一個很大的原因是高層模塊過分依賴底層模塊。

但是一個良好的設計應該是系統的每個部分都是可替換的。如果高層模塊過分依賴底層模塊,一方面一旦底層模塊需要替換或者修改,高層模塊將受到影響,另外一方面,高層模塊很難可以重用。

問題的解決:
Robert C. Martin提出了Dependency Inversion Principle (DIP) 原則。

DIP給出了一個解決方案:在高層模塊與底層模塊之間,引入一個抽象接口層。

High Level Classes(高層模塊) --> Abstraction Layer(抽象接口層) --> Low Level Classes(低層模塊)。

抽象接口是對底層模塊的抽象,低層模塊繼承或者實現該抽象接口。

這樣高層模塊不直接依賴低層模塊,而是依賴抽象接口層。抽象接口也不依賴低層模塊的實現細節,而是低層模塊依賴(繼承或實現)抽象接口。

類與類之間都是通過抽象接口層來建立關系的。

怎么使用依賴倒置原則

1、依賴於抽象

  • 任何變量都不應該持有一個指向具體類的指針或引用。
  • 任何類都不應該從具體類派生。

2、設計接口而非設計實現

  • 使用繼承避免對類的直接綁定。
  • 抽象類/接口:傾向於較少的變化,抽象是關鍵點,它易於修改和擴展,不要強制修改那些抽象接口/類。

例外:

有些類不可能變化,在可以直接使用具體類的情況下,不需要插入抽象層,如字符串。

3、避免傳遞依賴

  • 避免高層依賴底層。
  • 使用基層和抽象類來有效消除傳遞依賴。

優點:

  • 減少類之間的耦合性
  • 提高系統的穩定性
  • 提高代碼可讀性和可維護性
  • 降低修改程序所造成的風險

四、接口隔離原則

不能強迫用戶依賴那些它們不適用的接口。

概念理解

換句話說,使用多個專門的接口比使用單一的總接口要好。

  • 接口的設計原則:接口的設計應該遵循最小接口原則,不要把用戶不使用的方法放在同一個接口里。如果一個接口的方法沒有被使用到,則說明該接口過胖,應該將其分割成幾個功能專一的接口。
  • 接口的依賴(繼承)原則:如果一個接口a繼承另外一個接口b,則接口a相當於繼承了接口b的方法,那么繼承了接口b后接口a也應該遵循上述原則:不應該包含用戶不使用的方法。反之,則說明接口a被b給污染了,應該重新設計它們的關系。

接口隔離原則指導我們:

  • 一個類對一個類的依賴應該建立在最小的接口上
  • 建立單一接口,不要建立龐大臃腫的接口。
  • 盡量細化接口,接口中的方法盡量少。

下面我們舉例說明怎么設計接口或類之間的關系,使其不違反ISP原則。

假如有一個Door,有lock,unlock功能,另外,可以在Door上安裝一個Alarm而使其具有報警功能。用戶可以選擇一般的Door,也可以選擇具有報警功能的Door。

ISP原則的違反例一:在Door接口里定義所有的方法。

但這樣一來,依賴Door接口的CommonDoor卻不得不實現未使用的alarm()方法。違反了ISP原則。

ISP原則的違反例二:在Alarm接口定義alarm方法,在Door接口定義lock,unlock方法,Door接口繼承Alarm接口。

跟方法一一樣,依賴Door接口的CommonDoor卻不得不實現未使用的alarm()方法。違反了ISP原則。

遵循ISP原則的例一:通過多重繼承實現

在Alarm接口定義alarm方法,在Door接口定義lock,unlock方法。接口之間無繼承關系。CommonDoor實現Door接口,AlarmDoor有2種實現方案:

1)同時實現Door和Alarm接口。

2)繼承CommonDoor,並實現Alarm接口。

第2)種方案更具有實用性。

這樣的設計遵循了ISP設計原則。

遵循ISP原則的例二:通過關聯(或者叫做組合)實現

在這種方法里,AlarmDoor實現了Alarm接口,同時把功能lock和unlock委讓給CommonDoor對象完成。

這種設計遵循了ISP設計原則。

接口分隔原則的優點和適度原則

  • 接口分隔原則從對接口的使用上為我們對接口抽象的顆粒度建立了判斷基准:在為系統設計接口的時候,使用多個專門的接口代替單一的胖接口。
  • 符合高內聚低耦合的設計思想,從而使得類具有很好的可讀性、可擴展性和可維護性。
  • 注意適度原則,接口分隔要適度,避免產生大量的細小接口。

五、單一職責原則

如果一個類需要改變,改變它的理由永遠只有一個。如果存在多個改變它的理由,就需要重新設計該類。也就一個類只做一件事。

只能讓一個類/接口/方法有且僅有一個職責。

如果一個類具有一個以上的職責,那么就會有多個不同的原因引起該類變化,而這種變化將影響到該類不同職責的使用者(不同用戶):

  • 一方面,如果一個職責使用了外部類庫,則使用另外一個職責的用戶卻也不得不包含這個未被使用的外部類庫。
  • 另一方面,某個用戶由於某個原因需要修改其中一個職責,另外一個職責的用戶也將受到影響,他將不得不重新編譯和配置。

這違反了設計的開閉原則,也不是我們所期望的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM