1. 程式人生 > >面向物件程式設計的五大原則例子分析

面向物件程式設計的五大原則例子分析

在應用開發的過程中,感覺最快樂也是最痛苦的莫過於優化、重構程式碼。在版本不斷地迭代更新上線中,我們不但要保證功能能正常執行,而且我們的程式碼需要保證健壯性、穩定性、拓展性。然而在我們不斷接受新的知識過程中,我們對程式碼的理解也會越來越深刻,從而出現了優化,甚至是重構程式碼的過程。在此之前我們更需要知道面向物件程式設計的五大原則。

單一職責原則(Single Responsibility Principle)

單一職責原則比較容易理解,所謂單一職責,就是對於一個類來說,應該只專注於做一件事和只有一個引起他變的原因。所謂職責,我們可以理解成功能的意思。

  • 如果一個類負責的職責過多,應該細分給其他類。
  • 要儘量清楚職責的劃分,設計類的功能只負責它所應該負責的一個功能。
  • 然而每個人對劃分的理解都不一致。例一個複雜的功能封裝一個類中導致程式碼X百行,正確是有一個個封裝類來組成來實現這個功能。

開閉原則(Open Close Principle)

一個軟體實體類,模組和函式應該對擴充套件是開放,對修改是封閉的。
在設計一個模組的時候,應當使這個模組可以在不被修改的前提下被擴充套件.換言之,應當可以在不必修改原始碼的情況下改變這個模組的行為,在保持系統一定穩定性的基礎上,對系統進行擴充套件。這是面向物件設計(OOD)的基石,也是最重要的原則。之前我們說的工廠模式就很好解析了開閉原則。

里氏替換原則(Liskov Substitution Principle)

說明:子型別必須能夠替換他們的基型別。
可以很容易的實現同一父類下各個子類的互換,而替換成子類也不會產生任何錯誤或異常。
換個方式說就是:如果每一個型別為T1的物件o1,都有型別為T2的物件o2,使得以T1定義的所有程式P在所有的物件o1都代換稱o2時,程式P的行為沒有變化,那麼型別T2是型別T1的子型別。
里氏替換原則核心原理是抽象,抽象又依賴於繼承這個特性,繼承作為面向物件三大特性之一,在給程式設計帶來巨大便利的同時,也帶來了弊端。比如使用繼承會給程式帶來侵入性,程式的可移植性降低,增加了物件間的耦合性,如果一個類被其他的類所繼承,則當這個類需要修改時,必須考慮到所有的子類,並且父類修改後,所有涉及到子類的功能都有可能會產生故障。
我們用例子分析下:

public class JavaDemo {

    public static void main(String[] args) {
        A a = new A();
        System.out.println("100-50=" + a.func1(100, 50));
        System.out.println("100-80=" + a.func1(100, 80));
    }
}

class A {
    public int func1(int a, int b) {
        return a - b;
    }
}

執行結果:

100-50=50
100-80=20

新需求,我們需要增加一個新的功能:完成兩數相加,然後再與100求和,由類B來負責。即類B需要完成兩個功能:

public class JavaDemo {

    public static void main(String[] args) {
        B b = new B();
        System.out.println("100-50=" + b.func1(100, 50));
        System.out.println("100-80=" + b.func1(100, 80));
        System.out.println("100+20+100=" + b.func2(100, 20));
    }
}

class A {
    public int func1(int a, int b) {
        return a - b;
    }
}

class B extends A {
    public int func1(int a, int b) {
        return a + b;
    }

    public int func2(int a, int b) {
        return func1(a, b) + 100;
    }
}

類B完成後,執行結果:

100-50=150
100-80=180
100+20+100=220

我們發現原本執行正常的相減功能發生了錯誤。原因就是類B在給方法起名時無意中重寫了父類的方法,造成所有執行相減功能的程式碼全部呼叫了類B重寫後的方法,造成原本執行正常的功能出現了錯誤。在本例中,引用基類A完成的功能,換成子類B之後,發生了異常。在實際程式設計中,我們常常會通過重寫父類的方法來完成新的功能,這樣寫起來雖然簡單,但是整個繼承體系的可複用性會比較差,特別是運用多型比較頻繁時,程式執行出錯的機率非常大。如果非要重寫父類的方法,比較通用的做法是:原來的父類和子類都繼承一個更通俗的基類,原有的繼承關係去掉,採用依賴、聚合,組合等關係代替。

里氏替換原則通俗的來講就是:子類可以擴充套件父類的功能,但不能改變父類原有的功能。它包含以下4層含義:

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

依賴倒置原則(Dependence Inversion Principle)

定義:高層模組不應該依賴低層模組,二者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象。

我們用例子好好理解下:

public class JavaDemo {

    public static void main(String[] args) {
        Mother mother = new Mother();
        mother.narrate(new Book());
    }
}

class Book {
    public String getContent() {
        return "很久很久以前有一個白雪公主";
    }
}

class Mother {
    public void narrate(Book book) {
        System.out.println("媽媽開始講故事");
        System.out.println(book.getContent());
    }
}

執行結果:

媽媽開始講故事
很久很久以前有一個白雪公主

感覺挺不錯的,但是某一天需求變了,媽媽說的不是故事書,而是一份當天的報紙。報紙的程式碼:

    class NewsPaper{  
        public String getContent(){  
            return "雙12準備開啟,某某大減價!";  
        }  
    }  

但是媽媽沒有讀報紙的方法,這樣媽媽就不會讀報紙了,這可說不過去,那隻能過載媽媽的narrate()方法。那以後讀雜誌、讀郵件等等呢。我們需要不斷修改媽媽這個高層類嗎?這肯定不是一個好方法。

所以我們引入一個抽象的介面IReader。讀物介面。

interface IReader{  
    public String getContent();  
} 

Mother類與介面IReader發生依賴關係,而Book和NewsPaper都屬於讀物的範疇,他們各自都去實現IReader介面,這樣就符合依賴倒置原則了,程式碼修改為:

public class JavaDemo {

    public static void main(String[] args) {
        Mother mother = new Mother();
        mother.narrate(new Book());
        mother.narrate(new NewsPaper());
    }
}

class NewsPaper implements IReader {
    public String getContent() {
        return "雙12準備開啟,某某大減價!";
    }
}

class Book implements IReader {
    public String getContent() {
        return "很久很久以前有一個白雪公主";
    }
}

class Mother{  
    public void narrate(IReader reader){  
        System.out.println("媽媽開始講故事");  
        System.out.println(reader.getContent());  
    }  
}  

interface IReader {
    public String getContent();
}

執行結果:

媽媽開始講故事
很久很久以前有一個白雪公主
媽媽開始講故事
雙12準備開啟,某某大減價!

這樣修改後,無論以後怎樣擴充套件Main類,都不需要再修改Mother類了。這只是一個簡單的例子,實際情況中,代表高層模組的Mother類將負責完成主要的業務邏輯,一旦需要對它進行修改,引入錯誤的風險極大。所以遵循依賴倒置原則可以降低類之間的耦合性,提高系統的穩定性,降低修改程式造成的風險。

依賴倒置原則的核心思想是:面向介面程式設計。

介面隔離原則 (Interface Segregation Principle)

  • 一個類對另外一個類的依賴是建立在最小的介面上。
  • 使用多個專門的介面比使用單一的總介面要好。
  • 客戶端不應該依賴它不需要的介面。

廣義的介面:一個介面相當於劇本中的一種角色,而此角色在一個舞臺上由哪一個演員來演則相當於介面的實現。因此一個介面應當簡單的代表一個角色,而不是一個多重身份角色。如果系統設計多個角色的話,則應當每一個角色都由一個特定的介面代表。
狹義的介面(Interface):介面隔離原則講的就是同一個角色提供寬、窄不同的介面,以對付不同的客戶端。

我們用例子理解一下:

public class JavaDemo {

    public static void main(String[] args) {
        A a = new A();
        a.depend1(new B());
        a.depend2(new B());
    }
}

interface I {
    public void method1();

    public void method2();

    public void method3();

    public void method4();

}

class A {
    public void depend1(I i) {
        i.method1();
    }

    public void depend2(I i) {
        i.method2();
    }

}

class B implements I {
    public void method1() {
        System.out.println("類B實現介面I的方法1");
    }

    public void method2() {
        System.out.println("類B實現介面I的方法2");
    }

    public void method3() {
        System.out.println("類B實現介面I的方法3");
    }

    // 對於類B來說,method4不是必需的,但是由於介面I中有這個方法,
    // 所以在實現過程中即使這個方法的方法體為空,也要將這個沒有作用的方法進行實現。
    public void method4() {
    }

}

執行結果:

類B實現介面I的方法1
類B實現介面I的方法2

可以看到,如果介面過於臃腫,只要介面中出現的方法,不管對依賴於它的類有沒有用處,實現類中都必須去實現這些方法,這顯然不是好的設計。如果將這個設計修改為符合介面隔離原則,就必須對介面I進行拆分。具體如下:

interface I1 {
    public void method1();
}

interface I2 {
    public void method2();

    public void method3();
}

interface I3 {
    public void method4();
}

class A {
    public void depend1(I1 i) {
        i.method1();
    }

    public void depend2(I2 i) {
        i.method2();
    }

    public void depend3(I2 i) {
        i.method3();
    }
}

class B implements I1, I2 {
    public void method1() {
        System.out.println("類B實現介面I1的方法1");
    }

    public void method2() {
        System.out.println("類B實現介面I2的方法2");
    }

    public void method3() {
        System.out.println("類B實現介面I2的方法3");
    }
}

介面隔離原則的含義是:建立單一介面,不要建立龐大臃腫的介面,儘量細化介面,介面中的方法儘量少。也就是說,我們要為各個類建立專用的介面,而不要試圖去建立一個很龐大的介面供所有依賴它的類去呼叫。
在程式設計中,依賴幾個專用的介面要比依賴一個綜合的介面更靈活。介面是設計時對外部設定的“契約”,通過分散定義多個介面,可以預防外來變更的擴散,提高系統的靈活性和可維護性。

參考:
卡奴達摩的專欄
OOP幾大原則