迪米特法则(Law of Demeter, LoD)
定义
- 定义
- 一个对象应该对其他对象保持最少的了解。
Talk only to your immediate friends.
(只与你的直接朋友交谈)
迪米特法则(Law of Demeter, LoD)又叫最少知识原则(Least Knowledge Principle, LKP)。它是由美国东北大学(Northeastern University)的 Ian Holland 在 1987 年提出的,最初是为了指导软件项目中的模块设计,后来成为面向对象设计中最重要的原则之一。
这条原则强调了两点:
- 一个类应该对自己需要耦合或调用的类知道得最少。你(类)内部如何复杂、如何实现都没有关系,但对其他类的内部细节,知道得越少越好。
- 一个类只与"直接的朋友"通信。不要跟"陌生人"说话,不要调用与自己无关的对象的方法。
简单理解就是:在系统设计时,不要让两个无关的类直接发生交互。如果两个对象没有必要直接通信,就不要让它们直接发生调用关系;即使需要,也尽量通过一个中间者(朋友)来转发。
核心思想:只与直接的朋友交谈
什么是"直接的朋友"?一个对象的"朋友"包括:
- 成员变量中出现的类(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() 这种多层穿透仍然容易出问题。更推荐的做法是:
- 使用 DTO/VO 聚合:查询时一次性查出需要的数据,组装成扁平的对象返回,调用方只看到结果,不看过程。
- 使用门面(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 层),并且优先通过封装来隐藏内部结构。
迪米特法则的代价
任何原则都不能滥用,迪米特法则也是如此:
- 会产生大量"中介"类:为了让对象之间少通信,我们经常引入 Broker、Facade、Adapter 等中间类。中间类过多,会增加系统的整体复杂度和理解成本。
- 降低通信效率:消息经过多次转发,路径变长,性能和多线程环境下的数据一致性都可能受到影响。
- "转发地狱":严格形式的转发方法会让接口变得冗长,一个类可能堆满
getXxx()的转发方法。
所以使用迪米特法则要把握一个度:该隔离的隔离(比如跨模块、跨团队的调用),该直接的联系就直接联系(比如同一 个模块内、关系紧密的类)。原则是用来指导设计的,不是用来约束每一个调用的教条。
总结
- 迪米特法则的核心是最少知识:一个对象只和它的直接朋友通信,对其他对象保持最少的了解。
- 直接朋友包括成员变量、方法参数、方法返回值中出现的对象;调用链上的"间接对象"是陌生人,尽量不直接打交道。
- 通过引入经纪人/门面等中间层、把内部结构封装成语义化方法、使用 DTO 聚合数据,都能有效降低对象间的耦合。
- 过度使用会产生过多的中间类和转发方法,实践中建议采用宽松形式,把握耦合与复杂度的平衡。
推荐阅读
- 《设计模式:可复用面向对象软件的基础》(GoF)——开篇即讨论"对依赖关系的管理"
- Law of Demeter - Wikipedia
- 本站:单一职责原则、依赖倒置原则