2.1 迪米特法则 (Law of Demeter, LoD) / 最少知识原则 (Least Knowledge Principle, LKP) – 避免过度依赖
核心定义
迪米特法则,也被称为最少知识原则,其核心思想是:一个对象应当尽可能少地了解其他对象的内部细节,只与直接关联的对象(即“朋友”)交互。 这样做可以降低类之间的耦合度,提高模块的独立性和可维护性。
“朋友”的定义通常包括:
- 当前对象自身 (
this或self)。 - 当前对象的成员变量(实例变量)。
- 当前对象方法的参数。
- 当前对象方法内部创建的对象。
- 当前对象的组件(如果对象是一个集合,那么集合中的元素是朋友;如果一个对象包含其他对象作为其一部分,这些部分也是朋友——但这需要谨慎界定,避免穿透封装)。
简单来说,就是“只和你的直接朋友说话,不要和朋友的朋友说话”。如果需要与“朋友的朋友”交互,应该通过你的直接朋友来转发请求。
深层解读与目的
迪米特法则旨在限制信息传播的范围,从而减少依赖关系。
- 减少耦合:当一个对象只了解其直接朋友时,如果朋友的内部实现或其更深层次的依赖发生变化,对当前对象的影响会降到最低。
- 增强封装:对象将其内部结构和复杂性隐藏起来,只暴露必要的接口给直接朋友。
- 提高可维护性:当系统某部分需要修改时,由于依赖关系清晰且有限,更容易定位和控制修改范围,减少连锁反应。
- 促进模块独立性:每个模块(或类)都像一个“黑箱”,只关心自己的直接邻居,不关心邻居的邻居是如何工作的。
违反迪米特法则的典型表现是链式调用(如 object.getFriend().getAnotherFriend().doSomething()),这种调用链条越长,当前对象就对越多间接对象的内部结构产生了依赖。
生活化类比
- 向陌生人问路:你想知道去某个地方怎么走。你问路边的一位警察(直接朋友)。警察告诉你具体路线。你不需要去了解警察是如何获取这些信息的(比如他查了地图应用,还是问了指挥中心——这些是警察的“朋友的朋友”)。你只和警察交互。
- 公司经理分配任务:经理(当前对象)需要完成一个项目。他将任务分配给他的直接下属——团队负责人(直接朋友)。团队负责人再去协调他团队内的成员(团队负责人的直接朋友,经理的“朋友的朋友”)来完成具体工作。经理不应该绕过团队负责人直接指挥团队成员。
- 点外卖:你(客户)通过外卖App(直接朋友)点餐。App将订单信息传递给餐厅(App的朋友)。餐厅的厨师(餐厅的朋友)制作菜品。外卖员(App或餐厅的朋友)将菜品送到你手中。你只与App和外卖员交互,不需要知道厨师是谁,也不需要知道餐厅的采购渠道。
实际应用场景
- 对象间的消息传递:一个
Order对象需要更新库存。它不应该直接获取Product对象,然后调用Product的Inventory对象的decreaseStock()方法(如order.getProduct().getInventory().decreaseStock())。而应该让Order对象调用Product对象的updateStockForOrder()方法,由Product对象内部去处理库存更新的逻辑(Product是Order的直接朋友,Inventory是Product的直接朋友)。 - UI与业务逻辑分离:UI层(如Controller)调用Service层的方法。Service层不应该返回一个包含大量内部细节的复杂对象给UI层,让UI层去解析和调用这个复杂对象的内部对象的内部方法。Service层应该封装好数据,提供给UI层直接展示所需的信息,或者UI层只调用Service层提供的直接方法。
作用与价值
| 作用维度 | 具体表现 |
|---|---|
| 降低耦合度 | 类与类之间的依赖关系被限制在直接朋友之间,减少了不必要的间接依赖。 |
| 提高封装性 | 对象的内部实现细节被更好地隐藏,只暴露有限的接口给直接朋友。 |
| 增强可维护性 | 修改一个类的内部实现,只要其对直接朋友的接口不变,就不会影响到其他类。 |
| 提升系统的模块化程度 | 模块间的界限更清晰,依赖关系更简单。 |
| 减少“涟漪效应” | 一个模块的变更不容易扩散到系统的其他不相关部分。 |
代码示例 (Kotlin)
违反迪米特法则的例子:
// 钱包类
class Wallet_LoD_Bad(var balance: Double)
// 顾客类,拥有一个钱包
class Customer_LoD_Bad(val name: String, val wallet: Wallet_LoD_Bad)
// 送报员类
class Paperboy_LoD_Bad {
// 收取报纸费,直接访问了顾客钱包的余额
fun collectMoney(customer: Customer_LoD_Bad, amount: Double): Boolean {
// 违反LoD: customer.wallet.balance 访问了朋友(customer)的朋友(wallet)的内部状态(balance)
if (customer.wallet.balance >= amount) {
customer.wallet.balance -= amount
println("送报员从 ${customer.name} 收取了 $${amount}")
return true
}
println("送报员: ${customer.name} 余额不足。")
return false
}
}
// 使用示例
// val customerWalletBad = Wallet_LoD_Bad(20.0)
// val customerBad = Customer_LoD_Bad("张三", customerWalletBad)
// val paperboyBad = Paperboy_LoD_Bad()
// paperboyBad.collectMoney(customerBad, 15.0) // 输出: 送报员从 张三 收取了 $15.0
// paperboyBad.collectMoney(customerBad, 10.0) // 输出: 送报员: 张三 余额不足。
在这个例子中,Paperboy_LoD_Bad类中的collectMoney方法直接访问了customer.wallet.balance。customer是Paperboy_LoD_Bad的直接朋友(通过方法参数传入),但customer.wallet是customer的内部成员(朋友的朋友),balance更是wallet的内部状态。Paperboy_LoD_Bad不应该直接操作customer.wallet.balance。
遵循迪米特法则的例子:
// 钱包类,封装余额操作
class Wallet_LoD_Good(private var balance: Double) { // balance设为private,加强封装
fun hasSufficientFunds(amount: Double): Boolean {
return balance >= amount
}
fun deduct(amount: Double): Boolean {
if (hasSufficientFunds(amount)) {
balance -= amount
return true
}
// 可以选择抛出异常或返回false来指示失败
// println("钱包错误: 余额不足以扣除 $${amount}")
return false
}
fun getBalance(): Double { // 提供获取余额的方法,而不是直接暴露变量
return balance
}
}
// 顾客类,封装支付操作
class Customer_LoD_Good(val name: String, private val wallet: Wallet_LoD_Good) { // wallet设为private
// Customer提供一个支付接口,封装对Wallet的操作
fun makePayment(amount: Double): Boolean {
if (wallet.deduct(amount)) { // 调用自身成员wallet的方法
println("${name} 支付了 $${amount}. 剩余余额: $${wallet.getBalance()}")
return true
}
println("${name} 余额不足以支付 $${amount}.")
return false
}
}
// 送报员类
class Paperboy_LoD_Good {
// Paperboy只与直接朋友Customer交互,调用Customer提供的支付方法
fun collectMoney(customer: Customer_LoD_Good, amount: Double) {
println("送报员尝试从 ${customer.name} 收取 $${amount}")
if (customer.makePayment(amount)) { // 只调用直接朋友(customer)的方法
println("送报员成功收款。")
} else {
println("送报员收款失败。")
}
}
}
// 使用示例
// val customerWalletGood = Wallet_LoD_Good(20.0)
// val customerGood = Customer_LoD_Good("李四", customerWalletGood)
// val paperboyGood = Paperboy_LoD_Good()
// paperboyGood.collectMoney(customerGood, 15.0)
// // 输出:
// // 送报员尝试从 李四 收取 $15.0
// // 李四 支付了 $15.0. 剩余余额: $5.0
// // 送报员成功收款。
// println("-----")
// paperboyGood.collectMoney(customerGood, 10.0)
// // 输出:
// // 送报员尝试从 李四 收取 $10.0
// // 李四 余额不足以支付 $10.0.
// // 送报员收款失败。
在这个遵循迪米特法则的例子中:
Wallet_LoD_Good封装了其balance,并提供了hasSufficientFunds和deduct等方法来操作余额。Customer_LoD_Good封装了其wallet,并提供了一个makePayment方法,该方法内部调用wallet的方法。Customer_LoD_Good是Paperboy_LoD_Good的直接朋友。Paperboy_LoD_Good的collectMoney方法只调用customer.makePayment(amount),它不关心customer是如何完成支付的,也不关心Wallet的存在。
这样,Paperboy_LoD_Good的依赖就仅限于Customer_LoD_Good的接口,耦合度降低了。
优缺点
| 优点 | 缺点 |
|---|---|
| 降低了类之间的耦合度。 | 可能导致系统中产生大量的包装类或转发方法:为了不让“朋友”和“朋友的朋友”说话,可能需要在“朋友”类中添加很多简单的方法将请求转发给其内部成员,这会增加类的数量和代码量。 |
| 提高了模块的独立性和封装性。 | 过度使用可能使系统结构变得复杂:如果每个细小的交互都需要通过包装方法,可能会使调用链在逻辑上变长,尽管直接的链式调用被避免了。 |
| 增强了系统的可维护性。 | 有时难以严格遵守:在某些情况下,为了获取深层嵌套对象的信息,严格遵守LoD可能会显得非常繁琐。需要权衡。 |
最佳实践与应用指南
- 避免链式调用:如
a.getB().getC().doSomething()通常是违反迪米特法则的信号。应考虑将doSomething()的逻辑(或其一部分)移到C的直接朋友B中,或者移到B的直接朋友A中,或者让A通过B间接调用C的方法。 - 封装对象的内部结构:不要轻易暴露类的成员变量,尤其是那些代表其他对象的成员。通过方法来提供对内部状态的访问和修改。
- 设计“窄”接口:类应该只暴露必要的公共方法给它的朋友。
- 使用中介者模式 (Mediator Pattern):当对象间存在复杂的网状交互时,可以使用中介者来集中控制交互,减少对象间的直接依赖,这符合迪米特法则的精神。
- 使用外观模式 (Facade Pattern):为一个复杂的子系统提供一个简化的接口,客户端只需要与外观对象交互,而不需要了解子系统内部的复杂结构,这也体现了最少知识原则。
- 区分数据对象 (Data Transfer Objects, DTOs) 和服务对象:对于纯粹的数据对象,其主要目的是传递数据,暴露其属性通常是可以接受的,因为它们没有复杂的行为。迪米特法则更多地应用于具有行为的服务对象之间的交互。
- 权衡利弊:在实践中,需要根据具体情况权衡。有时为了代码的简洁性和可读性,轻微地“违反”迪米特法则(例如,访问一个简单数据对象的属性)可能是可以接受的,特别是如果那个“朋友的朋友”是一个非常稳定且简单的对象。
迪米特法则鼓励我们创建松耦合的系统,通过限制对象间的知识范围,使得系统更容易理解、维护和修改。