一. 什麼是迪米特法則
迪米特法則(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的瞭解,從而降低了它們之間的耦合。
在實踐中,只要做到只和直接的朋友交流和減少對朋友的瞭解,就能滿足迪米特法則。因此我們不難想象,迪米特法則的目的,是把我們的類變成一個個“肥宅”。“肥”在於一個類對外暴露的方法可能很少,但是它內部的實現可能非常複雜(這個解釋有點牽強~)。“宅”在於它只和直接的朋友交流。在現實生活中“肥宅”是個貶義詞,在日本“肥宅”已經成為社會問題。但是在程式中,一個“肥宅”的類卻是優秀類的典範。
六. 注意事項
第一:在類的劃分上,應當建立弱耦合的類,類與類之間的耦合越弱,就越有利於實現可複用的目標。
第二:在類的結構設計上,每個類都應該降低成員的訪問許可權。
第三:在類的設計上,只要有可能,一個類應當設計成不變的類。
第四:在對其他類的引用上,一個物件對其他類的物件的引用應該降到最低。
第五:儘量限制區域性變數的有效範圍,降低類的訪問許可權。