我和一位同事为我们的客户设计了一个系统,我们认为我们创造了一个漂亮的干净设计。但是我遇到了一些我们引入的耦合问题。我可以尝试创建一个包含与我们的设计相同的问题的示例设计,但如果您原谅我,我将创建一个设计摘录来支持这个问题。
我们正在开发一种为患者注册某些治疗方案的系统。为了避免链接到图像,我将概念性UML类图描述为c#样式类定义。
class Discipline {}
class ProtocolKind
{
Discipline;
}
class Protocol
{
ProtocolKind;
ProtocolMedication; //1..*
}
class ProtocolMedication
{
Medicine;
}
class Medicine
{
AdministrationRoute;
}
class AdministrationRoute {}
我将尝试解释一下设计,协议是新治疗的模板。并且协议属于某种类型并且具有需要施用的药物。根据协议,对于相同的药物(以及其他事物),剂量可以不同,因此存储在ProtocolMedication类中。 AdministrationRoute是药物的管理方式,与协议管理分开创建/更新。
我发现以下地方违反了得墨忒耳法则:
例如,在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法的问题?
我对自己有一些想法,但我会将它们作为答案发布,以便他们可以单独评论和投票。 (我不确定这是怎么做的,如果没有,我会删除我的答案并将它们添加到问题中。)
答案 0 :(得分:30)
我对Demeter法的后果的理解似乎与DrJokepu的不同 - 每当我将它应用于面向对象的代码时,它会导致更严格的封装和内聚,而不是在程序代码中向合同路径添加额外的getter
维基百科的规则为
更正式地说,得墨忒耳的法则 函数需要一个方法M 对象O只能调用 方法有以下几种 对象:
- O本身
- M的参数
- 在M
中创建/实例化的任何对象- O的直接组件对象
醇>
如果您的方法以“厨房”作为参数,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)
我和你们中的许多人一样苦苦挣扎,直到我看到“干净的代码会谈”会议:
该视频可以帮助您更好地使用依赖注入,这本身可以解决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)
关于第一个具有“可溶”属性的例子,我有几点评论:
答案 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#类图更多的耦合。 [编辑]我在下面的图表中用虚线添加了我怀疑你的问题域中的关系:
另一方面,您可以通过应用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