設計模式六大原則(五)----迪米特法則

盛開的太陽 發表於 2021-06-10

一. 什麼是迪米特法則

迪米特法則(Law of Demeter )又叫做最少知識原則,也就是說,一個物件應當對其他物件儘可能少的瞭解。不和陌生人說話。英文簡寫為: LoD。

迪米特法則的目的在於降低類之間的耦合。由於每個類儘量減少對其他類的依賴,因此,很容易使得系統的功能模組功能獨立,相互之間不存在(或很少有)依賴關係。

迪米特法則不希望類之間建立直接的聯絡。如果真的有需要建立聯絡,也希望能通過它的友元類來轉達。因此,應用迪米特法則有可能造成的一個後果就是:系統中存在大量的中介類,這些類之所以存在完全是為了傳遞類之間的相互呼叫關係——這在一定程度上增加了系統的複雜度。

二. 為什麼要遵守迪米特法則?

在物件導向程式設計中有一些眾所周知的抽象概念,比如封裝、內聚和耦合,理論上可以用來生成清晰的設計和良好的程式碼。雖然這些都是非常重要的概念,但它們不夠實用,不能直接用於開發環境,這些概念是比較主觀的,非常依賴於使用人的經驗和知識。對於其他概念,如單一責任原則、開閉原則等,情況也是一樣的。迪米特法則的獨特之處在於它簡潔而準確的定義,它允許在編寫程式碼時直接應用,幾乎自動地應用了適當的封裝、低內聚和鬆耦合。

三. 迪米特法則的廣狹義

3.1. 狹義的迪米特法則

如果兩個類不必彼此直接通訊,那麼這兩個類就不應當發生直接的相互作用。如果其中的一個類需要呼叫另一個類的某一個方法的話,可以通過第三者轉發這個呼叫。

朋友圈的確定“朋友”條件:

1)當前物件本身(this)

2)以引數形式傳入到當前物件方法中的物件.

方法入參是一個物件, 這是這個物件和當前類是朋友

3)當前物件的例項變數直接引用的物件

定一個一個類, 裡面的屬性引用了其他物件, 那麼這個物件的例項和當前例項是朋友

4)當前物件的例項變數如果是一個聚集,那麼聚集中的元素也都是朋友

如果屬性是一個物件, 那麼屬性和物件裡的元素都是朋友

5)當前物件所建立的物件

任何一個物件,如果滿足上面的條件之一,就是當前物件的“朋友”;否則就是“陌生人”。

狹義的迪米特法則的缺點:

在系統裡造出大量的小方法,這些方法僅僅是傳遞間接的呼叫,與系統的業務邏輯無關。
遵循類之間的迪米特法則會是一個系統的區域性設計簡化,因為每一個區域性都不會和遠距離的物件有直接的關聯。但是,這也會造成系統的不同模組之間的通訊效率降低,也會使系統的不同模組之間不容易協調。

2. 廣義的迪米特法則在類的設計上的體現:

  優先考慮將一個類設定成不變類。
  儘量降低一個類的訪問許可權。
  謹慎使用Serializable。
  儘量降低成員的訪問許可權。

四. 迪米特法則在設計模式中的應用

設計模式的門面模式(Facade)和中介模式(Mediator),都是迪米特法則的應用

下面我們已經租房為例, 來研究迪米特法則.
通常 客戶要找房子住, 我們就直接建一個房子類, 建一個客戶類, 客戶找房子即可.

public interface IHouse {
    // 住房子
    public void Housing();
}

public class House implements IHouse{

    @Override
    public void Housing() {
        System.out.println("住房子");
    }
}


public class Customer {
    public String name;

    public void findHourse(IHouse house) {
        house.Housing();
    }
}

客戶找房子住, 邏輯很簡單, 這樣是ok的. 雖然違背了迪米特法則, 但符合業務邏輯也說得通.
但是, 通常我們找房子, 不是一下子就能找到的, 我們要找很多家, 這就很費勁, 那不如交給中介. 中介有很多房源, 房東吧房子給了中介, 不需要關心租戶是誰, 租戶將找房的事交給房東, 他也不用管房東是誰, 而且租戶+房東都很省事.

/**
 * 房子
 */
public interface IHouse {
    // 住房子
    public void Housing();
}

public class House implements IHouse{

    @Override
    public void Housing() {
        System.out.println("住房子");
    }
}

public interface ICustomer {

    void findHourse(IHouse house) ;
}

public class Customer implements ICustomer {

    public void findHourse(IHouse house) {
        house.Housing();
    }
}

/**
 * 中介
 */
public class Intermediary {
    // 找房子
    public IHouse findHouse(ICustomer customer){
        // 幫租戶找房子
        return null;
    }
}

房子,客戶是相互獨立的, 彼此之間沒有引用. 他們之間建立關係是通過中介. 也就是, 客戶找中介租房子, 房東吧房子交給租戶, 最後中介將找好的房子給到客戶. 客戶和房東彼此隔離, 符合迪米特法則.

五. 迪米特法則實踐

那麼在實踐中如何做到一個物件應該對其他物件有最少的瞭解呢?如果我們把一個物件看作是一個人,那麼要實現“一個人應該對其他人有最少的瞭解”,做到兩點就足夠了:

1.只和直接的朋友交流;

2.減少對朋友的瞭解。下面就詳細說說如何做到這兩點。

1. 只和直接的朋友交流

迪米特法則還有一個英文解釋是:talk only to your immediate friends(只和直接的朋友交流)。

什麼是朋友呢?

每個物件都必然會與其他的物件有耦合關係,兩個物件之間的耦合就會成為朋友關係。那麼什麼又是直接的朋友呢?出現在成員變數方法的輸入輸出引數中的類就是直接的朋友。迪米特法則要求只和直接的朋友通訊。

注意:

只出現在方法體內部的類就不是直接的朋友,如果一個類和不是直接的朋友進行交流,就屬於違反迪米特法則。

我們舉一個例子說明什麼是朋友,什麼是直接的朋友。很簡單的例子:老師讓班長清點全班同學的人數。這個例子中總共有三個類:老師Teacher、班長GroupLeader和學生Student。


public interface ITeacher {
    void command(IGroupLeader groupLeader);
}

public class Teacher implements ITeacher{
    @Override
    public void command(IGroupLeader groupLeader) {
        // 全班同學
        List<Student> allStudent = new ArrayList<>();
        allStudent.add(new Student());
        allStudent.add(new Student());
        allStudent.add(new Student());
        allStudent.add(new Student());
        allStudent.add(new Student());
        // 班長清點人數
        groupLeader.count(allStudent);

    }
}

**
 * 班長類
 */
public interface IGroupLeader {

    // 班長清點人數
    void count(List<Student> students);
}

/**
 * 班長類
 */
public class GroupLeader implements IGroupLeader{
    /**
     * 班長清點人數
     * @param students
     */
    @Override
    public void count(List<Student> students) {
        // 班長清點人數
        System.out.println("上課的學生人數是: " + students.size());
    }
}

/**
 * 學生類
 */
public interface IStudent {
}

/**
 * 學生類
 */
public class Student implements IStudent {


}

/**
 * 客戶端
 */
public class Client {
    public static void main(String[] args) {
        // 老師類
        ITeacher wangTeacher = new Teacher();

        // 班長
        IGroupLeader zhangBanzhang = new GroupLeader();
        wangTeacher.command(zhangBanzhang);
    }
}


執行結果:

上課的學生人數是: 5

在這個例子中,我們的Teacher有幾個朋友?兩個,一個是GroupLeader,它是Teacher的command()方法的入參;另一個是Student,因為在Teacher的command()方法體中使用了Student。

那麼Teacher有幾個是直接的朋友?按照直接的朋友的定義

出現在成員變數、方法的輸入輸出引數中的類就是直接的朋友

只有GroupLeader是Teacher的直接的朋友。

Teacher在command()方法中建立了Student的陣列,和非直接的朋友Student發生了交流,所以,上述例子違反了迪米特法則。方法是類的一個行為,類竟然不知道自己的行為與其他的類產生了依賴關係,這是不允許的,嚴重違反了迪米特法則

為了使上述例子符合迪米特法則,我們可以做如下修改:

public interface ITeacher {
    void command(IGroupLeader groupLeader);
}

public class Teacher implements ITeacher {
    @Override
    public void command(IGroupLeader groupLeader) {
        // 班長清點人數
        groupLeader.count();
    }
}

/**
 * 班長類
 */
public interface IGroupLeader {
    // 班長清點人數
    void count();
}

/**
 * 班長類
 */
public class GroupLeader implements IGroupLeader {

    private List<Student> students;

    public GroupLeader(List<Student> students) {
        this.students = students;
    }

    /**
     * 班長清點人數
     */
    @Override
    public void count() {
        // 班長清點人數
        System.out.println("上課的學生人數是: " + students.size());
    }
}


/**
 * 學生類
 */
public interface IStudent {
}

/**
 * 學生類
 */
public class Student implements IStudent {


}


/**
 * 客戶端
 */
public class Client {
    public static void main(String[] args) {
        // 老師類
        ITeacher wangTeacher = new Teacher();

        List<Student> allStudent = new ArrayList(10);
        allStudent.add(new Student());
        allStudent.add(new Student());
        allStudent.add(new Student());
        allStudent.add(new Student());

        // 班長
        IGroupLeader zhangBanzhang = new GroupLeader(allStudent);
        wangTeacher.command(zhangBanzhang);
    }
}

執行結果:

上課的學生人數是: 4

這樣修改後,每個類都只和直接的朋友交流,有效減少了類之間的耦合

2. 減少對朋友的瞭解

如何減少對朋友的瞭解?如果你的朋友是個話癆加大喇叭,那就算你不主動去問他,他也會在你面前說個不停,把他所有的經歷都講給你聽。所以,要減少對朋友的瞭解,請換一個內斂一點的朋友吧~換作在一個類中,就是儘量減少一個類對外暴露的方法。

舉一個簡單的例子說明一個類暴露方法過多的情況。一個人用咖啡機煮咖啡的過程,例子中只有兩個類,一個是人,一個是咖啡機。

首先是咖啡機類CoffeeMachine,咖啡機制作咖啡只需要三個方法:1.加咖啡豆;2.加水;3.製作咖啡:

/**
 * 咖啡機抽象介面
 */
public interface ICoffeeMachine {

    //加咖啡豆
    void addCoffeeBean();

    //加水
    void addWater();

    //製作咖啡
    void makeCoffee();
}


/**
 * 咖啡機實現類
 */
public class CoffeeMachine implements ICoffeeMachine{

    //加咖啡豆
    public void addCoffeeBean() {
        System.out.println("放咖啡豆");
    }

    //加水
    public void addWater() {
        System.out.println("加水");
    }

    //製作咖啡
    public void makeCoffee() {
        System.out.println("製作咖啡");
    }
}


/**
 * 人, 製作咖啡
 */
public interface IMan {
    /**
     * 製作咖啡
     */
    void makeCoffee();
}

/**
 * 人制作咖啡
 */
public class Man implements IMan {
    private ICoffeeMachine coffeeMachine;

    public Man(ICoffeeMachine coffeeMachine) {
        this.coffeeMachine = coffeeMachine;
    }

    /**
     * 製作咖啡
     */
    public void makeCoffee() {
        coffeeMachine.addWater();
        coffeeMachine.addCoffeeBean();
        coffeeMachine.makeCoffee();
    }
}

/**
 * 客戶端
 */
public class Client {
    public static void main(String[] args) {
        ICoffeeMachine coffeeMachine = new CoffeeMachine();

        IMan man = new Man(coffeeMachine);
        man.makeCoffee();

    }
}

執行結果:

加水

放咖啡豆

製作咖啡

在這個例子中,CoffeeMachine是Man的直接好友,但問題是Man對CoffeeMachine瞭解的太多了,其實人根本不關心咖啡機具體制作咖啡的過程。所以我們可以作如下優化:

優化後的咖啡機類,只暴露一個work方法,把製作咖啡的三個具體的方法addCoffeeBean、addWater、makeCoffee設為私有:


/**
 * 咖啡機抽象介面
 */
public interface ICoffeeMachine {

    //咖啡機工作
    void work();

}

/**
 * 咖啡機實現類
 */
public class CoffeeMachine implements ICoffeeMachine {

    //加咖啡豆
    public void addCoffeeBean() {
        System.out.println("放咖啡豆");
    }

    //加水
    public void addWater() {
        System.out.println("加水");
    }

    //製作咖啡
    public void makeCoffee() {
        System.out.println("製作咖啡");
    }

    @Override
    public void work() {
        addCoffeeBean();
        addWater();
        makeCoffee();
    }
}

/**
 * 人, 製作咖啡
 */
public interface IMan {
    /**
     * 製作咖啡
     */
    void makeCoffee();
}


/**
 * 人制作咖啡
 */
public class Man implements IMan {
    private ICoffeeMachine coffeeMachine;

    public Man(ICoffeeMachine coffeeMachine) {
        this.coffeeMachine = coffeeMachine;
    }

    /**
     * 製作咖啡
     */
    public void makeCoffee() {
        coffeeMachine.work();
    }
}

/**
 * 客戶端
 */
public class Client {
    public static void main(String[] args) {
        ICoffeeMachine coffeeMachine = new CoffeeMachine();

        IMan man = new Man(coffeeMachine);
        man.makeCoffee();

    }
}

這樣修改後,通過減少CoffeeMachine對外暴露的方法,減少Man對CoffeeMachine的瞭解,從而降低了它們之間的耦合。

在實踐中,只要做到只和直接的朋友交流和減少對朋友的瞭解,就能滿足迪米特法則。因此我們不難想象,迪米特法則的目的,是把我們的類變成一個個“肥宅”。“肥”在於一個類對外暴露的方法可能很少,但是它內部的實現可能非常複雜(這個解釋有點牽強~)。“宅”在於它只和直接的朋友交流。在現實生活中“肥宅”是個貶義詞,在日本“肥宅”已經成為社會問題。但是在程式中,一個“肥宅”的類卻是優秀類的典範。

六. 注意事項

第一:在類的劃分上,應當建立弱耦合的類,類與類之間的耦合越弱,就越有利於實現可複用的目標。
第二:在類的結構設計上,每個類都應該降低成員的訪問許可權。
第三:在類的設計上,只要有可能,一個類應當設計成不變的類。
第四:在對其他類的引用上,一個物件對其他類的物件的引用應該降到最低。
第五:儘量限制區域性變數的有效範圍,降低類的訪問許可權。