迪米特法則又叫作最少知識原則,就是說,一個對象應當對其他對象要有儘可能少的瞭解。
一、狹義的迪米特法則
如果兩個類不必彼此直接通信,那麼這兩個類就不應該發生直接的相互作用。如果其中的一個類需要調用另一個類的某一個方法時,可以通過第三者轉發這個調用。
1.朋友圈與陌生人
如下圖所示,“某人”與一個“朋友”組成自己的朋友圈,兩個人都需要與一個圈外的“陌生人”發生相互作用。
“朋友”與“陌生人”若是朋友,組成“朋友”的朋友圈如下圖所示。
相比較之下,“某人”其實並不需要與陌生人直接發生相互作用,但是“朋友”更需要與“陌生人”發生相互作用。這時候,迪米特法則建議“某人”不要直接與“陌生人”發生相互作用,而是通過“朋友”與之直接發生相互作用,如下圖所示。
這時候,“朋友”實際上起到了將“某人”對“陌生人”的調用轉發給“陌生人”的作用。這種傳遞叫作調用轉發(Call Forwarding)。所謂調用轉發,需要隱藏“陌生人”的存在,使得“某人”僅知道“朋友”,而不知道“陌生人”。換言之,“某人”會認爲他調用的這個方法是“朋友”的方法。
2、朋友圈的確定
”某人”的朋友圈是如何確定的?以下的條件成爲朋友條件:
- 當前對象本身(this)。
- 以參量形式傳入到當前對象方法中的對象。
- 當前對象的實例變量直接引用的對象。
- 當前對象的實例變量如果是一個聚集,那麼聚集中的元素也都是朋友。
- 當前對象所創建的對象。
任何一個對象,如果滿足上述條件之一,就是當前對象的“朋友”,否則就是“陌生人”。
3、不滿足迪米特法則的系統
系統由三個類組成:分別是Someone,Friend和Stranger。其中Someone與Friend是朋友,而Friend與Stranger是朋友。系統的結構如下圖所示。
從上面的類圖可以看出,Friend持有一個Stranger對象的引用,所以Friend與Stranger是朋友。Friend類的源代碼。
public class Friend {
private Stranger stranger=new Stranger();
public void operation2(){
//code
}
public Stranger provide(){
return stranger;
}
}
Someone的源代碼
public class Someone {
/**
* 調用Stranger的方法
* @param friend
*/
public void operation1(Friend friend){
Stranger stranger=friend.provide();
stranger.operation3();
}
}
可以看出,在這種情況下,Someone與Stranger是朋友關係。Someone的operation1()方法接受Friend爲參量,顯然根據“朋友”的定義,Friend是Someone的朋友。其中Friend的provide()方法會提供自己所創建的Stranger的實例。顯然,Someone的operation1()方法不滿足迪米特法則。因爲這個方法引用了Stranger對象,而Stranger對象不應該是Someone的朋友。
4、使用迪米特法則就行代碼重構
可以使用迪米特法則對上面的例子進行改造,改造的做法是調用轉發。改造後的類圖如下圖所示。
從上面的類圖可以看出,與改造前相比,Someone與Stranger之間的聯繫已經沒有了。Someone不需要知道Stranger的存在就可以做同樣的事情。
Someone的源代碼。
public class Someone {
/**
* 通過Friend轉發方法,調用Stranger的方法
* @param friend
*/
public void operation1(Friend friend){
friend.forward();
}
}
從Someone的源代碼可以看出,Someone通過調用自己的朋友Friend對象的forward()方法做到了原來需要調用Stranger對象才能夠做到的事情。
Friend的源代碼。
public class Friend {
private Stranger stranger=new Stranger();
public void operation2(){
//code
}
/**
* 調用轉發方法
*/
public void forward(){
stranger.operation3();
}
}
原來Friend類的forward()方法所做的就是以前Someone要做的事情:使用了Stranger的operation3()方法。這種forward()方法叫做轉發方法。
由於使用了調用轉發,使得調用的細節被隱藏在Friend的內部,從而使Someone與Stranger之間的直接聯繫被省略掉了。這樣一來,使得系統內部的耦合度降低。在系統的某一個類需要修改時,僅僅會直接影響到這個類的“朋友”們,而不會直接影響到其餘部分。
5、狹義迪米特法則的缺點
遵循狹義的迪米特法則會產生一個明顯的缺點:會在系統裏製造出大量的小方法,散落在系統的各個角落。這些方法僅僅是傳遞間接的調用,因此與系統的商務邏輯無關。當設計師試圖從一張類圖看出總體的架構時,這些小的方法會造成迷惑和困擾。
遵循狹義的迪米特法則會使一個系統的局部設計簡化,因爲每一個局部都不會和遠距離的對象有直接的關聯。但是,這也會造成系統的不同模塊之間的通信效率降低,也會使系統的不同模塊之間不容易協調。
6、與依賴倒轉原則互補使用,克服狹義迪米特法則的缺點
爲了克服狹義迪米特法則的缺點,可以使用依賴倒轉原則,引入一個抽象類型引用“抽象陌生人”對象,使“某人”依賴於“抽象陌生人”。換言之,就是將“抽象陌生人”變成朋友。
如下圖所示。
“某人”現在與一個抽象角色建立了朋友關係,這樣做的好處是“朋友”可以隨時將具體“陌生人”換掉。只有新的具體“陌生人”具有相同的抽象類型,那麼“某人”就無法區分他們。這就允許“陌生人”的具體實現可以獨立於“某人”而變化。如下圖所示。
可以引入一個抽象AbstractStranger,讓Someone依賴於這個抽象角色,從而使Someone與Stranger的具體實現脫耦。如下圖所示。
AbstractStranger由一個Java接口實現,源代碼如下。
public interface AbstractStranger {
abstract void operation3();
}
Stranger實現了這個接口,源代碼如下。
public class Stranger implements AbstractStranger{
public void operation3(){
//code
}
}
Someone依賴於抽象類型AbstractStranger,源代碼如下。
public class Someone {
public void operation1(Friend friend){
AbstractStranger stranger=friend.provide();
stranger.operation3();
}
}
Friend提供的也是抽象類型AbstractStranger,源代碼如下。
public class Friend {
private AbstractStranger stranger=new Stranger();
public void operation2(){
//code
}
public AbstractStranger provide(){
return stranger;
}
}
二、廣義的迪米特法則
迪米特法則所談論的,就是對對象之間的信息流量、流向以及信息的影響的控制。
在軟件系統中,一個模塊設計得好不好最主要、最重要的標誌,就是該模塊在多大程度上將自己的內部數據和其他與實現有關的細節隱藏起來。一個設計得好的模塊可以將它所有實現的細節隱藏起來,徹底的將提供給外界的API和自己的實現分隔開來。這樣,模塊和模塊之間就可以僅僅通過彼此的API相互通信,而不會理會模塊內部的工作細節。這一概念就是“信息的隱藏”,或者叫作“封裝”,是軟件設計的基本教義之一。
信息的隱藏非常重要的原因在於,它可以使各個子系統之間脫耦,從而允許它們獨立的被開發、優化、使用、閱讀以及修改。這種脫耦化可以有效的加快系統的開發過程,因爲可以獨立地同時開發各個模塊。它可以使維護過程變得容易,因爲所有的模塊都容易讀懂,特別是不必擔心對其他模塊的影響。
迪米特法則的主要用意是控制信息的過載。將迪米特法則運用到系統設計中時,要注意下面的幾點。
(1) 在類的劃分上,應當創建有弱耦合的類。類之間的耦合越弱,就越有利於複用。一個處於弱耦合中類一旦被修改,不會對有關係的類造成波及。
(2) 在類的結構設計上,每一個類都應當儘量降低成員的訪問權限(Accessibility)。換言之,一個類包裝好各自的private狀態。一個類不應當public自己的屬性,而應當提供取值和賦值的方法讓外界訪問自己的屬性。
(3) 在類設計上,只要有可能,一個類應當設計成不變類。
(4) 在對其他類的引用上,一個對象對其對象的引用應當降到最低。
1、廣義的迪米特法則在類的設計上的體現
優先考慮將一個類設置成不變類
Java語言的API提供了很多的不變類,比如:String,BigInteger,BigDecimal等封裝類都是不變類。
一個對象與外界的通信大體可分兩種,一種是改變這個對象的狀態,另一種是不改變這個對象的狀態。當設計任何一個類的時候,都首先考慮這個類的狀態是否需要改變。即便一個類必須是可變類,在給他的屬性設置賦值方法的時候,也要保持吝嗇的態度。除非真的需要,否則不要爲一個屬性設置賦值方法。
儘量降低一個類的訪問權限
在滿足一個系統對這個類的需求的同時,應當儘量降低這個類的訪問權限(Accessibility)。對於頂級的類來說,只有兩個可能性的訪問等級。
- package-private:這是默認的訪問權限。如果一個類是package-private的,那麼它就只能從當前庫訪問。
- public:如果一個類是public的,那麼這個類從當前庫和其他庫都可以訪問。
一個具有package-private訪問權限的好處是,一旦這個類發生修改,那麼受到影響的客戶端必定都在這個庫內部。
儘量降低成員的訪問權限
類的成員包括屬性、方法、嵌套類和嵌套接口等,一個類的成員可以有四種不同的訪問權限。
- private:這個成員只可能從當前頂級類的內部訪問。
- package-private:這個成員可以被當前庫中的任何一個類訪問,這是默認訪問權限。
- protected:如果一個成員是protected的,那麼當前庫中的任何一個類都可以訪問它,而且在任何庫中的這個類的子類也都可以訪問它。
- public:此成員可以從任何地方被訪問。
取代C Struct
Point類是一個類似於C Struct 的Java類,這個類被叫作退化的類,因爲沒有提供數據的封裝。
一個類似於C Struct的Java類源代碼。
public class Point {
public int x;
public int y;
}
這個類的設計是錯誤的,因爲這個類沒有提供給自己演化的空間。一個好的設計應該提供適當的訪問方法,包括取值方法和賦值方法。
public class Point {
private int x;
private int y;
public Point(int x, int y) {
super();
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
}
2、廣義的迪米特法則在代碼層次上的體現
限制局域變量的有效範圍
在需要一個變量的時候才聲明它,可以有效地限制局域變量的有效範圍。一個變量如果僅僅在塊的內部使用的話,就應當將這個變量在程序塊的內部使用它的地方聲明,而不是放到塊的外部或者塊的開頭聲明。這樣做的好處有兩個:
(1) 程序可讀性比較好。
(2) 如果一個變量是在需要它的程序塊的外部聲明的,那麼當這個塊還沒有被執行時,這個變量就已經被分配了內存;而且在這個程序塊已經執行完畢後,這個變量所佔據的內存空間還沒有釋放,這顯然是不好的。如果局域變量都是在馬上就要使用的時候才聲明,就可以避免這種情況。