什么是迪米特法则?
迪米特法则(Law of Demeter, LoD)也称为最少知识原则(Least Knowledge Principle, LKP),由 Ian Holland 在 1987 年提出。
Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. 每个单元应该只对与其紧密相关的单元有有限的了解。
更通俗的表述是:只和直接朋友交流,不要和陌生人说话。
核心思想
什么是"直接朋友"?
一个对象的"直接朋友"包括:
- 当前对象本身(this/self)
- 作为参数传入的对象
- 当前对象创建的对象
- 当前对象的成员变量
- 全局对象(如单例,技术上可访问,但应谨慎依赖)
class Person {
let wallet: Wallet // 成员变量 - 直接朋友
func buyItem(shop: Shop, item: Item) { // 参数 - 直接朋友
let receipt = Receipt() // 自己创建的对象 - 直接朋友
// 以下都是与直接朋友交流
let money = wallet.getMoney()
shop.sell(item: item)
receipt.record(item: item)
}
}
什么是"陌生人"?
如果需要通过多层对象去访问另一个对象,那个被间接访问的对象就是"陌生人"。
// 违反迪米特法则:深层链式调用
class Customer {
let wallet: Wallet
func pay(shop: Shop, amount: Double) {
// 访问了三层结构:wallet -> creditCard -> bank
// Customer 不应该知道信用卡内部有银行网关
shop.charge(wallet.creditCard.bank.paymentGateway, amount: amount)
}
}
// 典型的违规链式调用
order.customer.address.city.name // 访问了太多陌生人
iOS开发中的迪米特法则实践
1. 避免链式属性访问
// 违反迪米特法则
class OrderViewController: UIViewController {
var order: Order?
func displayCustomerCity() {
// 链式访问:知道太多内部结构
let city = order?.customer?.address?.city?.name
cityLabel.text = city
}
}
// 问题:
// 1. OrderViewController 需要了解 Order、Customer、Address、City 的结构
// 2. 任何中间结构的变化都会影响这段代码
// 3. 需要处理多层可选值
// 遵循迪米特法则:封装访问路径
struct Order {
let customer: Customer
// 提供直接的访问方法
var customerCityName: String? {
return customer.cityName
}
}
struct Customer {
let address: Address
var cityName: String? {
return address.cityName
}
}
struct Address {
let city: City?
var cityName: String? {
return city?.name
}
}
// 使用
class OrderViewController: UIViewController {
var order: Order?
func displayCustomerCity() {
// 简洁,只和直接朋友(order)交流
cityLabel.text = order?.customerCityName
}
}
2. 使用中介者/外观模式
// 违反迪米特法则:ViewController 直接操作多个底层模块
class CheckoutViewController: UIViewController {
func checkout() {
// 直接和多个模块交互
let cart = CartManager.shared.getCart()
let user = UserManager.shared.currentUser
let inventory = InventoryService.shared
let payment = PaymentService.shared
let shipping = ShippingService.shared
// 检查库存
for item in cart.items {
if !inventory.checkAvailability(item.productId, quantity: item.quantity) {
showError("库存不足")
return
}
}
// 计算运费
let shippingFee = shipping.calculate(address: user.address, items: cart.items)
// 处理支付
let total = cart.total + shippingFee
payment.process(amount: total, method: user.paymentMethod)
// ... 更多逻辑
}
}
// 遵循迪米特法则:引入 Facade(外观)
class CheckoutFacade {
private let cartManager: CartManager
private let userManager: UserManager
private let inventoryService: InventoryService
private let paymentService: PaymentService
private let shippingService: ShippingService
init(
cartManager: CartManager = .shared,
userManager: UserManager = .shared,
inventoryService: InventoryService = .shared,
paymentService: PaymentService = .shared,
shippingService: ShippingService = .shared
) {
self.cartManager = cartManager
self.userManager = userManager
self.inventoryService = inventoryService
self.paymentService = paymentService
self.shippingService = shippingService
}
func checkout() async throws -> CheckoutResult {
let cart = cartManager.getCart()
let user = userManager.currentUser
// 检查库存
try await validateInventory(for: cart)
// 计算总价
let shippingFee = try await calculateShipping(for: cart, to: user.address)
let total = cart.total + shippingFee
// 处理支付
let paymentResult = try await processPayment(amount: total, method: user.paymentMethod)
return CheckoutResult(orderId: paymentResult.orderId, total: total)
}
private func validateInventory(for cart: Cart) async throws { /* ... */ }
private func calculateShipping(for cart: Cart, to address: Address) async throws -> Double { /* ... */ }
private func processPayment(amount: Double, method: PaymentMethod) async throws -> PaymentResult { /* ... */ }
}
// ViewController 只和 Facade 交互
class CheckoutViewController: UIViewController {
private let checkoutFacade = CheckoutFacade()
func checkout() {
Task {
do {
let result = try await checkoutFacade.checkout()
showSuccess("订单号:\(result.orderId)")
} catch {
showError(error.localizedDescription)
}
}
}
}
3. 委托模式的应用
// 违反迪米特法则:直接访问内部对象
class MusicPlayer {
let audioEngine: AVAudioEngine
let playerNode: AVAudioPlayerNode
func play() {
// 外部可以直接访问和操作内部组件
playerNode.play()
}
}
// 外部代码可能这样使用
let player = MusicPlayer()
player.audioEngine.connect(player.playerNode, to: someOtherNode, format: nil) // 违反!
player.playerNode.volume = 0.5 // 违反!
// 遵循迪米特法则:隐藏内部实现
class MusicPlayer {
private let audioEngine: AVAudioEngine
private let playerNode: AVAudioPlayerNode
var volume: Float {
get { playerNode.volume }
set { playerNode.volume = newValue }
}
var isPlaying: Bool {
playerNode.isPlaying
}
func play() {
do {
try audioEngine.start()
playerNode.play()
} catch {
print("Failed to start audio engine: \(error)")
}
}
func pause() {
playerNode.pause()
}
func stop() {
playerNode.stop()
audioEngine.stop()
}
}
// 外部代码只能通过公开的接口操作
let player = MusicPlayer()
player.volume = 0.5
player.play()
4. 数据传输对象(DTO)
// 违反迪米特法则:直接传递领域对象
class OrderDetailViewController: UIViewController {
var order: Order? // Order 是复杂的领域对象
func updateUI() {
// ViewController 需要了解 Order 的完整结构
titleLabel.text = order?.id
dateLabel.text = order?.createdAt.formatted()
statusLabel.text = order?.status.displayName
// 访问嵌套对象
customerLabel.text = order?.customer.name
addressLabel.text = order?.customer.address.fullAddress
// 计算派生数据
let itemCount = order?.items.reduce(0) { $0 + $1.quantity }
itemCountLabel.text = "\(itemCount ?? 0) 件商品"
}
}
// 遵循迪米特法则:使用 DTO/ViewModel
struct OrderDetailViewModel {
let orderId: String
let formattedDate: String
let statusText: String
let customerName: String
let deliveryAddress: String
let itemCountText: String
let totalPriceText: String
init(order: Order) {
self.orderId = order.id
self.formattedDate = order.createdAt.formatted()
self.statusText = order.status.displayName
self.customerName = order.customer.name
self.deliveryAddress = order.customer.address.fullAddress
let itemCount = order.items.reduce(0) { $0 + $1.quantity }
self.itemCountText = "\(itemCount) 件商品"
self.totalPriceText = "¥\(order.totalAmount)"
}
}
class OrderDetailViewController: UIViewController {
var viewModel: OrderDetailViewModel? // 只依赖简单的 ViewModel
func updateUI() {
// ViewController 只需要知道 ViewModel 的扁平结构
titleLabel.text = viewModel?.orderId
dateLabel.text = viewModel?.formattedDate
statusLabel.text = viewModel?.statusText
customerLabel.text = viewModel?.customerName
addressLabel.text = viewModel?.deliveryAddress
itemCountLabel.text = viewModel?.itemCountText
}
}
5. 协议隔离
// 违反迪米特法则:依赖完整对象
class ReportGenerator {
func generateSalesReport(orders: [Order]) -> Report {
// 只需要订单金额,却依赖了完整的 Order 对象
let totalSales = orders.reduce(0.0) { $0 + $1.totalAmount }
return Report(type: .sales, value: totalSales)
}
}
// 遵循迪米特法则:只依赖需要的接口
protocol HasAmount {
var amount: Double { get }
}
extension Order: HasAmount {
var amount: Double { totalAmount }
}
class ReportGenerator {
func generateSalesReport(items: [HasAmount]) -> Report {
// 只依赖需要的数据
let totalSales = items.reduce(0.0) { $0 + $1.amount }
return Report(type: .sales, value: totalSales)
}
}
// 可以传入任何符合 HasAmount 的类型
let orders: [Order] = []
let report = generator.generateSalesReport(items: orders)
优缺点分析
优点
1. 降低耦合
// 高耦合:A 依赖 B, C, D
class A {
func doSomething(b: B) {
b.c.d.action() // A 知道 B, C, D 的结构
}
}
// 低耦合:A 只依赖 B
class A {
func doSomething(b: B) {
b.action() // A 只知道 B
}
}
class B {
private let c: C
func action() {
c.performAction()
}
}
2. 提高可维护性
当内部结构变化时,只需要修改封装层:
// 地址结构从 city 字符串变成 City 对象
// 只需要修改 Address 的 cityName 计算属性
struct Address {
// 旧实现
// let city: String
// var cityName: String { city }
// 新实现
let city: City
var cityName: String { city.name }
}
// 外部代码无需修改
3. 便于测试
依赖更少,Mock 更简单:
// 高耦合版本需要 Mock 整个依赖链
let mockOrder = Order(
customer: Customer(
address: Address(
city: City(name: "Beijing")
)
)
)
// 低耦合版本只需要简单的 Mock
struct MockOrder: OrderDisplayable {
var customerCityName: String? = "Beijing"
}
缺点
1. 可能产生大量包装方法
class Wrapper {
private let inner: Inner
// 需要暴露 inner 的每个需要的方法
func method1() { inner.method1() }
func method2() { inner.method2() }
func method3() { inner.method3() }
// ... 可能有很多
}
2. 增加间接层
// 直接访问(简单但违反 LoD)
user.wallet.creditCard.number
// 间接访问(符合 LoD 但有中间层)
user.creditCardNumber // -> wallet.creditCardNumber -> creditCard.number
3. 可能导致接口膨胀
为了不暴露内部对象,可能需要在外层添加很多委托方法。
权衡建议
适合严格遵循的场景:
- 公共 API / SDK 设计
- 跨模块边界
- 核心业务逻辑
可以适度放松的场景:
- 同一模块内部
- 数据模型的属性访问
- 原型/实验性代码
// 模块内部,简单的数据访问可以接受链式调用
// 这在同一个模块内是可以接受的
let userName = response.data.user.name
// 但对外暴露的 API 应该封装好
public var userName: String? {
response.data.user.name
}