今天要介紹的東西有兩個名字,
Law of Demeter, LoD (狄米特法則)
Least Knowledge Principle, LKP (最少知識原則)
這兩個原則,其實是在講一樣的事。在SOLID原則中,拿來判斷class耦合性的原則之一。
會用到之前講到的『封裝變化』的概念。
定義
一個object應該對其他object有最少的瞭解。
簡單的說
任何一個object應該只要讓外部object知道,最少且缺一不可的資訊,就要可以正常的interact。
目的
用來解耦,也就是降低類別與類別之間耦合的程度。當耦合程度降低時,每一個class可以跟其他class互動的機會就會增加,reuse的機會就會增加。
How
如何降低本身class的資訊被外界知道太多,導致邏輯混亂或class被隨意改變狀態?
透過封裝以及visibility的控制,謹慎的考慮該使用public, protected, private 還是internal的能見度。
舉例
我們這邊舉個例子,承接著上次Daddy要刷牙、洗臉、大便的例子,我們要來說明,怎麼樣的設計可以符合LoD。
先來看看我們的class diagram:
我們現在的場景是,每天早上媽媽要叫爸爸起床,起床的動作,包含了叫爸爸去刷牙、洗臉跟大便。
所以我們的CallDaddyGetUp的內容如下:
public override void CallDaddyGetUp(AbstractDaddy thisDaddy)
{
AbstractToothBrush toolBrush = new 牙刷();
#region 叫Daddy起床,要Daddy做的事
thisDaddy.刷牙(toolBrush);
thisDaddy.洗臉();
thisDaddy.大便();
#endregion
}
AbstractDaddy的class長這樣:
public abstract class AbstractDaddy
{
private AbstractToothBrush _toothBrush;
public AbstractDaddy()
{
}
public AbstractDaddy(AbstractToothBrush toothBrush) {
this._toothBrush = toothBrush;
}
public abstract void 刷牙(AbstractToothBrush toothBrush);
public abstract void 洗臉();
public abstract void 大便();
}
對我們來說,如果刷牙()、洗臉()、大便()稱為『起床三部曲』,而且只有在起床的時候用的到,那麼我們就應該把這三個行為封裝在MyDaddy的Class裡面,稱為起床()的行為。
倘若全世界的每個Daddy都是做這樣的事,且不容改變。那我們就應該將此行為封裝在AbstractDaddy裡面,且不允許繼承的子類別變更,另一方面,也可以讓繼承的子類別不必重寫這樣的行為。
所以我們的AbstractDaddy變成下面這樣,對外只剩下GetUp(),而不讓外面的Mommy來決定Daddy起床要做哪一些事,Mommy只要專注於叫Daddy起床即可。
還記得我們提到封裝變化嗎?當未來需求異動,Daddy決定起床第一件事先大便,或是先喝水時,我們只需要修改GetUp()的內容即可。對外界來說,仍是Daddy的起床行為,而不用去管到底Daddy起床是先大便還是先刷牙。
所以我們的Mommy內容就變成下面這樣:
public override void CallDaddyGetUp(AbstractDaddy thisDaddy)
{
#region 叫Daddy起床,要Daddy做的事
//thisDaddy.刷牙(toolBrush);
//thisDaddy.洗臉();
//thisDaddy.大便();
thisDaddy.GetUp();
#endregion
}
上面的例子,我們看到了,如何Daddy原本對外的方法,變成了只有一個,其餘都是屬於Daddy本身的行為。
第二個例子,則在剛剛的程式碼裡面已經露出一點端倪了,
我們先假設Daddy刷牙()這個方法是public的,
我們原本的Mommy要叫Daddy刷牙的code是長這樣:
public class MyMommy : AbstractMommy
{
/// <summary>
/// Daddy刷牙要用的牙刷,由Mommy產生出來遞給他,但牙刷跟Mommy無直接關係,只是為了叫Daddy刷牙
/// </summary>
/// <param name="thisDaddy"></param>
public override void CallDaddyBrushTooth(AbstractDaddy thisDaddy)
{
AbstractToothBrush toolBrush = new 牙刷();
thisDaddy.刷牙(toolBrush);
}
}
可以看到,Mommy只是為了叫Daddy刷牙,還要去生一根牙刷給Daddy,但是在這個case裡,牙刷對於Mommy來說,一點意義都沒有。
class關係就變成下圖所示:
我們覺得,應該讓Daddy自己想辦法去找牙刷就好,而不是需要Mommy什麼都準備的好好的,而且Mommy找的牙刷,也不一定合適Daddy用。
所以修正一下我們的class diagram,Mommy只叫Daddy刷牙,他要用啥刷是他的事情。
如此一來,牙刷怎麼改變,都與Mommy無關,Mommy也可以專心在叫Daddy刷牙這件事。未來Daddy刷牙的實際內容改變,Mommy與Daddy的互動關係也不需要改變。
Mommy的class改成這樣,單純多了:
Daddy則自己去生一把牙刷,或是隨意的想辦法完成刷牙這個行為即可。
可以看到這裡用new,其實是一個頗糟糕的用法。
沒錯,new牙刷的這個部分,還可以套用factory pattern來決定用哪一種牙刷。
也可以再將Clean的方法抽象成一個interface,再用IoC的方式,來決定究竟要用什麼樣的東西來Clean()即可。
結論
透過上面這樣的重構例子,可以看到重構完class之間的耦合性降低了,也因為耦合性降低,封裝變化後,不論是classs要重複使用時,可以降低一堆包袱與糾葛,或是需求異動時,通常場景類抽象的使用各個抽象介面的行為,幾乎都不需要改變到邏輯。
除非真的是商業邏輯的異動,如果只是場景類的商業邏輯異動,則只是重組各個抽象介面的行為邏輯。而不需要修改到抽象介面後的concrete class。