Design patterns 长对象调用链有什么固有的错误吗?

Design patterns 长对象调用链有什么固有的错误吗?,design-patterns,architecture,method-chaining,law-of-demeter,Design Patterns,Architecture,Method Chaining,Law Of Demeter,我已经按层次组织了我的代码,我发现自己正在使用下面的代码爬树 File clientFolder = task.getActionPlan().getClientFile().getClient().getDocumentsFolder(); 我没有深入研究任务对象;我正在钻研它的父母,所以我不认为我在封装方面损失了任何东西;但我的脑海中有一面旗帜在飘扬,告诉我这样做有点肮脏 这不对吗?世界上最大的旗帜。 您无法轻松检查这些调用是否返回空对象,从而无法跟踪任何类型的错误 getClientFi

我已经按层次组织了我的代码,我发现自己正在使用下面的代码爬树

File clientFolder = task.getActionPlan().getClientFile().getClient().getDocumentsFolder();
我没有深入研究
任务
对象;我正在钻研它的父母,所以我不认为我在封装方面损失了任何东西;但我的脑海中有一面旗帜在飘扬,告诉我这样做有点肮脏


这不对吗?

世界上最大的旗帜。

您无法轻松检查这些调用是否返回空对象,从而无法跟踪任何类型的错误


getClientFile()可能会返回null,然后getClient()将失败,当您捕获此消息时,假设您正在尝试捕获,您将不知道哪一个失败。

获得null或无效结果的可能性有多大?这段代码依赖于许多函数的成功返回,并且可能更难对诸如空指针异常之类的错误进行排序


这对调试器也不好:信息量少,因为您必须运行函数而不是只查看几个局部变量,并且很难进入链中的后续函数。

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

这是一个主观问题,但我不认为在某种程度上有任何问题。例如,如果链延伸到编辑器的可读区域之外,那么应该引入一些局部变量。例如,在我的浏览器上,我看不到最后3个电话,所以我不知道你在做什么:)。

这要看情况而定。你不应该穿过那样的物体。 如果您控制这些方法的实现,我建议您进行重构,这样您就不必这样做

否则,我看不出你做的有什么害处。这肯定比我的好

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

好吧,每一个间接过程都会增加一个可能出错的点

特别是,此链中的任何方法都可能返回null(理论上,在您的案例中,您可能有无法返回null的方法),当这种情况发生时,您将知道它发生在其中一个方法上,但不知道发生在哪一个方法上

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

除此之外,我看不出它有任何明显的问题。如果您已经或能够保证空引用不会成为问题,那么它会做您想要的事情

可读性如何?如果添加命名变量,会更清晰吗?编写代码时,一定要像您希望其他程序员阅读的那样,并且只能偶尔由编译器进行解释

在这种情况下,我必须阅读方法调用链并找出。。。好的,它得到一个文件,这是一个客户的文件,客户来自一个。。。文件对,而且文件来自行动计划等。长链可能会使其可读性低于,比如:

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

我想这可以归结为个人对这件事的看法。

这本身并不坏,但你可能会在6个月内读到这篇文章。或者同事可能会在维护/编写代码时遇到问题,因为对象链非常复杂。。。长

我确认您不必在代码中引入变量。因此,对象确实知道从一个方法跳到另一个方法所需的一切。(如果你没有过度设计一点,就会产生一个问题,但我该告诉谁?)

我会介绍一种“方便方法”。假设您在“任务”中有一个方法-对象类似

    task.getClientFromActionPlan();
那你当然可以用

    task.getClientFromActionPlan().getDocumentsFolder();
更具可读性,如果您正确地使用了这些“方便方法”(即,对于大量使用的对象链),更不用说键入;-)

Edith说:我建议的这些方便的方法在编写它们时经常包含空指针检查。这样,您甚至可以在中抛出带有良好错误消息的空指针(即“ActionPlan在尝试从中检索客户端时为空”)。

相关讨论:


这个问题非常接近

总的来说,人们似乎都同意链条太长是不好的,你最多应该坚持一到两个链式呼叫


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

首先,像这样的代码堆积会使分析NullPointerException和检查引用时进入调试器变得烦人

除此之外,我认为这一切都可以归结为:打电话的人需要具备所有这些知识吗?


也许它的功能可以变得更通用;然后可以将该文件作为参数传递。或者,也许行动计划甚至不应该透露其实施是基于ClientFile的?

根据您的最终目标,您可能希望使用最少知识的原则来避免严重耦合,并最终降低成本。正如head首先喜欢说的。。“只和你的朋友说话。”

我同意海报上提到的德米特定律。您所做的是在这些类的实现和层次结构本身上创建不必要的依赖关系。隔离测试代码将非常困难,因为您需要初始化十几个其他对象,才能获得要测试的类的工作实例。

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

在大多数情况下,这不是什么大问题,但特别是在循环中,您可以看到一个合理的
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
    }
    ...
}
Object a = b.getA();
doSomething(a);
b.doSomething();
enum FolderType { ActionPlan, ClientFile, Client, etc }

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

    if( parent == null )
        return null;

    IFolder parentIFolder = (IFolder)parent;
    return parentIFolder.FindTypeViaParent(folderType)
}
interface IFolder
{
    FolderType FolderType { get; }
    IFolder Parent { get; }
}