第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()),那大概率是违反迪米特法则了。这时候就该考虑重构。

知识体系图

下面这张图展示了迪米特法则的核心知识结构:

迪米特法则 (LoD) 定义与起源 1987年迪米特项目提出 最少知识原则 只与直接朋友通信 朋友类定义 自身成员 成员变量对象 参数对象 函数内创建的对象 核心价值 降低耦合度 提高可维护性 减少修改影响范围

避坑指南

我曾经在一个大型交易系统里看到过这样的代码:

// 真实项目中的反面教材
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