iT邦幫忙

DAY 19
4

ASP.NET 由淺入深系列 第 19

91之ASP.NET由淺入深 不負責講座 Day19 - LoD/LKP 最少知識原則

今天要介紹的東西有兩個名字,
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。


上一篇
91之ASP.NET由淺入深 不負責講座 Day18 - 開放封閉原則
下一篇
91之ASP.NET由淺入深 不負責講座 Day20 - ISP 介面隔離原則
系列文
ASP.NET 由淺入深30

尚未有邦友留言

立即登入留言