第七章 迪米特法则:最少知识原则

说实话,迪米特法则是我在项目中最常用的设计原则之一。它有个很形象的外号——「最少知识原则」。什么意思呢?就是一个对象应该对其他对象保持最少的了解。说白了,别跟陌生人说话太多。

我记得有一次接手一个老项目,代码里到处都是a.getB().getC().doSomething()这种链式调用。改一个地方,牵连七八个类。那感觉,就像拆炸弹,剪错一根线就全炸了。后来我重构时,核心思路就是应用迪米特法则。

7.1 什么是迪米特法则

迪米特法则(Law of Demeter,LoD)是1987年由Ian Holland提出的。它的核心思想是:

  • 每个单元只应该与它的直接朋友通信
  • 不要与「陌生人」说话
  • 只调用属于以下范围的方法:
    • 对象自身的方法
    • 方法参数传入的对象的方法
    • 对象内部创建的对象的方法
    • 对象的直接组件(成员变量)的方法

你想想看,如果A类直接调用了B类的内部数据,那B类一旦修改内部结构,A类也得跟着改。这就是高耦合的典型症状。

核心观点:迪米特法则的本质是控制信息的可见范围。每个类只暴露必要的接口,隐藏内部实现细节。

7.2 违反迪米特法则的典型场景

我在项目中见过最多的反例,就是过度暴露内部结构。来看个例子:

// ❌ 违反迪米特法则
public class OrderService {
    public void processOrder(Order order) {
        // 直接获取内部对象,再调用内部对象的方法
        Customer customer = order.getCustomer();
        Address address = customer.getAddress();
        String city = address.getCity();
        
        // 还调用了更深层的对象
        Payment payment = order.getPayment();
        String cardNumber = payment.getCard().getNumber();
        
        System.out.println("订单发往:" + city);
    }
}

这段代码的问题在哪?OrderService 知道了太多不该知道的事。它知道Order里有CustomerCustomer里有AddressAddress里有city。一旦这些内部结构变化,OrderService就得跟着改。

我曾经在一个电商项目中,就因为这种写法,改一个地址字段的命名,结果改了20多个文件。嗯,从那以后我再也不敢这么写了。

7.3 如何遵守迪米特法则

正确的做法是:让直接朋友提供你需要的方法。改造上面的例子:

// ✅ 遵守迪米特法则
public class OrderService {
    public void processOrder(Order order) {
        // 只调用order的方法,不深入内部
        String city = order.getShippingCity();
        String maskedCardNumber = order.getMaskedCardNumber();
        
        System.out.println("订单发往:" + city);
    }
}

// Order类提供封装好的方法
public class Order {
    private Customer customer;
    private Payment payment;
    
    public String getShippingCity() {
        return customer.getAddress().getCity();
    }
    
    public String getMaskedCardNumber() {
        return payment.getCard().getMaskedNumber();
    }
}

你看,现在OrderService只跟Order打交道。至于Order内部怎么获取城市、怎么脱敏卡号,它完全不用关心。这就是降低耦合的精髓。

我的习惯:写代码时,我会问自己一个问题——「这个类真的需要知道这些细节吗?」如果答案是否定的,那就封装起来。

7.4 迪米特法则与门面模式

说到迪米特法则的实战应用,门面模式(Facade Pattern)是最经典的例子。门面模式说白了就是给复杂子系统提供一个统一的入口

我参与过一个支付系统的重构。原来调用方要跟支付网关、风控系统、积分系统、通知系统分别打交道。代码长这样:

// ❌ 调用方需要知道太多子系统
public class PaymentClient {
    public void pay(Order order) {
        PaymentGateway gateway = new PaymentGateway();
        RiskControl risk = new RiskControl();
        PointsSystem points = new PointsSystem();
        Notification notifier = new Notification();
        
        if (risk.check(order)) {
            String result = gateway.charge(order);
            points.addPoints(order);
            notifier.send(order, result);
        }
    }
}

调用方直接依赖了4个类。任何一个类变化,调用方都得改。这就是高耦合

用门面模式重构后:

// ✅ 门面模式:统一入口
public class PaymentFacade {
    private PaymentGateway gateway;
    private RiskControl risk;
    private PointsSystem points;
    private Notification notifier;
    
    public PaymentFacade() {
        this.gateway = new PaymentGateway();
        this.risk = new RiskControl();
        this.points = new PointsSystem();
        this.notifier = new Notification();
    }
    
    public void pay(Order order) {
        if (risk.check(order)) {
            String result = gateway.charge(order);
            points.addPoints(order);
            notifier.send(order, result);
        }
    }
}

// 调用方只需要知道门面
public class PaymentClient {
    private PaymentFacade facade = new PaymentFacade();
    
    public void pay(Order order) {
        facade.pay(order);  // 只跟门面打交道
    }
}

现在调用方只依赖PaymentFacade一个类。子系统内部怎么变化,调用方完全不用管。这就是最少知识原则的实战体现。

注意:门面模式不是万能的。过度使用会导致门面类变得臃肿,变成「上帝类」。我建议一个门面只负责一组相关的功能,不要把所有功能都塞进去。

7.5 知识体系图

下面这张图展示了迪米特法则的核心逻辑和门面模式的应用关系:

迪米特法则 · 知识体系 最少知识原则 违反:链式调用、高耦合 遵守:只与直接朋友通信 门面模式(Facade) 典型反例 a.getB().getC().do() 一个类依赖多个内部对象 正确做法 封装内部细节 只暴露必要接口 门面模式要点 统一入口 隐藏子系统复杂性 目标:降低类间耦合,提高可维护性

7.6 避坑指南

我总结了几条实战中容易踩的坑:

  • 不要过度封装:迪米特法则不是让你把所有方法都藏起来。该暴露的接口还是要暴露,关键是暴露什么、暴露给谁
  • 注意性能开销:有时候为了遵守迪米特法则,会多写一些委托方法。如果这些方法被频繁调用,要考虑性能影响。我一般会在热点路径上做权衡。
  • 门面不要变成上帝类:一个门面类如果塞了几十个方法,那它本身就变成了新的耦合点。我习惯按业务领域拆分门面,比如OrderFacadePaymentFacadeInventoryFacade
我的经验:写代码时,我会先画出类的依赖关系图。如果发现某个类依赖了5个以上的其他类,我就会考虑引入门面或者重新设计接口。这招帮我避免了很多后期重构的麻烦。

7.7 小结

迪米特法则说白了就是一句话:让每个类只管好自己的事,别操心别人的内部细节。它和门面模式是天生一对——门面模式帮你实现「只与直接朋友通信」这个目标。

我在项目中一直坚持这个原则。刚开始可能会觉得多写几个封装方法有点麻烦,但项目大了以后,你会发现改代码的胆子越来越小——因为你知道,改一个地方不会牵连一片。这就是优雅代码的底气。


公众号:蓝海资料掘金营,微信deep3321