37

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

我们正在开发一个系统,用于为患者注册某些治疗。为了避免到图像的链接断开,我将把 UML 概念类图描述为 ac# 样式类定义。

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 的数据绑定(必须显示协议纪律的列必须绑定到 Kind.Discipline.Name),它们是字符串,所以没有编译时错误

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

所以我认为实际的问题可能是,什么时候可以将其更多地视为德墨忒耳的建议,以及如何解决违反德墨忒耳法则的行为?

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

4

10 回答 10

31

我对得墨忒耳定律后果的理解似乎与 DrJokepu 的不同——每当我将它应用于面向对象的代码时,都会导致更紧密的封装和内聚,而不是在程序代码的契约路径中添加额外的 getter。

维基百科的规则为

更正式地说,函数的得墨忒耳定律要求对象 O 的方法 M 只能调用以下类型对象的方法:

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

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

像这样写一堆函数来满足得墨忒耳定律

Kitchen.GetCeilingColour()

对我来说看起来完全是浪费时间,实际上得到是我完成工作的方式

如果将 Kitchen 之外的方法传递给厨房,则严格的 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 方法来完全消除 getter,但这通常会引入脆弱的耦合。

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

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

于 2009-01-22T13:33:08.697 回答
21

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

dictionary["somekey"].headers[1].references[2]

真的很丑,但考虑一下:

Kitchen.Ceiling.Colour

我对此没有任何反对意见。像这样写一堆函数来满足得墨忒耳定律

Kitchen.GetCeilingColour()

对我来说,这看起来完全是在浪费时间,实际上得到是我完成工作的方式。例如,如果要求发生变化,我也需要天花板高度怎么办?使用得墨忒耳定律,我将不得不在 Kitchen 中编写另一个函数,这样我就可以直接获得天花板高度,最后我会到处都有一堆微小的 getter 函数,我认为这是一团糟。

编辑:让我重新表述我的观点:

这种抽象级别的东西是否如此重要,以至于我将花时间编写 3-4-5 级别的 getter/setter?它真的使维护更容易吗?最终用户有什么收获吗?值得我花时间吗?我不这么认为。

于 2009-01-22T10:47:58.437 回答
12

Demeter 违规的传统解决方案是“告诉,不要问”。换句话说,根据你的状态,你应该告诉一个托管对象(你持有的任何对象)采取一些行动——它会根据自己的状态决定是否按照你的要求去做。

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

但是,这个例子的关键点是记录器实现了行为

我认为 LoD 发生故障的地方是在处理表示数据的对象时,没有任何行为

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

于 2009-01-22T13:57:57.883 回答
4

就像你们中的许多人一样,我一直在为 LoD 苦苦挣扎,直到我观看了 The Clean Code Talks” 会议:

《不要找东西》

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

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

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

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

于 2012-07-16T23:44:54.653 回答
2

我不得不假设需要可溶性的业务逻辑也需要其他东西。如果是这样,它的某些部分是否可以以有意义的方式封装在医学中(比 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);
  }
}
于 2009-01-22T12:57:08.457 回答
1

对于 BLL,我的想法是在 Medicine 上添加一个属性,如下所示:

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

这就是我认为关于得墨忒耳法则的文章中描述的内容。但这会给课堂带来多大的混乱呢?

于 2009-01-22T10:18:17.653 回答
1

关于“可溶”属性的第一个示例,我有几点意见:

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

与其一路走来为每个包含对象的每个成员提供 getter/setter,您可以进行的一种更简单的更改为您提供一些未来更改的灵活性,即为对象提供返回其包含对象的方法。

例如在 C++ 中:

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

private:
    AdministrationRoute _adminRoute;
};

然后

if (Medicine.AdministrationRoute.Soluble) ...

变成

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

这使您可以灵活地在将来更改 getAdministrationRoute(),例如按需从数据库表中获取 AdministrationRoute。

于 2009-01-22T11:27:52.427 回答
1

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

Protocol 和 Discipline 之间的这种联系不同于“实施”细节,例如列表的顺序、数据结构的格式等,这些细节可能会因性能原因而改变。确实,这是一个有点灰色的区域。

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

领域模型UML图
(来源:plantuml.com

另一方面,你总是可以通过应用Tell, don't ask重构你的代码:

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

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

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

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

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

为了重构第三个问题(在 UI 内部),我也不熟悉 ASP.NET,但是如果有办法告诉Kind 对象返回 Disciplines 的名称(而不是像在 Kind. Discipline.Name),这是尊重 LoD 的方法。

于 2013-08-08T04:57:52.837 回答
1

第三个问题很简单:Discipline.ToString()应该评估Name属性 那样你只调用Kind.Discipline

于 2013-10-18T22:03:54.400 回答