第12讲:迪米特法则(LoD)上——定义与起源,最少知识原则,朋友类的定义
各位同学,今天我们来聊一个很有意思的设计原则——迪米特法则。说实话,我第一次听到这个名字的时候,还以为是某个古希腊哲学家的理论。后来才知道,这其实是1987年一个叫迪米特的项目里总结出来的经验。
嗯,先说说我个人的感受。我在早期写C++代码的时候,经常遇到一个问题:一个类为了完成某个功能,需要跟七八个其他类打交道。代码写起来倒是挺爽,但维护起来就头疼了。后来我意识到,这其实就是违反了迪米特法则。
什么是迪米特法则?
迪米特法则的英文全称是 Law of Demeter,简称 LoD。它还有一个更形象的名字——最少知识原则(Least Knowledge Principle)。
说白了,就是一句话:一个对象应该对其他对象保持最少的了解。你想想看,如果你跟一个人打交道,他上来就问你的银行卡密码,你肯定觉得这人不对劲。对象之间也是一样,不该知道的就别问。
核心思想:每个类只应该与它的直接朋友通信,不要跟陌生人说话。
起源故事
迪米特法则最早来自美国东北大学的一个研究项目,项目名字就叫“迪米特”。这个项目主要研究的是面向对象系统的设计质量。我记得当时读到原始论文的时候,里面提到一个很关键的观点:
“一个对象的方法只能调用以下对象的方法:
- 该对象自身
- 方法参数中的对象
- 方法内部创建的对象
- 该对象的直接成员变量
- 全局变量(尽量避免)
你看,这个列表里没有“通过其他对象获取到的对象”。这就是问题的关键所在。
朋友类的定义
在迪米特法则里,什么是“朋友”?我给大家一个明确的定义:
| 朋友类型 | 说明 | 示例 |
|---|---|---|
| 自身 | 类自己的成员函数和成员变量 | this->doSomething() |
| 成员对象 | 作为类成员的其他对象 | m_engine.start() |
| 参数对象 | 通过函数参数传入的对象 | void process(Logger& log) |
| 创建的对象 | 在函数内部new或创建的对象 | auto p = std::make_unique |
除了这些,其他的都是“陌生人”。我曾经在项目中见过这样的代码:
// ❌ 违反迪米特法则
void processOrder(Order& order) {
Customer& customer = order.getCustomer();
Address& addr = customer.getAddress();
std::string city = addr.getCity();
// 这里Order、Customer、Address三个类耦合在一起
}
// ✅ 符合迪米特法则
void processOrder(Order& order) {
std::string city = order.getCustomerCity();
// 把获取城市信息的职责交给Order自己处理
}
你看出区别了吗?第一种写法里,processOrder 函数知道了太多不该知道的事情。它知道Order有Customer,Customer有Address,Address有City。这一串调用,就是典型的“火车残骸”代码。
注意:链式调用(如 a.getB().getC().doSomething())是违反迪米特法则的典型表现。我曾经在一个遗留系统里看到过长达7层的链式调用,改一个底层接口,上面所有代码都要跟着改,那叫一个酸爽。
为什么叫“最少知识”?
这个名字其实更直白。你想想看,一个类知道得越少,它跟其他类的耦合就越低。耦合低了,修改起来就方便。我有个习惯:写代码的时候,会问自己一个问题——“这个类真的需要知道这个信息吗?”
如果答案是否定的,那就想办法把信息封装起来。比如上面的例子,Order完全可以提供一个 getCustomerCity() 方法,把内部的调用链隐藏起来。
一个更完整的例子
咱们来看一个实际项目中可能会遇到的场景。假设我们有一个文档编辑器:
// 违反迪米特法则
class DocumentEditor {
private:
Document* m_doc;
public:
void saveToFile(const std::string& path) {
// 直接操作Document的内部结构
Paragraph* para = m_doc->getFirstParagraph();
while (para) {
std::string text = para->getText();
// 处理文本...
para = para->getNext();
}
}
};
// 符合迪米特法则
class DocumentEditor {
private:
Document* m_doc;
public:
void saveToFile(const std::string& path) {
// 让Document自己处理保存逻辑
m_doc->exportToFile(path);
}
};
你看,第二种写法里,DocumentEditor 只需要告诉 Document:“嘿,你把自己保存到文件里。”至于Document内部怎么遍历段落、怎么格式化文本,Editor完全不需要知道。
小技巧:如果你发现自己在一个函数里写了超过两个“点号”(比如 obj.getX().getY().getZ()),那大概率是违反迪米特法则了。这时候就该考虑重构。
知识体系图
下面这张图展示了迪米特法则的核心知识结构:
避坑指南
我曾经在一个大型交易系统里看到过这样的代码:
// 真实项目中的反面教材
void TradeEngine::execute(const Order& order) {
auto* account = order.getAccount();
auto* portfolio = account->getPortfolio();
auto* position = portfolio->getPosition("AAPL");
double qty = position->getAvailableQty();
// ... 执行交易逻辑
}
这段代码的问题在于:TradeEngine 知道了整个对象图的内部结构。后来需求变了,Position 类的 getAvailableQty() 改成了 getUsableQty(),结果所有调用链上的代码都要改。这就是典型的“牵一发而动全身”。
正确的做法应该是:
// 重构后
void TradeEngine::execute(const Order& order) {
double qty = order.getAvailableQty("AAPL");
// TradeEngine只需要知道Order就够了
}
嗯,这里要注意:迪米特法则不是让你把所有方法都塞到一个类里。它的核心是让每个类只关注自己该关注的事情。说白了,就是“各扫门前雪”。
小结
迪米特法则的核心就三点:
- 只跟直接朋友通信——别绕弯子
- 减少类之间的耦合——改一处不影响全局
- 提高封装性——把内部实现藏起来
我个人觉得,这个原则特别适合用来检查代码的“味道”。如果你看到一串链式调用,或者一个类里引用了太多其他类,那大概率是设计上出了问题。这时候,不妨停下来想想:是不是该引入一个中间层?或者把职责重新分配一下?
记住一句话:好的设计,是让每个类都活得简单一点。
公众号:蓝海资料掘金营,微信deep3321