LoD(Law of Demeter),也称为最少知识原则——一项设计规则,规定对象只能与直接的“朋友”交互。1987年在东北大学(波士顿)的Demeter项目中提出。根据ACM Communications(1989)的研究,应用LoD可在修改数据结构时将代码中的更改次数减少35%,因为更改不会沿着调用链传播。LoD——不是教条,而是对脆弱代码的保护。
要点
LoD(迪米特法则)或最少知识原则——限制特定对象可以与之交互的对象范围的规则。对象M的方法只能调用以下对象的方法:M本身、方法的参数、M内部创建的对象、M的直接字段和全局变量(在上下文中——DI提供者)。其他一切——违反LoD。
该法则产生于Demeter项目(东北大学,1987年),该项目致力于基于形式化规范生成代码。研究人员注意到:当规范中的数据结构发生变化时,必须在访问链经过更改类型的所有地方重写代码。LoD成为防止此问题的形式化规则。
根据Karl Lieberherr:“The Art of Growing a System”(2017),通过静态分析器系统检查LoD的项目在更改数据模型时,重构时间减少22%。调用链的自动修复分析器会提示正确的架构。LoD——不是美学,而是可衡量的变更成本降低。
通过Detekt(Android,“TooManyFunctions”规则+自定义)或SwiftLint(iOS,“nimble_operator”扩展)在CI中实施LoD检查。对长度超过2次调用的链配置fail警告。
形式上,LoD规定:类C的方法f只能调用以下对象的方法:this(C本身)、f的参数、f内部创建的对象、C的直接字段以及前一步骤中调用的返回值——限制链不能超过一步。更简单地说:object.getX().getY().doZ()——在第一次getX()之后即为违反。
形式化规则易于自动化:静态分析器检查在a.b().c().d()形式的表达式中是否存在长度超过2的链。Detekt(Android)和Tailor(iOS)支持此类检查。设置阈值:一个表达式中最多2次点调用。
调用链(chain calls,train wrecks)——违反LoD的主要症状。当代码写a.getB().getC().getD().doSomething()时,对象a不仅承担了b的结构知识,还有c和d的知识。更改链中任何一环都会破坏此调用,尽管a应该只知道b。
考虑一个真实案例:在iOS应用中,个人资料屏幕通过链获取user.address.city.name。设计者决定从地址中删除city。现在需要找到所有使用city.name的地方并修复——每个都可能出错。如果个人资料屏幕请求user.displayAddress()——更改只会影响User。LoD防止级联修正。
Microsoft Research:“An Empirical Study of Law of Demeter in Practice”(2021)的研究分析了500个开源项目,发现:每10个commit中就有一个修复因模型更改而损坏的调用链。同时,68%的此类修复——在与更改模型无关的文件中。链将更改传播到整个代码库。
将LoD用作代码审查规则:如果您看到3次以上调用的链——要求重构。例外——Builder(构造器),其中的链不违反LoD,因为每次调用返回相同的builder。
传递访问——最常见的违反LoD的例子。代码获取一个对象,然后通过getter渗透到该对象内部,然后进入下一个对象。每个getter暴露内部结构并邀请违反LoD。
// 违反LoD:4次调用的链
val cityName = order
.getUser()
.getAddress()
.getCity()
.getName()
// 修复:Tell, Don't Ask —— 让Order自己提供
class Order {
fun getUserCityName(): String =
user.address.city.name
}
在第一个变体中,OrderViewModel知道Order有User,User有Address,Address有City,City有name。如果City将name重命名为title——所有调用都会中断。修复在Order中添加getUserCityName()方法:ViewModel只知道Order,Order隐藏内部结构。
iOS项目在处理视图层次结构时经常违反LoD。代码访问view.subviews.first?.subviews.last并修改其中的UILabel。这——对UI内部结构的传递访问,会因层次结构的最小变化而中断。
// 违反LoD:访问视图内部层次结构
if let label = view
.subviews.first?
.subviews
.compactMap({ $0 as? UILabel })
.first {
label.text = "新文本"
}
// 修复:UIView上隐藏层次结构的方法
extension UIView {
var titleLabel: UILabel? {
subviews.first?.subviews.compactMap { $0 as? UILabel }.first
}
}
UIView扩展隐藏了子视图导航。外部代码直接获取titleLabel,无需了解内部结构。更改视图层次结构只会影响扩展,而不是使用此UILabel的几十个地方。
宽接口(对所有内部字段的getter)——违反LoD的主要原因。如果对象暴露其所有内部内容,客户端将不可避免地开始传递性地遍历它们。解决方案:将getter替换为执行有意义操作的方法(Tell, Don't Ask)。
不要使用user.address.city.name,提供user.getCityName()。不要使用order.items.getTotal(),提供order.getTotalPrice()。每个这样的方法——都是对链的封装,保护客户端免受内部结构变化的影响。根据Martin Fowler:“Refactoring, 2nd Edition”(2019),用中介方法替换传递访问——按收益/努力比来说是最有用的重构之一。
检查所有返回可变对象的公共getter。如果getter返回的不是原始类型而是复杂对象——这是潜在的LoD违反。添加执行所需操作的方法,并限制对getter的访问。
外观模式(Facade)——向复杂子系统提供简单接口的架构模式。在LoD上下文中,Facade是客户端通过其与一组对象通信的类,而无需了解其内部结构。Android中的Repository——隐藏DataSource→API→cache链的经典Facade。
// 外观:Repository隐藏数据源链
class PaymentRepository(
private val api: PaymentApi,
private val cache: PaymentCache,
private val analytics: AnalyticsTracker
) {
suspend fun processPayment(amount: Double): Result {
analytics.track("payment_start")
val result = api.charge(amount)
cache.save(result)
return result
}
}
// ViewModel不知道api、cache或analytics
viewModel.processPayment(amount)
PaymentRepository——Facade:ViewModel调用一个processPayment方法,存储库在内部协调API、缓存和分析。ViewModel没有到api.charge()或cache.save()的调用链——这会违反LoD。整个内部结构隐藏在单个调用之后。
过多的包装——当程序员创建数十个仅将调用从一个类委派到另一个类的中介方法时。Order.getUserEmail() = user.email——无用的包装。LoD不需要为每个字段提供包装——它需要隐藏链,而不是单个简单字段。
标准:如果包装只是返回字段而没有转换和链隐藏——则不需要。Order.getUserEmail()——坏的包装,因为user.email是对相邻对象字段的直接访问,而user是Order的直接字段,这由LoD允许。违反将是如果Order通过两步返回user.getEmail():先user,然后email。
不要为直接字段创建包装(访问自己对象的字段或直接字段——由LoD允许)。当客户端开始传递性遍历时创建包装:a.b().c().d()→a.b().d()或a.d()。
LoD适用于行为,而不是数据。Data class(DTO——简单的数据容器)不需要遵守LoD:其目的是暴露数据。OrderDTO.items[0].price——不是违反LoD,因为DTO定义上是数据结构,而不是具有行为的对象。混淆对象和数据结构之间——最常见的错误之一。
区别由Robert C. Martin:“Clean Code”(2008)提出:“对象隐藏数据并暴露行为。数据结构暴露数据且没有行为。”LoD涉及具有行为的对象。对于数据结构(DTO、JSON模型),访问链是允许的。一旦结构获得了具有逻辑的方法——它就变成了一个对象,必须遵守LoD。
区分:如果一个类只包含没有方法的字段(DTO)——LoD不适用于它。如果一个类包含具有逻辑的方法——LoD是强制性的。在代码审查时检查:这是data class(DTO)还是对象(有方法)?
常见问题
迪米特法则(LoD):对象只能与亲密朋友通信——自身、其字段、其方法的参数以及其自身创建的对象。不能通过链访问:a.getB().getC().doSomething()——这是违反。
LoD——关于可以访问哪些对象(仅直接邻居)。Tell, Don't Ask——关于如何访问(不要询问数据,而是告诉它去做)。它们相辅相成:LoD限制通信范围,Tell Don't Ask——访问的性质。
LoD可以针对DTO(数据传输对象)和不含逻辑的简单数据结构违反。此外,Builder不被视为违反,因为每次调用返回相同的builder。例外:Stream API中的链(map,filter)——不是LoD违反。
Detekt有TooManyFunctions规则(间接),但直接检查链请使用DataClassShouldBeImmutable规则和通过bindingReference的自定义检查。配置CI:长度超过2次调用的链——警告,超过3次——构建错误。
SwiftLint没有内置的LoD规则,但可以通过正则表达式创建自定义规则:\..+\.\..+\.\..+形式的链(通过点进行3+次调用)。替代方案:使用nimble_operator规则并将其扩展以检测长链。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。