面向對象的設計原則最終篇

關於面向對象的設計原則我之前已經解釋過四種了,分別是單一職責原則,開放關閉原則,裏式替換原則,依賴倒置原則而接下來我們要解釋的就是最後的三種原則了,分別是接口隔離原則, 迪米特法則, 組合複用原則

前言

在面向對象的軟件設計中,只有儘量降低各個模塊之間的耦合度,才能提高代碼的複用率,系統的可維護性、可擴展性才能提高。面向對象的軟件設計中,有23種經典的設計模式,是一套前人代碼設計經驗的總結,如果把設計模式比作武功招式,那麼設計原則就好比是內功心法。常用的設計原則有七個,下文將具體介紹。

設計原則簡介

  • 單一職責原則:專注降低類的複雜度,實現類要職責單一;

  • 開放關閉原則:所有面向對象原則的核心,設計要對擴展開發,對修改關閉;

  • 裏式替換原則:實現開放關閉原則的重要方式之一,設計不要破壞繼承關係;

  • 依賴倒置原則:系統抽象化的具體實現,要求面向接口編程,是面向對象設計的主要實現機制之一;

  • 接口隔離原則:要求接口的方法儘量少,接口儘量細化;

  • 迪米特法則:降低系統的耦合度,使一個模塊的修改儘量少的影響其他模塊,擴展會相對容易;

  • 組合複用原則:在軟件設計中,儘量使用組合/聚合而不是繼承達到代碼複用的目的。

這些設計原則並不說我們一定要遵循他們來進行設計,而是根據我們的實際情況去怎麼去選擇使用他們,來讓我們的程序做的更加的完善。

接口隔離原則

定義

接口隔離原則經常略寫爲ISP,講的是使用多個專門的接口比使用單一的接口要好。

換句話來說從一個客戶類的角度來說,一個類對另外一個類的依賴性應當是建立在最小的接口上的。

那麼到底該怎麼去理解這個接口隔離原則呢?

我覺得可以從三個方面去理解這個事情。

1. 角色的合理劃分

將“接口”理解爲一個類所提供的所有的方法的特徵集合,也就是一種在邏輯上才存在的概念,這樣的話,接口的劃分其實就是直接在類型上的劃分。

其實可以這麼想,一個接口就相當於劇本中的一個角色,而這個角色在表演的過程中,決定由哪一個演員來進行表演就相當於是接口的實現,因此,一個接口代表的應當是一個角色而不是多個角色,如果系統涉及到多個角色的話,那麼每一個角色都應當由一個特定的接口代表纔對。

而爲了避免我們產山混淆的想法,這時候我們就可以把接口隔離原則理解成角色隔離原則。

2. 定製服務

將接口理解成我們開發中狹義的JAVA接口的話,這樣子,接口隔離原則講的就是爲同一個角色提供寬窄不同的接口,來應對不同的客戶端內容,我畫一個簡單的圖示,大家就完全能明白了。

在這裏插入圖片描述
上面這個辦法其實就可以稱之爲定製服務,在上面的圖中有一個角色service以及三個不同的客戶端,這三個Client需要的服務是不一樣的,所以我給他分成了是三個接口,也就是Service1,Service2和Service3,顯而易見,每一個JAVA接口,都僅僅是將Cilent需要的行爲暴露給Client,而沒有將不需要的方法暴露出去。

其實瞭解設計模式的很容易就想到這是適配器模式的一個應用場景,我不細聊適配器模式,設計模式我們在知識星球中會進行講解。

3. 接口污染

這句話的意思就是過於臃腫的接口就是對接口的污染。

由於每一個接口都代表一個角色,實現一個接口對象,在他的整個生命週期中,都扮演着這個角色,因此將角色分清就是系統設計的一個重要的工作。因此一個符合邏輯的判斷,不應該是將幾個不同的角色都交給一個接口,而是應該交給不同的接口來進行處理。

準確而恰當的劃分角色以及角色所對應的接口,就是我們面向對象設計中的一個重要的組成部分,如果將沒有關係或者關係不大的接口整合到一起去的話,那就是對角色和接口的污染。

我們來寫代碼來舉個例子論證一下:

public interface TestInterface {
    public void method1();

    public void method2();

    public void method3();

    public void method4();

    public void method5();
}

class Test1 {
    public void mm1(TestInterface i) {
     i.method1();
    }

    public void mm2(TestInterface i) {
        i.method2();
    }
    public void mm3(TestInterface i) {
        i.method3();
    }
}

class Test2 implements TestInterface{
    @Override
    public void method1() {
        System.out.println("類Test2實現接口TestInterface的方法1");
    }
    @Override
    public void method2() {
        System.out.println("類Test2實現接口TestInterface的方法2");
    }
    @Override
    public void method3() {
        System.out.println("類Test2實現接口TestInterface的方法3");
    }
    @Override
    public void method4() {}
    @Override
    public void method5() {}
}
class Test3{
    public void mm1(TestInterface i) {
        i.method1();
    }

    public void mm2(TestInterface i) {
        i.method4();
    }
    public void mm3(TestInterface i) {
        i.method5();
    }
}

class Test4 implements TestInterface{
    @Override
    public void method1() {
        System.out.println("類Test4實現接口TestInterface的方法1");
    }

    @Override
    public void method2() {

    }

    @Override
    public void method3() {

    }

    @Override
    public void method4() {
        System.out.println("類Test4實現接口TestInterface的方法4");
    }

    @Override
    public void method5() {
        System.out.println("類Test4實現接口TestInterface的方法5");
    }

}

然後我們看一下調用方式
public class Client {
    public static void main(String[] args) {
        Test1 test1 =  new Test1();
        test1.mm1(new Test2());
        test1.mm2(new Test2());
        test1.mm3(new Test2());

        Test3 test3 = new Test3();
        test3.mm1(new Test4());
        test3.mm2(new Test4());
        test3.mm3(new Test4());
    }
}

執行結果如下:

類Test2實現接口TestInterface的方法1
類Test2實現接口TestInterface的方法2
類Test2實現接口TestInterface的方法3
類Test4實現接口TestInterface的方法1
類Test4實現接口TestInterface的方法4
類Test4實現接口TestInterface的方法5

那麼我們怎麼去設計遵循接口設計原則的代碼呢?

public interface TestInterface1 {
    public void method1();
}
interface TestInterface2{
    public void method2();

    public void method3();
}

interface TestInterface3 {
    public void method4();
    public void method5();
}

class Test1{
    public void mm1(TestInterface1 i){
        i.method1();
    }
    public void mm2(TestInterface2 i){
        i.method2();
    }
    public void mm3(TestInterface2 i){
        i.method3();
    }
}

class Test2 implements TestInterface1,TestInterface2{
    @Override
    public void method1() {
        System.out.println("類Test2實現接口TestInterface1的方法1");
    }

    @Override
    public void method2() {
        System.out.println("類Test2實現接口TestInterface2的方法2");
    }

    @Override
    public void method3() {
        System.out.println("類Test2實現接口TestInterface2的方法3");
    }
}
class Test3{
    public void mm1(TestInterface1 i){
        i.method1();
    }
    public void mm2(TestInterface3 i){
        i.method4();
    }
    public void mm3(TestInterface3 i){
        i.method5();
    }
}
class Test4 implements TestInterface1,TestInterface3{
    @Override
    public void method1() {
        System.out.println("類Test4實現接口TestInterface1的方法1");
    }

    @Override
    public void method4() {
        System.out.println("類Test4實現接口TestInterface3的方法4");
    }

    @Override
    public void method5() {
        System.out.println("類Test4實現接口TestInterface3的方法5");
    }
}


接口隔離原則的含義是:建立單一接口,不要建立龐大臃腫的接口,儘量細化接口,接口中的方法儘量少。也就是說,我們要爲各個類建立專用的接口,而不要試圖去建立一個很龐大的接口供所有依賴它的類去調用。

我寫的這個例子中,將一個龐大的接口變更爲3個專用的接口所採用的就是接口隔離原則。

所以其實接口隔離原則其實也算是“看人下菜碟”,它的意思就是要看客人是誰,在提供不同檔次的飯菜。

從接口隔離原則的角度出發的話,要根據客戶不同的需求,去指定不同的服務,這就是接口隔離原則中推薦的方式。

迪米特法則

定義

迪米特法則(Law of Demeter)又叫作最少知識原則(Least Knowledge Principle 簡寫LKP),就是說一個對象應當對其他對象有儘可能少的瞭解,不和陌生人說話。英文簡寫爲: LoD。

其實他主要是爲了解決一個我們最常見的問題,就是類之間的關係,所以類與類之間的關係越密切,耦合度就越大,當一個類放生改變的時間,對另一個類的影響也會越大。

而他最終的解決方案就是降低類和類之間的耦合度,這也是我們所說的高內聚,低耦合。

我們來通過簡單的一個系統的代碼來理解一下迪米特法則。

不滿足迪米特法則的系統

這裏的系統有三個類,分別是SomeOne,Friend和Stranger。其中SomeOne與Friend是朋友,而Friend和Stranger是朋友,系統結構圖就像下面的。

在這裏插入圖片描述

從上面的類圖中,我們可以看到,Friend持有Stranger對象的引用,這就解釋了爲什麼Friend與Stranger是朋友,我們給出一點代碼來解釋SomeOne和Friend的朋友關係。

public class Someone{
    public void operation1(Friend friend){
        Stranger stranger = friend.provide();
        stranger.operation3();
    }
}

可以看出,SomeOne具有一個方法operation1(),這個方法接收friend爲參數,顯然,根據朋友的定義,Friend和Stranger是朋友關係,其中的Friend的provide()方法會提供自己所創建的Stranger實例,就像下面的代碼

public class Friend{
    private Stranger stranger = new Stranger();
    public void operation2(){
        
    }
    public Stranger provide(){
        return stranger;
    }
}

這其實就很顯然了,SomeOne的方法operation1()並不滿足迪米特法則,爲什麼會這麼說呢?因爲這個方法引用Stranger對象,而Stranger對象不是SomeOne的朋友。

我們下面使用迪米特法則來進行改造一下這個關係和代碼。

使用迪米特法則進行改造

從上面的圖中我們可以看出,與改造之前相比,在SomeOne與Stranger之間的聯繫已經沒有了,SomeOne不需要知道Stranger的存在就可以做同樣的事情,我們看一下SomeOne的代碼,

public class Someone{
    public void operation1(Friend friend){
        friend.forward();
    }
}

從源代碼中我們可以看出,SomeOne通過調用自己的朋友Friend對象的forward()方法做到了原來需要調用Stranger對象才能夠做到的事情,那麼我們再來看一下forward()方法是做什麼呢?

public class Frend{
    private Stranger stranger = new Stranger();
    
    public void operation2(){
        System.out.printIn("In Friend.operation2()");
    }
    public void forward(){
        stranger.operation3();
    }
}

原來Friend類的forward()方法所做的就是以前SomeOne要做的事情,使用Stranger的operation3()方法,而這種forward()方法叫做轉發方法,

由於使用了調用轉發,使得調用的具體的細節被隱藏在Friend內部,從而使SomeOne與Stranger之間的直接聯繫被省略掉了,這樣一來,系統內部的耦合度降低了,在系統的某一個類需要修改時,僅僅會影響這個類的“朋友們”,而不會直接影響到其他的部分。

以上就是我對迪米特法則的一些理解,有興趣的人也可以去深入研究一下,將來對理解設計模式會有很好的見解的。

組合複用原則

定義

組合複用原則經常又叫做合成複用原則。該原則就是在一個新的對象裏面使用一些已有的對象,使之成爲新對象的一部分:新的對象通過向這些對象的委派達到複用已有功能的目的。

而在我們的代碼中儘可能使用組合而不是用繼承是什麼原因呢?
原因如下

  • 第一,繼承複用破壞包裝,它把父類的實現細節直接暴露給了子類,這違背了信息隱藏的原則;
  • 第二:如果父類發生了改變,那麼子類也要發生相應的改變,這就直接導致了類與類之間的高耦合,不利於類的擴展、複用、維護等,也帶來了系統僵硬和脆弱的設計。而用合成和聚合的時候新對象和已有對象的交互往往是通過接口或者抽象類進行的,就可以很好的避免上面的不足,而且這也可以讓每一個新的類專注於實現自己的任務,符合單一職責原則。

其實這個組合複用原則最好的理解就是我們各種系統中的後臺系統裏面的權利和角色的分配,我相信很多公司的項目中都會有,我來闡述一下這個問題。

“Has-A”和“Is-A”

“Is-A”是嚴格的分類學意義上的定義,意思是一個類是另外一個類的“一種”。而“Has-A”則不同,他表示某一個角色具有某一項責任。

我們看一個圖解

在這裏插入圖片描述

人被繼承到“僱員”,“經理”,“學生”等子類,而實際上,“僱員”,“經理”,“學生”分別描述一種角色,而“人”可以同時有幾種不同的角色,比如,一個“人”即使“經理”,就必然是“僱員”,而有可能這個“人”還是一個“學生”。如果說使用繼承來說,那麼如果這個人是“學生”,那麼它一定不能再是經理,這個大家可以思考一下爲什麼,很簡單,這顯然就是不合理的。

圖中的這種就是把“角色”的等級結構和“人”的等級結構混淆了,把“Has-A”角色誤解成爲了“Is-A”角色,而下面這幅圖就成功的解釋了這一點
在這裏插入圖片描述

而在這個圖中,就不存在之前混淆的問題了,每個人都可以擁有一個以上的“角色”了。

組合/聚合複用原則使用總結:

合成和聚合均是關聯的特殊情況。聚合用來表示“擁有”關係或者整體與部分的關係;而合成則用來表示一種強得多的“擁有”關係。在一個合成關係裏面,部分和整體的生命週期是一樣的。一個合成的新的對象完全擁有對其組成部分的支配權,包括它們的創建和銷燬等。使用程序語言的術語來說,組合而成的新對象對組成部分的內存分配、內存釋放有絕對的責任。要正確的選擇合成/複用和繼承,必須透徹地理解里氏替換原則和Coad法則。(Coad法則由Peter Coad提出,總結了一些什麼時候使用繼承作爲複用工具的條件。Coad法則:只有當以下Coad條件全部被滿足時,才應當使用繼承關係)

  1. 子類是基類的一個特殊種類,而不是基類的一個角色。區分“Has-A”和“Is-A”。只有“Is-A”關係才符合繼承關係,“Has-A”關係應當用聚合來描述。

  2. 永遠不會出現需要將子類換成另外一個類的子類的情況。如果不能肯定將來是否會變成另外一個子類的話,就不要使用繼承。

  3. 子類具有擴展基類的責任,而不是具有置換掉(override)或註銷掉(Nullify)基類的責任。如果一個子類需要大量的置換掉基類的行爲,那麼這個類就不應該是這個基類的子類。

  4. 只有在分類學角度上有意義時,纔可以使用繼承。不要從工具類繼承。

以上就是我最後介紹的關於設計模式之前的設計原則的所有了,三篇文章,你學會了麼?

我是懿,一個正在被打擊還在努力前進的碼農。歡迎大家關注我們的公衆號,加入我們的知識星球,我們在知識星球中等着你的加入。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章