如何解决违反德米特法的行为?

时间:2009-01-22 10:17:00

标签: language-agnostic design-patterns coupling law-of-demeter

我和一位同事为我们的客户设计了一个系统,我们认为我们创造了一个漂亮的干净设计。但是我遇到了一些我们引入的耦合问题。我可以尝试创建一个包含与我们的设计相同的问题的示例设计,但如果您原谅我,我将创建一个设计摘录来支持这个问题。

我们正在开发一种为患者注册某些治疗方案的系统。为了避免链接到图像,我将概念性UML类图描述为c#样式类定义。

class Discipline {}
class ProtocolKind 
{ 
   Discipline; 
}
class Protocol
{
   ProtocolKind;
   ProtocolMedication; //1..*
}
class ProtocolMedication
{
   Medicine;
}
class Medicine
{
   AdministrationRoute;
}
class AdministrationRoute {}

我将尝试解释一下设计,协议是新治疗的模板。并且协议属于某种类型并且具有需要施用的药物。根据协议,对于相同的药物(以及其他事物),剂量可以不同,因此存储在ProtocolMedication类中。 AdministrationRoute是药物的管理方式,与协议管理分开创建/更新。

我发现以下地方违反了得墨忒耳法则:

违反得墨忒耳法

BLL内部

例如,在ProtocolMedication的业务逻辑中,存在依赖于药物的AdministrationRoute.Soluble属性的规则。代码将成为

if (!Medicine.AdministrationRoute.Soluble)
{
   //validate constrains on fields
}

在存储库内

列出某个学科中所有协议的方法将写成:

public IQueryable<Protocol> ListQueryable(Discipline discipline)
{
    return ListQueryable().Where(p => (p.Kind.Discipline.Id == discipline.Id)); // Entity Frameworks needs you to compare the Id...
}

在用户界面内

我们使用ASP.NET(没有MVC)作为我们系统的界面,在我看来这个层目前有最严重的违规行为。 gridview的数据绑定(必须显示协议的Discipline的列必须绑定到Kind.Discipline.Name),这是字符串,所以没有编译时错误

<asp:TemplateField HeaderText="Discipline" SortExpression="Kind.Discipline.Name">
   <ItemTemplate>
      <%# Eval("Kind.Discipline.Name")%>
   </ItemTemplate>
</asp:TemplateField>

所以我认为实际的问题可能是,何时可以将其视为Demeter的建议,以及如何解决违反Demeter法的问题?

我对自己有一些想法,但我会将它们作为答案发布,以便他们可以单独评论和投票。 (我不确定这是怎么做的,如果没有,我会删除我的答案并将它们添加到问题中。)

10 个答案:

答案 0 :(得分:30)

我对Demeter法的后果的理解似乎与DrJokepu的不同 - 每当我将它应用于面向对象的代码时,它会导致更严格的封装和内聚,而不是在程序代码中向合同路径添加额外的getter

维基百科的规则为

  更正式地说,得墨忒耳的法则   函数需要一个方法M   对象O只能调用   方法有以下几种   对象:

     
      
  1. O本身
  2.   
  3. M的参数
  4.   
  5. 在M
  6. 中创建/实例化的任何对象   
  7. O的直接组件对象
  8.   

如果您的方法以“厨房”作为参数,Demeter说您无法检查厨房的组件,而不是您只能检查直接组件。

  

编写一堆函数只是为了满足像这样的Demeter法

Kitchen.GetCeilingColour()
     

看起来对我来说完全浪费时间,实际上是我完成任务的方式

如果厨房以外的方法通过厨房,严格的Demeter它也不能调用任何关于GetCeilingColour()结果的方法。

但无论哪种方式,重点是消除对结构的依赖,而不是将结构的表示从一系列链式方法移动到方法的名称。在Dog类中创建诸如MoveTheLeftHindLegForward()之类的方法对于实现Demeter没有任何作用。相反,请致电dog.walk()并让狗自己动手。

  

例如,如果需求发生变化并且我也需要天花板高度怎么办?

我会重构代码,以便您使用房间和天花板:

interface RoomVisitor {
  void visitFloor (Floor floor) ...
  void visitCeiling (Ceiling ceiling) ...
  void visitWall (Wall wall ...
}

interface Room { accept (RoomVisitor visitor) ; }

Kitchen.accept(RoomVisitor visitor) {
   visitor.visitCeiling(this.ceiling);
   ...
}

或者你可以通过将天花板的参数传递给visitCeiling方法来进一步消除吸气剂,但这通常会引入脆弱的耦合。

将它应用于医学示例,我希望SolubleAdminstrationRoute能够验证药物,或者至少调用药物的validateForSolubleAdministration方法,如果药物类中包含的信息是验证所必需的。

然而,Demeter适用于OO系统 - 其中数据被封装在对数据进行操作的对象中 - 而不是您正在谈论的系统,其具有不同的层,在愚蠢的可导航结构中的层之间传递数据。我不认为Demeter必须像单片或基于消息的那样容易地应用于这样的系统。 (在基于消息的系统中,您无法导航到不在消息克中的任何内容,因此无论您是否喜欢,您都会被Demeter困住)

答案 1 :(得分:21)

我知道我会被彻底毁灭,但我必须说我不喜欢得墨忒耳的法则。当然,像

这样的事情
dictionary["somekey"].headers[1].references[2]

真的很难看,但请考虑一下:

Kitchen.Ceiling.Coulour

我没有反对这一点。编写一堆函数只是为了满足像这样的Demeter法

Kitchen.GetCeilingColour()

看起来对我来说完全浪费时间,实际上是我完成工作的方式。例如,如果要求发生变化并且我也需要天花板高度怎么办?根据Demeter法则,我将不得不在Kitchen中编写另一个函数,这样我就可以直接获得Ceiling高度,最后我会在各处获得一些微小的getter函数,这些都是我认为非常混乱的事情。 / p>

编辑:让我重新说一下我的观点:

这个抽象事物的水平是否如此重要以至于我会花时间写3-4-5级的getter / setter?它真的能让维护更轻松吗?最终用户获得了什么吗?值得我花时间吗?我不这么认为。

答案 2 :(得分:11)

Demeter违规的传统解决方案是“告诉,不要问”。换句话说,根据您的状态,您应该告诉托管对象(您拥有的任何对象)采取某些操作 - 它将根据自己的状态决定是否按照您的要求执行操作。

作为一个简单的例子:我的代码使用了一个日志框架,我告诉我的记录器我想输出一个调试消息。然后,记录器根据其配置(可能未启用调试)决定是否实际将消息发送到其输出设备。在这种情况下,LoD违规将是我的对象询问记录器它是否会对消息做任何事情。通过这样做,我现在将我的代码与记录器内部状态的知识相结合(是的,我故意选择了这个例子)。

但是,此示例的关键点是记录器实现行为

我认为LoD发生故障的时候是处理代表数据的对象没有行为

在这种情况下,IMO遍历对象图与将XPath表达式应用于DOM没有什么不同。添加诸如“isThisMedicationWarranted()”之类的方法是一种更糟糕的方法,因为现在你在对象中分配业务规则,使它们更难理解。

答案 3 :(得分:4)

我和你们中的许多人一样苦苦挣扎,直到我看到“干净的代码会谈”会议:

"Don't Look For Things"

该视频可以帮助您更好地使用依赖注入,这本身可以解决LoD的问题。通过稍微改变您的设计,您可以在构造父对象时传入许多较低级别的对象或子类型,从而防止父级必须通过子对象遍历依赖关系链。

在您的示例中,您需要将AdministrationRoute传递给ProtocolMedication的构造函数。你必须重新设计一些事情才有意义,但这就是想法。

话虽如此,作为LoD的新手,没有专家,我倾向于同意你和Dr.Jokepu。像大多数规则一样,LoD可能有例外,它可能不适用于您的设计。

[迟了几年,我知道这个答案可能对初始人没有帮助,但这不是我发布这个的原因]

答案 4 :(得分:2)

我必须假设需要Soluble的业务逻辑也需要其他东西。如果是这样,它的某些部分是否可以以有意义的方式包含在医学中(比Medicine.isSoluble()更有意义?)

另一种可能性(可能是一种过度杀伤而不是同时完整的解决方案)是将业务规则作为自己的对象呈现并使用双重调度/访问者模式:

RuleCompilator
{
  lookAt(Protocol);
  lookAt(Medicine);
  lookAt(AdminstrationProcedure) 
}

MyComplexRuleCompilator : RuleCompilator
{
  lookaAt(Protocol)
  lookAt(AdminstrationProcedure)
}

Medicine
{
  applyRuleCompilator(RuleCompilator c) {
    c.lookAt(this);
    AdministrationProtocol.applyRuleCompilator(c);
  }
}

答案 5 :(得分:1)

对于BLL,我的想法是在医学上添加一个属性:

public Boolean IsSoluble
{
    get { return AdministrationRoute.Soluble; } 
}

我认为这是关于得墨忒耳法的文章中描述的内容。但这会让这个课程混乱多少呢?

答案 6 :(得分:1)

关于第一个具有“可溶”属性的例子,我有几点评论:

  1. 什么是“AdministrationRoute”以及为什么开发人员希望从中获得药物的可溶性?这两个概念似乎完全不相关。这意味着代码不能很好地通信,并且可能会改进您已经拥有的类的分解。更改分解可能会让您看到解决问题的其他解决方案。
  2. 可溶性不是药物的直接成员。如果您发现必须直接访问它,那么也许它应该是直接成员。如果需要额外的抽象,则从医学中返回该额外的抽象(直接或通过代理或外观)。任何需要可溶性的东西都可以用于抽象,你可以对多种其他类型使用相同的抽象,例如底物或维生素。

答案 7 :(得分:1)

而不是一直为每个包含对象的每个成员提供getter / setter,你可以做一个更简单的更改,为将来的更改提供一些灵活性,就是给对象返回包含对象的方法。

E.g。在C ++中:

class Medicine {
public:
    AdministrationRoute()& getAdministrationRoute() const { return _adminRoute; }

private:
    AdministrationRoute _adminRoute;
};

然后

if (Medicine.AdministrationRoute.Soluble) ...

变为

if (Medicine.getAdministrationRoute().Soluble) ...

这使您可以灵活地将getAdministrationRoute()更改为例如根据需要从DB表中获取AdministrationRoute。

答案 8 :(得分:1)

我认为记住LoD的 raison d'être会有所帮助。也就是说,如果细节在关系链中发生变化,那么您的代码可能会中断。由于您拥有的类是接近问题域的抽象,因此如果问题保持不变,关系不可能改变,例如,Protocol使用Discipline来完成其工作,但是抽象是高水平,不太可能改变。想想信息隐藏,并且协议不可能忽略学科的存在,对吗?也许我对领域模型的了解......

协议和规则之间的这个链接不同于“实现”细节,例如列表的顺序,数据结构的格式等,这些细节可能因性能原因而改变。这是一个有点灰色的区域。

我认为如果你做了一个域模型,你会看到比你的C#类图更多的耦合。 [编辑]我在下面的图表中用虚线添加了我怀疑你的问题域中的关系:

UML Diagram of Domain model

另一方面,您可以通过应用Tell, don't ask metaphor

来重构代码
  

也就是说,你应该尽力告诉对象你想让他们做什么;不要问他们关于他们的状态的问题,做出决定,然后告诉他们该做什么。

您已经使用answer重构了第一个问题(BLL)。 (进一步抽象BLL的另一种方法是使用规则引擎。)

重构第二个问题(存储库),内部代码

    p.Kind.Discipline.Id == discipline.Id

可能会被使用标准API集合的某种.equals()调用所取代(我更像是一个Java程序员,所以我不确定C#的精确等价)。我们的想法是隐藏如何确定匹配的细节。

要重构第三个问题(在UI内部),我也不熟悉ASP.NET,但如果有办法告诉一个Kind对象返回Disciplines的名字(而不是而不是像在Kind.Discipline.Name中那样询问细节,这是尊重LoD的方式。

答案 9 :(得分:1)

第三个问题非常简单:Discipline.ToString()应评估Name属性 这样你只需拨打Kind.Discipline