跳到主要内容

迪米特法则(Law of Demeter, LoD)

定义​

定义
一个对象应该对其他对象保持最少的了解。

Talk only to your immediate friends.
(只与你的直接朋友交谈)

迪米特法则(Law of Demeter, LoD)又叫最少知识原则(Least Knowledge Principle, LKP)。它是由美国东北大学(Northeastern University)的 Ian Holland 在 1987 年提出的,最初是为了指导软件项目中的模块设计,后来成为面向对象设计中最重要的原则之一。

这条原则强调了两点:

  1. 一个类应该对自己需要耦合或调用的类知道得最少。你(类)内部如何复杂、如何实现都没有关系,但对其他类的内部细节,知道得越少越好。
  2. 一个类只与"直接的朋友"通信。不要跟"陌生人"说话,不要调用与自己无关的对象的方法。

简单理解就是:在系统设计时,不要让两个无关的类直接发生交互。如果两个对象没有必要直接通信,就不要让它们直接发生调用关系;即使需要,也尽量通过一个中间者(朋友)来转发。

核心思想:只与直接的朋友交谈​

什么是"直接的朋友"?一个对象的"朋友"包括:

  • 成员变量中出现的类(this 直接持有的对象)
  • 方法参数中出现的类(方法签名中传入的对象)
  • 方法返回值中出现的类(方法返回的对象)

而"陌生人"指的是:方法内部临时创建的对象、通过其他对象间接获取的对象等。

违反迪米特法则最常见的表现,就是过深的调用链:

order.getCustomer().getAddress().getCity().getName();

order 只应该和自己的直接朋友(如 customer)打交道,而 customer 的内部细节(它有 address,address 里有 city……)对 order 来说都是"陌生人"。这一条调用链让 order 同时依赖了 Customer、Address、City、Name 四个类,任何一个类的内部结构调整,都会破坏这条链路。

这样的代码就是典型的"高耦合":对象之间互相知道得太多了。

实例1:明星、经纪人与公司​

迪米特法则最经典的例子,就是**明星(Star)、经纪人(Agent)和公司(Company)**之间的关系。

假设一位明星要参加某公司的商业活动。如果违反迪米特法则,明星会直接和公司打交道:

对应代码:

public class Company {
private String name;

public void conductShow() {
System.out.println("公司正在安排演出");
}
}

public class Star {
private String name;

// 反例:明星直接依赖公司,还要自己安排演出
public void doBusiness(Company company) {
company.conductShow();
}
}

这样的设计问题很明显:明星的业务(接活动、谈价格、安排档期)全部暴露给了 Company,而 Star 也必须了解 Company 的内部业务。如果公司换了负责人、改了对接流程,Star 的代码就要跟着改。两个本不该直接耦合的类被绑在了一起。

正确的做法是引入**经纪人(Agent)**作为中间的朋友:

重构后的代码:

public class Star {
private String name;

// 明星的对外业务全部委托给经纪人
public void doBusiness(Agent agent) {
agent.doBusiness(this);
}
}

public class Agent {
private Star star;

public void doBusiness(Company company) {
// 经纪人负责对接公司,明星不需要知道公司的任何细节
company.conductShow();
}
}

public class Company {
private String name;

public void conductShow() {
System.out.println("公司正在安排演出");
}
}

重构后的好处:

  • Star 只和经纪人 Agent 打交道,对 Company 一无所知,耦合度大大降低。
  • 公司方的业务变化(换对接人、改流程、加签约环节)只影响 Agent,不影响 Star。
  • Agent 作为中间层,还可以承载更多职责(谈价格、签合同、排档期),符合单一职责原则。

这就是迪米特法则的典型应用:让本不该直接通信的两个类,通过一个中介来沟通。

实例2:订单查询中的"朋友圈"​

再来看一个贴近日常业务开发的例子。假设有一个订单查询功能,OrderService 需要返回订单及客户的简要信息。

如果违反迪米特法则,OrderService 可能直接穿透到 Customer 的内部:

// 反例:OrderService 对 Customer 的内部结构了解过多
public class OrderService {
public OrderVO queryOrder(Long orderId) {
Order order = orderDao.findById(orderId);
Customer customer = order.getCustomer(); // 直接拿到 Customer
Address address = customer.getAddress(); // 穿透 Customer 内部
City city = address.getCity(); // 继续穿透
return buildVO(order, city.getName()); // 依赖了 4 个类的内部结构
}
}

这里 OrderService 的直接朋友是 Order,而 Customer、Address、City 都是"陌生人的朋友"。任何一个类重构(比如 Address 拆分出 Contact),OrderService 都要跟着改。

符合迪米特法则的做法是:让 Order 自己提供聚合好的信息,把内部结构封装起来:

public class OrderService {
public OrderVO queryOrder(Long orderId) {
Order order = orderDao.findById(orderId);
// Order 自己负责提供客户所在城市,外部不需要了解 Order 的内部结构
return buildVO(order, order.getCustomerCity());
}
}
public class Order {
private Customer customer;

// 把"客户所在城市"封装在 Order 内部
public String getCustomerCity() {
return customer.getAddress().getCity().getName();
}
}

虽然 getCustomerCity() 内部仍然存在调用链,但这条链被封装在 Order 内部,对外只暴露一个语义清晰的方法。OrderService 只依赖 Order 这一个朋友,内部怎么实现是 Order 自己的事。

使用 Builder 和中间层控制链路​

在实际开发中,getCustomerCity() 这种多层穿透仍然容易出问题。更推荐的做法是:

  1. 使用 DTO/VO 聚合:查询时一次性查出需要的数据,组装成扁平的对象返回,调用方只看到结果,不看过程。
  2. 使用门面(Facade)或中间服务:复杂链路放在一个中间层里,调用方只和中间层通信。
// 通过门面对外提供统一查询入口
public class OrderQueryFacade {
public OrderDetailDTO queryDetail(Long orderId) {
// 内部可以自由组织 Order、Customer、Address 的查询与组装
return orderAssembler.toDetail(orderDao.findById(orderId));
}
}

这样,调用 OrderService/Facade 的客户端只认识一个对象,链路的任何变化都被挡在中间层内部。

严格形式与宽松形式​

迪米特法则有两种理解方式:

  • 严格形式:一个方法只能调用以下对象的方法——this 对象、方法参数对象、方法内部创建的对象、返回值对象。禁止通过"间接对象"(如 a.getB().getC() 中的 C)调用。
  • 宽松形式(实践中更常用):不限制间接调用,但要求调用链不能太长,且间接对象应当通过接口等抽象来访问。

完全遵守严格形式,会让代码出现大量转发方法(把 a.getB().getC() 变成 a.getC(),一层层转发),反而增加代码量和维护成本。所以实践中一般采用宽松形式:控制调用链的深度(一般不超过 3 层),并且优先通过封装来隐藏内部结构。

迪米特法则的代价​

任何原则都不能滥用,迪米特法则也是如此:

  1. 会产生大量"中介"类:为了让对象之间少通信,我们经常引入 Broker、Facade、Adapter 等中间类。中间类过多,会增加系统的整体复杂度和理解成本。
  2. 降低通信效率:消息经过多次转发,路径变长,性能和多线程环境下的数据一致性都可能受到影响。
  3. "转发地狱":严格形式的转发方法会让接口变得冗长,一个类可能堆满 getXxx() 的转发方法。

所以使用迪米特法则要把握一个度:该隔离的隔离(比如跨模块、跨团队的调用),该直接的联系就直接联系(比如同一个模块内、关系紧密的类)。原则是用来指导设计的,不是用来约束每一个调用的教条。

总结​

  • 迪米特法则的核心是最少知识:一个对象只和它的直接朋友通信,对其他对象保持最少的了解。
  • 直接朋友包括成员变量、方法参数、方法返回值中出现的对象;调用链上的"间接对象"是陌生人,尽量不直接打交道。
  • 通过引入经纪人/门面等中间层、把内部结构封装成语义化方法、使用 DTO 聚合数据,都能有效降低对象间的耦合。
  • 过度使用会产生过多的中间类和转发方法,实践中建议采用宽松形式,把握耦合与复杂度的平衡。

推荐阅读​