长对象调用链是否有任何内在错误?

时间:2008-10-01 20:40:04

标签: design-patterns architecture method-chaining law-of-demeter

我已经按层次结构组织了代码,并且发现自己使用以下代码爬上树。

File clientFolder = task.getActionPlan().getClientFile().getClient().getDocumentsFolder();

我没有钻进task对象;我正在向它的父母钻研,所以我认为我在封装方面没有失去任何东西;但是在我的脑海里,一面旗帜正在传递,告诉我这样做会有一些肮脏的东西。

这是错的吗?

20 个答案:

答案 0 :(得分:5)

标志为红色,并以粗体

表示两件事
  • 要跟随链,调用代码必须知道整个树结构,这不是很好的封装,并且
  • 如果层次结构发生变化,您将需要编辑大量代码

括号内有一件事:

  • 使用属性,即task.ActionPlan而不是task.getActionPlan()

一个更好的解决方案可能是 - 假设您需要在子级别公开树上的所有父级属性 - 继续实现对子级的直接属性,即

File clientFolder = task.DocumentsFolder;

这至少会隐藏调用代码中的树结构。在内部,属性可能如下所示:

class Task {
    public File DocumentsFolder {
        get { return ActionPlan.DocumentsFolder; }
    }
    ...
}
class ActionPlan {
    public File DocumentsFolder {
        get { return ClientFile.DocumentsFolder: }
    }
    ...
}
class ClientFile {
    public File DocumentsFolder {
        get { return Client.DocumentsFolder; }
    }
    ...
}
class Client {
    public File DocumentsFolder {
        get { return ...; } //whatever it really is
    }
    ...
}

但是如果树结构将来发生变化,您只需要更改树中涉及的类中的访问器函数,而不是每个调用链的地方。

[此外,在属性函数中正确捕获和报告空值会更容易,这在上面的示例中省略了]

答案 1 :(得分:4)

好吧,每次间接都会增加一个可能出错的地方。

特别是,这个链中的任何方法都可以返回null(理论上,在你的情况下,你可能有不可能的方法),当发生这种情况时,你会知道它发生在一个那些方法,但不是哪一个

因此,如果任何方法都有可能返回null,我至少会在这些点上拆分链,并存储在中间变量中,并将其分解为单独的行,以便崩溃报告将给我一个行号来看。

除此之外,我看不出任何明显的问题。如果你有或者可以保证空引用不会成为问题,它就可以做你想要的。

可读性如何?如果添加命名变量会更清楚吗?总是编写代码,就像你打算由同事程序员阅读一样,只是偶然地被编译器解释。

在这种情况下,我必须阅读方法调用链并弄清楚...好吧,它获取一个文档,它是客户端的文档,客户端来自...文件...右,并且该文件来自行动计划等。长链可能会使其比可读性低,例如:

ActionPlan taskPlan = task.GetActionPlan();
ClientFile clientFileOfTaskPlan = taskPlan.GetClientFile();
Client clientOfTaskPlan = clientFileOfTaskPlan.GetClient();
File clientFolder = clientOfTaskPlan.getDocumentsFolder();

我想这归结为个人对此事的看法。

答案 2 :(得分:3)

Getters and setters are evil。通常,避免让对象使用它做某事。而是委托任务本身。

而不是

Object a = b.getA();
doSomething(a);

DO

b.doSomething();

与所有设计原则一样,不要盲目跟随。没有吸气剂和制定者,我从来没有能够编写任何远程复杂的东西,但这是一个很好的准则。如果你有很多getter和setter,那可能意味着你做错了。

答案 3 :(得分:2)

首先,堆叠这样的代码会让分析NullPointerExceptions并在步进调试器时检查引用很烦人。

除此之外,我认为这一切都归结为:来电者是否需要拥有所有这些知识?

也许它的功能可以更通用;然后可以将文件作为参数传递。或者,也许ActionPlan甚至不应该透露它的实现是基于ClientFile吗?

答案 4 :(得分:1)

你是否真的会独立使用这些功能中的每一个?为什么不让make任务有一个GetDocumentsFolder()方法来完成为你调用所有这些方法的所有脏工作?然后你就可以做所有肮脏的工作,无需检查所有内容,而无需在不需要被逮捕的地方抄袭你的代码。

答案 5 :(得分:1)

多么及时。我今晚要在我的博客上写一篇关于这种气味的帖子Message Chains,而不是它的逆Middle Man

无论如何,更深层次的问题是为什么你有一个看似域对象的“获取”方法。如果你密切关注问题的轮廓,你会发现告诉任务获取某些东西是没有意义的,或者你正在做的事情实际上是非商业逻辑问题,比如准备UI显示,持久性,对象重建等。

在后一种情况下,只要它们是used by authorized classes,“get”方法就可以了。您如何实施该策略取决于平台和流程。

因此,在“get”方法被认为是正确的情况下,您仍然必须面对问题。不幸的是,我认为这取决于正在导航链的类。如果该类适合于结构(例如,工厂),那就让它成为。否则,您应该尝试Hide Delegate

修改:click here for my post

答案 6 :(得分:1)

我同意提到德米特定律的海报。您正在做的是为许多这些类的实现以及层次结构本身创建不必要的依赖项。这将使得单独测试代码变得非常困难,因为您需要初始化十几个其他对象才能获得要测试的类的工作实例。

答案 7 :(得分:0)

根据您的最终目标,您可能希望使用最少知识的原则来避免重度耦合并最终导致您损失。首先,首先喜欢把它说成“只与你的朋友交谈。”

答案 8 :(得分:0)

另一方面,Law of Demeter isn't universally applicable,也不是硬规则(arrr,it be more of a guideline!)。

答案 9 :(得分:0)

好的,因为其他人指出代码不是很好,因为您将代码锁定到特定的层次结构。它可能会出现调试问题,并且读取起来并不好,但主要的一点是,任务的代码对于遍历获取某些文件夹的东西知之甚少。美元到甜甜圈,有人想在中间插入一些东西。 (所有任务都在任务列表中等)

走出困境,所有这些类都是同一个特殊的名字吗?即他们是分层的,但每个级别可能有一些额外的属性?

因此,从不同的角度来看,我将简化为枚举和接口,如果子类不是被请求的东西,子类会委托链。为了争论,我称之为文件夹。

enum FolderType { ActionPlan, ClientFile, Client, etc }

interface IFolder
{
    IFolder FindTypeViaParent( FolderType folderType )
}

并且实现IFolder的每个类可能只是

IFolder FindTypeViaParent( FolderType folderType )
{
    if( myFolderType == folderType )
        return this;

    if( parent == null )
        return null;

    IFolder parentIFolder = (IFolder)parent;
    return parentIFolder.FindTypeViaParent(folderType)
}

变体是制作IFolder界面:

interface IFolder
{
    FolderType FolderType { get; }
    IFolder Parent { get; }
}

这允许您外部化遍历代码。然而,这可以控制远离类(可能它们有多个父级)并暴露实现。好与坏。

[随笔

乍一看,这似乎是一个非常昂贵的层次结构。我是否需要每次都自上而下实例化?即如果某些东西只需要一个任务,你是否必须自下而上实例化所有这些后向指针?即使它是懒惰的,我是否需要走上层次结构才能获得根目录?

那么,层次结构真的是对象身份的一部分吗?如果不是,也许您可​​以将层次结构外部化为n-ary树。

作为旁注,您可能需要考虑聚合的DDD(域驱动设计)概念,并确定主要参与者是谁。负责的最终所有者对象是什么?例如一辆车的轮子。在对汽车进行建模的设计中,车轮也是物体,但它们归汽车所有。

也许它适合你,也许它不适合你。就像我说的,这只是在黑暗中拍摄的。

答案 10 :(得分:0)

我也指出了德米特定律。

并添加一篇关于Tell, Don't Ask

的文章

答案 11 :(得分:0)

链接的另一个重要副产品是性能。

在大多数情况下这并不是什么大问题,但特别是在循环中,你可以通过减少间接来看到合理的性能提升。

链接也使得评估性能变得更难,你无法分辨哪些方法可能会或可能不会做复杂的事情。

答案 12 :(得分:0)

世界上最大的旗帜。

如果这些invokations中的任何一个返回一个null对象,那么你就无法轻易检查,从而无法跟踪任何类型的错误!

getClientFile()可能会返回null,然后getClient()将失败,当你捕获它时,假设你正在尝试捕获,你将无法知道哪一个失败了。

答案 13 :(得分:0)

这个问题非常接近https://stackoverflow.com/questions/154864/function-chaining-how-many-is-too-many#155407

一般来说,人们似乎认为太长的连锁店并不好,你最多应该坚持一两个链式电话。

虽然我听说Python粉丝认为链接很有趣。这可能只是一个谣言...: - )

答案 14 :(得分:0)

答案 15 :(得分:0)

这并不坏,但在6个月内你可能会遇到问题。或者同事可能在维护/编写代码时遇到问题,因为对象链很长......

我认为您不必在代码中引入变量。因此,对象确实知道从方法跳转到方法所需的全部内容。 (这里出现的问题是,如果你没有过度工程,但我该告诉谁?)

我会介绍一种“便利方法”。想象一下,你的“任务”中有一个方法 - 像

这样的对象
    task.getClientFromActionPlan();

然后你肯定可以使用

    task.getClientFromActionPlan().getDocumentsFolder();

更具可读性,如果你正确地使用这些“便利方法”(即对于重度使用的对象链),更不用说类型;-)。

伊迪丝说:我建议这些便捷方法在编写时经常包含Nullpointer检查。这样你甚至可以在Nullpointers中输入好的错误消息(即“ActionPlan在尝试从中检索客户端时为null”)。

答案 16 :(得分:0)

这取决于。你不应该通过像这样的对象到达。 如果您控制这些方法的实现,我建议您进行重构,这样您就不必这样做了。

否则,我认为做你正在做的事没有害处。它肯定比

更好
ActionPlan AP = task.getActionPlan();
ClientFile CF = AP.getClientFile();
Client C = CF.getClient();
DocFolder DF = C.getDocumentsFolder();

答案 17 :(得分:0)

这是一个主观问题,但我认为在某一点上没有任何问题。例如,如果链超出了编辑器的可读区域,那么您应该介绍一些本地人。例如,在我的浏览器上,我看不到最后3个电话,所以我不知道你在做什么:)。

答案 18 :(得分:0)

是。这不是最好的做法。首先,如果有错误,就很难找到它。例如,您的异常处理程序可能会显示一个堆栈跟踪,显示您在第121行有一个NullReferenceException,但这些方法中的哪一个返回null?你必须深入研究代码才能找到答案。

答案 19 :(得分:0)

获取空值或无效结果的可能性有多大?该代码依赖于许多函数的成功返回,并且可能更难以排除诸如空指针异常之类的错误。

对于调试器来说也是坏事:由于你必须运行函数而不仅仅是观察一些局部变量,而且难以进入链中的后续函数,因此信息量较少。