设计模式之——“迪米特法则”(“最少知道”原则)

                 设计模式之——“迪米特法则”(“最少知道”原则)

 

什么是“迪米特法则”


迪米特法则(Law of Demeter)又叫作最少知识原则(Least Knowledge Principle 简写LKP),就是说一个对象应当对其他对象有尽可能少的了解,不和陌生人说话。

上面是摘自百度百科对于“迪米特法则”的解释,相信有很多同学看完这段解释还是处于一脸懵逼的状态,接下来,让我们以一个较为贴近生活的例子里讲解一下“迪米特法则”到底指的是什么东西?又为什么要遵循“迪米特法则”呢?

 

“送奶工”和“客户”的故事


相信有很多同学都曾经订购过“鲜奶”吧,可以包月或者包年付费,也可以每天早上等送奶工直接把鲜奶送到家里来,然后在付钱给送奶工。

我们再来梳理一下整个业务流程:送奶工把“鲜奶”送到客户的家里,并且从客户手中得到相应的“报酬”。让我们来用代码来模拟一下这个业务场景吧!

 

模拟场景


让我们先来模拟一下“客户”对象,“客户”对象会有相应的“”姓名“”属性,或许还会有一个“账户”号,一个“地址”属性,和一些支付方法等等。为了简化描述,我们只给“客户对象”增加“姓名”和“钱包”属性,就像下面这样:

package test;
public class Customer {
    private String name;//客户姓名
    private Wallet Wallet;//客户的钱包

    public Customer(String name, test.Wallet wallet) {
        this.name = name;
        Wallet = wallet;
    }

    public String getName() {
        return name;
    }

    public Wallet getWallet() {
        return Wallet;
    }
}

接下来,眼尖的同学应该发现我们现在需要实现Wallet钱包类了:

package test;

public class Wallet {
    private float value;//钱包内的价值

    public Wallet(float value) {
        this.value = value;
    }

    /**
     * @return 返回当前钱包内的总额
     */
    public float getTotalMoney() {
        return value;
    }

    /**
     * @param deposit 向钱包中加入的金额数量
     */
    public void addMoney(float deposit) {
        value += deposit;
    }

    /**
     * @param debit 需要从钱包中扣除的金额数量
     */
    public void subtractMoney(float debit) {
        value -= debit;
    }

}

现在,“客户”和“钱包”类已经设计好了,“送奶工”可以“闪亮登场”了。回忆一下,“送奶工”把“鲜奶”带到你的家门前,按响门铃,你接过鲜奶,支付相应的费用......。我们抽象出“送奶工”的代码:

package test;

public class MilkMan {
    /**
     * 送奶工执行“交易”
     *
     * @param payment  送奶工应收取的费用
     * @param customer 被收取费用的客户对象
     */
    public void makeDeal(float payment, Customer customer) {
        Wallet wallet = customer.getWallet();
        //获取客户的钱包
        if (wallet.getTotalMoney() >= payment) {
            wallet.subtractMoney(payment);
            //从客户的钱包中减去相应的金额
            System.out.println("交易产生:送奶工从钱包中成功拿走" + payment + "元");
        }
    }
}

接下来,再写我们最后的驱动类:

package test;

public class Driver {
    public static void main(String[] args) {
        //先构造钱包类
        Wallet wallet = new Wallet(100);
        //在构造客户类
        Customer customer = new Customer("小明", wallet);
        //构构造送奶工
        MilkMan milkMan = new MilkMan();
        //模拟送奶工和客户之间的交易
        milkMan.makeDeal(5, customer);
    }
}

让我们来运行一下这个程序吧,看看结果会是怎样:

“送奶工”从“客户”手中获取到“钱包”,并从钱包中拿走相应的费用.......一切都看似的那么美好!等等,好像哪里出了点问题?

 

哪里出问题了?


这段代码看似“很好”的完成了任务,但是实际上却是一段非常糟糕的代码。为什么这样说呢?让我们再来梳理一遍整个的业务流程。

显然,当送奶工把“鲜奶”送到“客户”家门口时,“送奶工”直接从“客户”身上拿走他的钱包,并从中拿走5元。

我不知道你们是怎么想的,但是我几乎不会让别人直接动我的钱包。更不要说是不认识的陌生人了,万一他拿走你的钱包,取走的不是5元而是500元呢?亦或者直接把你的信用卡给拿走了,这将会是一件很可怕的事情!我们不禁得好好思考一下,送奶工需要直接拿到客户的钱包才能完成交易吗?

这是一个非常关键的地方。送奶工类现在“知道”了客户有一个钱包,并且能够直接随心所欲地操控这个钱包。当我们编译“MilkMan.java”时,他需要依赖一个“Wallet.java”和“Customer.java”,正如下面这段代码片段所示。现在这三个类已经紧紧的“耦合”在一起了。

public void makeDeal(float payment, Customer customer) {
        Wallet wallet = customer.getWallet();
        //获取客户的钱包
        if (wallet.getTotalMoney() >= payment) {

我们还要思考一个在现实生活中发生很普遍的事情:如果哪天客户的钱包被偷了怎么办?此时在代码中相对应的Wallet就被设置成为null了。此时,当送奶工又来到客户家门口时,他还以为能够直接从客户身上拿到钱包,然而等待他的将会是一个“空指针异常”。

当然,有同学会讲了,当送奶工执行钱包的任意方法之前,先检查一下钱包是不是为null在执行相应的方法不就可以了吗?当然可以,但是这样会增加送奶工业务代码的复杂度。

 

“迪米特法则”的威力


接下来,我们需要修改上面的代码,使其变得更接近于我们的真实世界:当送奶工来到我们家门前,他将会要求顾客支付鲜奶的费用。他再也不会像上面的代码描述的那样,直接从顾客手中的钱包拿钱了。或许,他甚至可以不知道顾客是用钱包支付还是用“微信支付”。

新的顾客(Customer2)类:

package test;

public class Customer2 {
    private String name;//客户姓名
    private Wallet wallet;//客户的钱包

    public Customer2(String name, test.Wallet wallet) {
        this.name = name;
        wallet = wallet;
    }

    /**
     * 由送奶工获取顾客的钱包变成顾客自己使用钱包支付费用
     *
     * @param bill 顾客支付的费用
     * @return 支付的费用
     */
    public float getPayment(float bill) {
        if (wallet != null) {
            if (wallet.getTotalMoney() > bill) {
                wallet.subtractMoney(bill);
            }
        }
        return bill;
    }

    public String getName() {
        return name;
    }

    public test.Wallet getWallet() {
        return wallet;
    }
}

注意上面的代码,顾客再也没有【getWallet()】这个方法了,取而代之的是一个【getPayment()】方法。让我们再来看一下新的送奶工类(MilkMan2)。

package test;

public class MilkMan2 {

    /**
     * 送奶工执行“交易
     *
     * @param payment  送奶工应收取的费用
     * @param customer 被收取费用的客户对象
     */
    public void makeDeal(float payment, Customer2 customer) {
        //此处不再获取顾客的钱包,而是要求顾客自己支付费用
        float customerPayment = customer.getPayment(payment);
        System.out.println("感谢您的订购!");
    }
}

接下来,让我们来模拟一下送奶工新的一天吧:

package test;

public class Driver {
    public static void main(String[] args) {
        //先构造钱包类
        Wallet wallet = new Wallet(100);
        //在构造客户类
        Customer2 customer = new Customer2("小明", wallet);
        //构构造送奶工
        MilkMan2 milkMan2 = new MilkMan2();
        //模拟送奶工和客户之间的交易
        milkMan2.makeDeal(5, customer);
    }
}

运行后的结果为:

 

为什么变好了?


为什么后面的代码要比我们前面看到的代码好呢?一些同学可能还是喜欢第一次我们写的代码,认为我们只不过仅仅把Customer顾客类对象变得更加的复杂了而已。

首先第一点,第二段的代码明显更加贴近我们的现实生活,现在送奶工是要求顾客主动支付费用,而再也不会直接从顾客身上拿钱包付钱了。

第二点,顾客的Wallet类可以随时改变,但送奶工类却不用跟着变。无论顾客类是改用微信支付,还是支付宝支付,亦或者是零钱支付,只要顾客类对外暴露的接口getPayment()方法声明不变,送奶工根本不在意顾客是用什么方式支付费用的,它只要调用顾客提供的getPayment()方法就好了,具体getPayment()方法是怎样实现的,送奶工并不用关心。这样,代码将会变得更加容易维护,因为顾客类一个小小的改变并不会波及整个源代码,也就是说,其他的代码不用跟着顾客类的变化而一起变化。

“迪米特”大法好


现在我们看到了“迪米特法则”带来的好处,那么什么时候该应用“迪米特法则”呢?又或者说怎样写代码才能满足迪米特法则呢?总结来说就是:

方法不能乱调用,必须调用下面类型的方法才满足“迪米特法则”:

①当前对象的其他方法,即同一个类中的其他方法。

②传递进来的参数对象的方法。

③在方法中自己实例化的任何对象的方法。

④当前对象内部的组件的方法,即当前对象定义的属性对象的方法。

 

 

博客文章版权申明


 

 

 

 

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页