了解德墨忒尔法则
德墨忒尔法则,简单地说,就是一个对象不应该 "穿过 "它的合作者来访问他们合作者的数据、方法或合作者。
该法则的名称参考了作者创造该法则时正在编码的项目名称。作者将该项目命名为 "Demeter",这本身就是对一种名为 "Zeus "的硬件描述语言的引用。德墨忒尔是收获和农业的女神,是宙斯的妹妹。
也许描述如何遵守德墨忒尔法则的最简单方法就是举出一个违反法则的例子。在这里,我们的游侠访问了dwarf.strength.modifier 和dwarf.proficiencies.include? ,这两件事都是违反律法的。
class Ranger
def toss_dwarf(dwarf, location)
dwarf_check = rand(20) + dwarf.strength.modifier + (dwarf.proficiencies.include?(:athletics) ? dwarf.proficiency_bonus : 0)
ranger_check = strength.modifier + (proficiencies.include?(:athletics) ? proficiency_bonus : 0)
if ranger_check > dwarf_check
dwarf.location = location
end
end
end
这里的问题是,Ranger对矮人如何工作的了解比使用其他方法时要多。这导致了对象的紧密耦合,这可能会使重构("简化修改")、测试和调试("简化编程的复杂性")更加困难。
德米特法则也被称为最小知识原则。这个概念最早是由Karl J. Lieberherr, Ian Holland和Arthur J. Riel提出的论文。
对于所有的类C,以及附属于C的所有方法M,M向其发送消息的所有对象必须是与以下类相关的实例。
- M的参数类(包括C)
- C的实例变量类
(由M创建的对象,或由M调用的函数或方法创建的对象,以及全局变量中的对象被认为是M的参数。)
对于使用这种语法的面向对象语言来说,不太正式,只在方法中使用一个点。
dwarf.strength.modifier 这是不对的,因为我们通过矮人到 ,得到 。strength modifier
dwarf.strength 和 是没有问题的,因为我们只通过dwarf得到 和 。dwarf.strength_modifier strength strength_modifier
德墨忒尔法则的好处
- 当合作者或你的系统发生变化时,使重构更容易。
- 使得写代码更容易:你限制了你需要考虑的直接合作者的API的数量。
- 让测试更容易:当你只需要构建直接合作者时,隔离的嘲弄和构建集成的对象就更简单。
不要违反法律
在Ruby中,有很好的方法来避免这种代码气味。
委托给前缀的方法
Rails有一个将方法委托给协作者的快捷方式,所以Dwarf可以这样做,以避免Ranger中的Demeter违规。
class Dwarf
delegate :modifier, to: :strength, prefix: true, allow_nil: true
end
在Dwarf的API上公开strength_modifier ,可以重构该方法,在保留行为的同时使用不同的内部实现。这也简化了对该方法的模拟,以便进行隔离测试。
告诉,不要问
与其让Ranger询问Dwarf的属性和技能来进行检查,不如教Dwarf自己进行能力检查,并将其作为dwarf.make_athletics_check 。
class Dwarf
def make_athletics_check
athletics_bonus = if proficiencies.include?(:athletics)
strength.modifier + proficiency_bonus
else
strength.modifier
end
rand(20) + athletics_bonus
end
end
将力量作为一个参数传递
从技术上讲,你可以通过在方法中传递strength 来满足这个要求,因为这样就可以接受调用strength.modifier 。这是因为strength 已经成为方法的一个参数,并且可以被接受来调用方法。在上面的例子中,这样做可能不会在你的拉动请求审查中获得通过,因为它为了遵守法律而把自己弄得很扭曲。如果有一些限制,阻止委托或告诉,不要问,传递强度可能是一种选择。在这种情况下,我会建议将其作为弯曲法律的时机,而不是通过modifier 。
规则的例外情况
德墨忒尔法则是关于通过物体接触其合作者。因此,当我们考虑与其他设计模式进行交互时,有一些例外情况。
构建器模式
例如,构建器模式期望有重要的方法链,这种类型的链不被认为是违反Demeter的。以ActiveRecord的查询接口为例,它使用构建器模式来建立SQL查询。
Dwarf
.where(friend: ranger)
.where(weapon: :axe)
.first # object created by our method
.name # one allowed method call
.split(",") # .split and .first are Demeter violations
.first
所有这些通过.first ,都是在使用ActiveRecord的查询生成器,所以你可以把这一切看作是为了这个规则而创建一个新对象。.name 是我们的 "一个点",所以.split.first 违反了规则,但就Demeter而言,在这一点上,方法链没有任何问题。
事实上,由于德墨忒尔法则是以类型来表述的,因为Dwarf.where 返回一个ActiveRecord::Relation 对象,这就成为我们自己创建的类型。任何返回类型为ActiveRecord::Relation 的对象的方法都可以被链起来,所以返回同一类型的修改对象的Builder模式是Demeter法则明确允许的。
在实践中,因为我们控制了Dwarf 对象,所以我们可以对它定义方法,如name_parts 。在上面的方法链中,.first 返回一个Dwarf ,所以这就是我们认为德墨忒尔法则开始被执行的地方。
Map/reduce
另一个可接受的例子是[].map {}.reduce {} ,这是一个成熟的数据操作模式。map 和reduce 的结果属于 "你创建的对象 "这一规则的例外,因为每个方法都返回一个由我们的方法创建的新对象。
与第三方数据或库进行交互
在消费API时,你可能会发现自己需要访问深度嵌套的数据,而你的应用程序并不是自己创建的。例如,要消费一个流行的纸牌游戏的ArkhamDB API,你可能需要深入挖掘,以了解你可以用这样的代码将哪些纸牌添加到牌组中。
card_json["deck_options"].each do |opt|
puts "#{opt["faction"]} (#{opt["level"]["min"]}–#{opt["level"]["max"]})"
end
在Ruby和其他弱类型、面向对象的语言中,这种类型的链式(记住.[] 是一种方法)是德墨忒尔法则的一个可接受的例外。在Go或Swift等强类型语言中,你可能会创建结构化的数据对象来解码JSON,在这种情况下,你可以定义方法,如card.factionOptions() ,以保持在法则之内。
你并不总是能控制宝石或库为你提供的API表面。有时你需要打破德墨忒尔法则来与你不拥有的代码进行交互,这也是可以接受的。我在这里提供的一个建议是,你可能想把第三方库和API封装到网关中,这样既可以在你自己的代码中提供一个缝隙来进行伪造,又可以封装那些外观和行为必然与你系统的其他部分不同的代码。
遵守德墨忒耳定律,或者不遵守
德墨忒耳定律指出了你代码中的潜在问题。虽然严格遵守规则是一种选择,但实用性和风格可能会促使你违反该法则。在一个代码库中,有少量这样的违规行为是可以的,你应该像对待其他最佳实践一样,在对你的代码有意义的时候自由地打破这个 "法则"。正如Martin Fowler所说,一个更好的名字可能是 "偶尔有用的Demeter建议"。
如果你确实看到了方法链,它应该作为一个值得猜测的东西脱颖而出,你应该考虑避免或重构该代码,但请放心考虑并决定不这样做。