第七章 迪米特法则:最少知识原则
说实话,迪米特法则是我在项目中最常用的设计原则之一。它有个很形象的外号——「最少知识原则」。什么意思呢?就是一个对象应该对其他对象保持最少的了解。说白了,别跟陌生人说话太多。
我记得有一次接手一个老项目,代码里到处都是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里有Customer,Customer里有Address,Address里有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 知识体系图
下面这张图展示了迪米特法则的核心逻辑和门面模式的应用关系:
7.6 避坑指南
我总结了几条实战中容易踩的坑:
- 不要过度封装:迪米特法则不是让你把所有方法都藏起来。该暴露的接口还是要暴露,关键是暴露什么、暴露给谁。
- 注意性能开销:有时候为了遵守迪米特法则,会多写一些委托方法。如果这些方法被频繁调用,要考虑性能影响。我一般会在热点路径上做权衡。
- 门面不要变成上帝类:一个门面类如果塞了几十个方法,那它本身就变成了新的耦合点。我习惯按业务领域拆分门面,比如
OrderFacade、PaymentFacade、InventoryFacade。
7.7 小结
迪米特法则说白了就是一句话:让每个类只管好自己的事,别操心别人的内部细节。它和门面模式是天生一对——门面模式帮你实现「只与直接朋友通信」这个目标。
我在项目中一直坚持这个原则。刚开始可能会觉得多写几个封装方法有点麻烦,但项目大了以后,你会发现改代码的胆子越来越小——因为你知道,改一个地方不会牵连一片。这就是优雅代码的底气。
公众号:蓝海资料掘金营,微信deep3321