面向對象這個詞對大(dà)多數編程人(rén)員(yuán)應該都(dōu)很熟悉。面向對象編程是當今最主流的計(jì)算機(jī)編程範式,應用面向對象編程範式的語言有Dart,Java,Python,JavaScript等主流的編程語言,所以當你(nǐ)開始學習編程,幾乎不可(kě)避免的要與面向對象相(xiàng)遇。但(dàn)什麽是面向對象設計(jì)?

面向對象設計(jì) (OOD) 是應用面向對象編程範式 (OO) 創建軟件(jiàn)系統或應用程序的過程。一般的軟件(jiàn)開發流程中都(dōu)遵循三個階段,分(fēn)析,設計(jì)以及實現,面向對象編程範式也同樣分(fēn)爲三個階段,OOA(面向對象分(fēn)析),OOD(面向對象設計(jì)),OOP(面向對象編程)。


如(rú)你(nǐ)所知,OOD在整個軟件(jiàn)開發流程中起到一個設計(jì)以及規劃的作(zuò)用。而對軟件(jiàn)設計(jì)而言,好的設計(jì)可(kě)以有很多維度,但(dàn)糟糕的設計(jì)往往表現的很客觀。如(rú)果一款軟件(jiàn)滿足功能需求但(dàn)表現出以下三個特征中的之一或全部,那麽它的設計(jì)就(jiù)很糟糕。

  • 難以改變,因爲每一個更改都(dōu)會影(yǐng)響系統的許多其他(tā)部分(fēn)。(僵化)
  • 當您進行更改時,系統的意外部分(fēn)會崩潰。(脆弱)
  • 它很難在另一個應用程序中重用,因爲它無法與當前應用程序分(fēn)離(lí)。 (不可(kě)重用)

爲了避免糟糕的表現,在OOD的階段我們就(jiù)不可(kě)避免的要考慮整個軟件(jiàn)的可(kě)拓展性,可(kě)維護性,可(kě)讀(dú)性以及是否足夠靈活等等。Robert C. Martin(uncle bob)因此提出了一些設計(jì)原則,在衆多實踐後證明,遵循這些原則有助于項目保持可(kě)拓展,可(kě)維護,靈活性等特點。而這其中最經受檢驗的是SOLID原則,SOLID原則一般被人(rén)們稱爲面向對象設計(jì)的五個原則。

SOLID是下面五個原則的縮寫

  • S: Single Responsibility Principle,單一職責原則
  • O: Open-Closed Principle,開閉原則
  • L: Liskov Substitution Principle,裡(lǐ)氏替換原則
  • I:Interface Segregation Principle,接口隔離(lí)原則
  • D:Dependency Inversion Principle,依賴倒置原則

單一職責原則

A class should have one, and only one, reason to change.

一個類應該并且隻有一個改變的動機(jī),這意味着一個類應該并且隻能負責一項工(gōng)作(zuò)。如(rú)果一個類有多個職責,那麽這些職責就(jiù)會耦合在一起。一項職責的改變可(kě)能會損害或抑制類滿足其他(tā)職責的能力。這種耦合會導緻脆弱的設計(jì),在更改時将會以意想不到的方式崩潰。

在單一責任原則 (SRP) 的背景下,我們将職責定義爲"變更的動機(jī)"。如(rú)果你(nǐ)能想到改變一個類的不止一個動機(jī),那麽這個類就(jiù)有不止一個責任。思考如(rú)下所示的獲取數據接口,我們大(dà)多數人(rén)都(dōu)會認爲這個接口看(kàn)起來(lái)非常合理(lǐ)。

interface class DataSource{

  void getRemoteData() {
    // TODO: implement getRemoteData
  }
  
  
  void getLocalData(){
    // TODO: implement getLocalData
  }
}

然而,這裡(lǐ)顯示了兩個責任。第一個職責是從(cóng)遠(yuǎn)程獲取數據。二是從(cóng)本地獲取數據。

這兩種責任應該分(fēn)開嗎(ma)?這取決于應用程序會如(rú)何變化,如(rú)果軟件(jiàn)的更改影(yǐng)響了獲取遠(yuǎn)程數據接口,那麽上面的設計(jì)就(jiù)會顯得(de)僵化。相(xiàng)較于我們的希望,調用獲取本地數據的類将需要更頻繁的更改。在這種情況下,單一職責可(kě)以防止客戶端應用程序将這兩個職責耦合起來(lái),所以我們決定把職責進行分(fēn)離(lí),以避免單一職責的變更對其他(tā)部分(fēn)産生(shēng)影(yǐng)響。

interface class RemoteDataSource{

  void getData() {
    // TODO: implement getData
  }
  
}

interface class LocalDataSource{
 
  void getData(){
    // TODO: implement getData
  }
}


開閉原則

You should be able to extend a classes behavior, without modifying it.

你(nǐ)應該能夠在不改變類的情況下拓展類的行爲,換句話(huà)說(shuō)就(jiù)是類的拓展應該開放(fàng),但(dàn)修改應該關閉。開閉原則是構建可(kě)維護的和可(kě)重用的代碼的基礎。

當對程序的單個更改導緻對依賴模塊的級聯更改時,該程序将變得(de)脆弱、僵化、不可(kě)預測和不可(kě)重用。開閉原則以一種非常直接的方式攻擊這一點,它說(shuō)你(nǐ)應該設計(jì)出永遠(yuǎn)不會改變的模塊。當需求發生(shēng)更改時,您可(kě)以通過添加新代碼來(lái)擴展此類模塊的行爲,而不是通過更改已經可(kě)以工(gōng)作(zuò)的舊代碼。

符合開閉原則的模塊有兩個主要屬性。

  • 它們是"開放(fàng)擴展"。這意味着可(kě)以擴展模塊的行爲。我們可(kě)以使模塊随着應用程序的需求的變化而以新的和不同的方式運行,或者滿足新應用程序的需求。
  • 它們是"關閉修改"。該模塊的源碼是不可(kě)侵犯的。任何人(rén)都(dōu)不允許對其更改源代碼。

這兩個屬性似乎是不一緻的。擴展模塊行爲的正常方法是對該模塊進行更改。不能更改的模塊通常被認爲具有固定的行爲。如(rú)何解決這兩個對立的屬性?

抽象是解決這個問(wèn)題的關鍵。使用面向對象設計(jì)的原則,可(kě)以創建固定但(dàn)代表無限可(kě)能行爲組的抽象。抽象是抽象基類,無限的可(kě)能行爲組由所有可(kě)能的派生(shēng)類表示。這樣的模塊可(kě)以關閉以進行修改,因爲它依賴于固定的抽象。然而,該模塊的行爲卻可(kě)以通過創建新的抽象衍生(shēng)物來(lái)進行擴展,如(rú)下。

interface class DataSource {
  //獲取數據
  void getData() {
    // TODO: implement getData
}
}

class RemoteDataSource extends DataSource{
  @override
  void getData() {
    // TODO: implement getData
}
  //其他(tā)行爲
  void functionOfRemoteDataSource(){}
}

class LocalDataSource extends DataSource{
  @override
  void getData(){
    // TODO: implement getData
}
  //其他(tā)行爲
  void functionOfLocalDataSource(){}
}


裡(lǐ)氏替換原則

Derived classes must be substitutable for their base classes.

派生(shēng)類必須能夠替換其基類。必須能夠毫無問(wèn)題的使用子類的對象調用基類的方法,子類不能矛盾或改變父類的行爲或含義,這意味着派生(shēng)類必須增強基類功能而不能減少功能。


接口隔離(lí)原則

Make fine grained interfaces that are client specific.

制作(zuò)針對特定客戶端的細顆粒度接口,不要制作(zuò)負擔過多職責的接口,客戶端不應該被迫依賴于他(tā)們不使用的接口。


如(rú)上,當使用端被迫依賴它們不使用的接口時,如(rú)果這些使用端對這些接口更改,這将導緻所有使用端之間的意外耦合。換句話(huà)說(shuō),當一個使用端依賴于一個類時,該類包含該使用端不使用但(dàn)其他(tā)使用端使用的接口,那麽該使用端将受到其他(tā)使用端對該類施加的更改的影(yǐng)響。我們希望盡可(kě)能避免這樣的耦合,所以我們希望在可(kě)能的情況下隔離(lí)接口。

我們可(kě)以通過适配器模式或委托,在有多重繼承特性的語言中也可(kě)以通過多重繼承,将負擔過多的接口隔離(lí),從(cóng)而破壞不同客戶端之間不需要的耦合。


依賴倒置原則

A.High level modules should not depend upon low level modules.both should depend upon abstractions

B.abstractions should not depend upon details,details should depend upon abstractions

A.高層模塊不應該依賴于低層模塊,兩者都(dōu)應該依賴于抽象

B.抽象不應該依賴于實現細節,細節應該取決于抽象概念。

爲什麽是倒置?因爲更傳統的軟件(jiàn)開發方法,如(rú)結構化分(fēn)析和設計(jì),傾向于創建軟件(jiàn)結構,其中高級模塊依賴于低級模塊,而抽象依賴于細節。實際上,這些方法的目标之一是定義描述高級模塊如(rú)何調用低級模塊的子程序層次結構。而一個設計(jì)良好的面向對象程序的依賴結構相(xiàng)對于傳統過程方法通常産生(shēng)的依賴結構是"倒置"的。


考慮到依賴于低級模塊的高級模塊的含義,它是包含應用程序的重要策略決策和業務模型的高級模塊,而正是這些模塊決定着應用程序的身(shēn)份(特性)。然而,當這些模塊依賴于較低層次的模塊時,改變到較低層次的模塊卻可(kě)能會對它們産生(shēng)直接的影(yǐng)響,并迫使它們改變。

這種困境是荒謬的!應該是高層模塊迫使低層模塊進行改變,高層模塊應優先于低層模塊,高層模塊不應該以任何方式依賴于低級模塊。

此外,我們希望能夠重用的是高級模塊。當高級模塊依賴于低級模塊時,在不同的上下文中重用這些高級模塊就(jiù)變得(de)非常困難。但(dàn)是,當高級模塊獨立于低級模塊時,就(jiù)可(kě)以對高級模塊進行相(xiàng)當簡單地重用。

依賴注入和層級分(fēn)離(lí)是解決困境的關鍵。我們可(kě)以在高級模塊和低級模塊之間引入一個機(jī)制層,機(jī)制層用于破壞高級模塊與低級模塊的直接依賴關系,同時我們可(kě)以通過聲明抽象接口和注入依賴的方式解決模塊依賴問(wèn)題,而因爲我們聲明的是抽象接口,這樣将可(kě)以很好的規避實現變更所帶來(lái)的影(yǐng)響。



interface class ModuleA {
  void function() {
    // TODO: implement getData
}
}

class ModuleAImpl extends ModuleA{
  @override
  void function() {
    // TODO: implement getData
}
}

interface class ModuleAClient{
  function() {
    // TODO: implement getData
throw UnimplementedError();
  }
}

class ModuleAClientImpl extends ModuleAClient {
  //注入ModuleAImpl 
  final ModuleA moduleA;

  ModuleAClientImpl(this.moduleA);
  @override
  function() => moduleA.function();
}

class HighLevelModule {
  //注入ModuleAClientImpl 
  final ModuleAClient moduleAClient;

  HighLevelModule(this.moduleAClient);

  function(){
    ...
    moduleAClient.function();
    ...
  }
}


總結

五個原則中最重要和最根本的是單一職責原則。單一職責很容易被誤會爲一個功能一個類,這太過純粹了,我們一般認爲職責之中實際會包含多個功能接口,我們需要通過實際的特征區分(fēn)職責。接口隔離(lí)是單一職責在接口方面的細化,裡(lǐ)氏替換則像是開閉原則的額外聲明。遵守依賴倒置的同時,不可(kě)避免的要遵守開閉原則和裡(lǐ)氏替換。而這五個原則的核心都(dōu)是爲了避免軟件(jiàn)的單一更改對其他(tā)部分(fēn)造成不必要的影(yǐng)響。

當我們談論面向對象時,我們總是在談論封裝,抽象,繼承,多态,而SOILD則在這些關鍵詞的實現上爲我們提供了一些避免變得(de)糟糕的指導。這些原則并非能夠完美套用在實際的應用中,在實際應用中我們不能隻追求适應原則範式,而是應該把實際與原則背後的邏輯結合。


· 參考文獻

[面向對象的設計(jì)原則]、[單一職責原則]、[開閉原則]、[裡(lǐ)氏替換原則]、[接口隔離(lí)原則]、[依賴倒置原則]