[ASP.NET]91之ASP.NET由浅入深 不负责讲座 Day19 – LoD/LKP 最少知识原则

[ASP.NET]91之ASP.NET由浅入深 不负责讲座 Day19 – LoD/LKP 最少知识原则


前言
今天要介绍的东西有两个名字,

  1. Law of Demeter, LoD (狄米特法则)
  2. 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:

image

我们现在的场景是,每天早上妈妈要叫爸爸起床,起床的动作,包含了叫爸爸去刷牙、洗脸跟大便。
所以我们的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起床即可。

image

还记得我们提到封装变化吗?当未来需求异动,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
    {       

        /// 
        /// Daddy刷牙要用的牙刷,由Mommy产生出来递给他,但牙刷跟Mommy无直接关系,只是为了叫Daddy刷牙
        /// 
        /// 
        public override void CallDaddyBrushTooth(AbstractDaddy thisDaddy)
        {
            AbstractToothBrush toolBrush = new 牙刷();
            thisDaddy.刷牙(toolBrush);
        }
    }

可以看到,Mommy只是为了叫Daddy刷牙,还要去生一根牙刷给Daddy,但是在这个case里,牙刷对于Mommy来说,一点意义都没有。

class关系就变成下图所示:

image

我们觉得,应该让Daddy自己想办法去找牙刷就好,而不是需要Mommy什么都准备的好好的,而且Mommy找的牙刷,也不一定合适Daddy用。

所以修正一下我们的class diagram,Mommy只叫Daddy刷牙,他要用啥刷是他的事情。

image

 如此一来,牙刷怎么改变,都与Mommy无关,Mommy也可以专心在叫Daddy刷牙这件事。未来Daddy刷牙的实际内容改变,Mommy与Daddy的交互关系也不需要改变。

Mommy的class改成这样,单纯多了:

image

Daddy则自己去生一把牙刷,或是随意的想办法完成刷牙这个行为即可。

image

可以看到这里用new,其实是一个颇糟糕的用法。

没错,new牙刷的这个部分,还可以套用factory pattern来决定用哪一种牙刷。

也可以再将Clean的方法抽象成一个interface,再用IoC的方式,来决定究竟要用什么样的东西来Clean()即可。

结论
透过上面这样的重构例子,可以看到重构完class之间的耦合性降低了,也因为耦合性降低,封装变化后,不论是classs要重复使用时,可以降低一堆包袱与纠葛,或是需求异动时,通常场景类抽象的使用各个抽象界面的行为,几乎都不需要改变到逻辑。
除非真的是商业逻辑的异动,如果只是场景类的商业逻辑异动,则只是重组各个抽象界面的行为逻辑。而不需要修改到抽象界面后的concrete class。


或许您会对下列培训课程感兴趣:

  1. 2019/7/27(六)~2019/7/28(日):演化式设计:测试驱动开发与持续重构 第六梯次(中国台北)
  2. 2019/8/16(五)~2019/8/18(日):【C#进阶设计-从重构学会高易用性与高弹性API设计】第二梯次(中国台北)
  3. 2019/9/21(六)~2019/9/22(日):Clean Coder:DI 与 AOP 进阶实战 第二梯次(中国台北)
  4. 2019/10/19(六):【针对遗留代码加入单元测试的艺术】第七梯次(中国台北)
  5. 2019/10/20(日):【极速开发】第八梯次(中国台北)

想收到第一手公开培训课程资讯,或想询问企业内训、顾问、教练、咨询服务的,请洽 Facebook 粉丝专页:91敏捷开发之路。