长对象调用链有天生的错误吗?
我已经对代码进行了组织,以便对数据进行分层组织。我发现自己使用如下代码爬到树上:
File clientFolder = task.getActionPlan().getClientFile().getClient().getDocumentsFolder();
现在,由于我没有深入研究任务对象,而是深入研究了它的父母,所以我不认为我在封装方面失去了任何东西,但是脑海里浮现出一个旗帜告诉我用这种方式做事有些肮脏。
有什么想法吗?
解决方案
世界上最大的国旗。
我们无法轻松检查这些调用中的任何一个是否返回空对象,从而使跟踪任何类型的错误几乎是不可能的!
getClientFile()可能返回null,然后getClient()将失败,并且在捕获此错误时,假设我们尝试捕获,则不会知道哪个失败。
获得空值或者无效结果的可能性有多大?该代码取决于许多函数的成功返回,并且可能更难排除诸如空指针异常之类的错误。
这对调试器也不利:信息量少,因为我们必须运行函数而不是仅观察几个局部变量,并且笨拙地步入链中的后续函数。
是的。这不是最佳实践。一方面,如果存在错误,很难找到它。例如,异常处理程序可能会显示堆栈跟踪,该堆栈跟踪显示我们在第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)说:我建议这些便利方法在编写时经常包含Nullpointer-checking。这样,我们甚至可以将带有良好错误消息的Nullpointers放入(例如," ActionPlan在尝试从中检索客户端时为null")。
阅读有关《得墨meter耳定律》的信息。
相关讨论:
函数链接多少是多少?
这个问题非常接近https://stackoverflow.com/questions/154864/function-chaining-how-many-is-too-many#155407
通常,似乎人们同意太长的链条不好,因此我们最多应该坚持一到两个链式呼叫。
尽管我听说Python爱好者认为链接非常有趣。那可能只是个谣言... :-)
首先,像这样堆叠代码会使在调试器运行时分析NullPointerExceptions和检查引用变得很烦人。
除此之外,我认为这可以归结为:呼叫者是否需要所有这些知识?
也许可以使其功能更通用。然后可以将File作为参数传递。或者,也许ActionPlan甚至不应该透露其实现是基于ClientFile的?
根据最终目标,我们可能希望使用"最少知识的负责人"来避免繁重的耦合并最终使我们付出成本。正如首长所说的那样。"只与朋友交谈。"
我同意提到Demeter法则的海报。我们正在做的事情是对许多此类的实现以及层次结构本身的结构创建不必要的依赖关系。这将使单独测试代码变得非常困难,因为我们将需要初始化许多其他对象才能获得要测试的类的有效实例。
链接的另一个重要副产品是性能。
在大多数情况下,这并不是什么大问题,但是特别是在循环中,通过减少间接访问,我们可以看到性能的合理提升。
链接也使评估性能变得更加困难,我们无法分辨出哪种方法可能会或者可能不会做复杂的事情。
标记为红色,并以粗体显示两件事:
- 要遵循该链,调用代码必须知道整个树结构,这不是很好的封装,并且
- 如果层次结构发生变化,我们将需要编辑大量代码
括号中的一件事:
- 使用属性,即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 } ... }
但是,如果将来树结构发生变化,则只需更改树所涉及类中的访问器函数,而不必更改调用链的每个位置。
[另外,在属性函数中更容易正确捕获和报告null,这在上面的示例中已省略]
多么及时。今晚,我将在我的博客上写一篇帖子,介绍这种气味,即消息链,与之相反的中间人。
无论如何,一个更深层次的问题是,为什么要对似乎是域对象的对象使用" get"方法。如果我们密切关注问题的轮廓,我们会发现告诉任务去做某件事没有任何意义,或者我们正在做的事情实际上是与业务逻辑无关的事情,例如为UI显示做准备,持久性,对象重建等。
在后一种情况下,只要授权类使用" get"方法就可以。我们如何执行该策略取决于平台和流程。
因此,在"获取"方法被认为可以的情况下,我们仍然必须面对问题。不幸的是,我认为这取决于在链上进行导航的班级。如果适合将该类耦合到该结构(例如工厂),则顺其自然。否则,我们应该尝试隐藏代理。
编辑:点击这里查看我的帖子。
吸气剂和吸气剂是邪恶的。通常,避免让某个对象对其执行某些操作。而是委派任务本身。
代替
Object a = b.getA(); doSomething(a);
做
b.doSomething();
与所有设计原则一样,请勿盲目遵循。如果没有getter和setter,我永远无法编写任何远程复杂的东西,但这是一个很好的指导原则。如果我们有很多获取方法和方法,那可能意味着我们做错了。
我也要指出《得墨meter耳法则》。
并添加有关"告诉,不要问"的文章
我们实际上要独立使用其中的每个功能吗?为什么不仅仅让任务具有一个GetDocumentsFolder()方法,该方法可以为我们调用所有这些方法呢?然后,我们可以做所有的工作,即对所有内容进行空检查,而无需在不需要收拾代码的地方收拾代码。
好的,因为其他人指出该代码不是很好,因为我们将代码锁定在特定的层次结构中。它可能会带来调试方面的问题,而且阅读起来不太好,但是要点是,承担任务的代码对遍历以获得文件夹内容的了解太多。美元到甜甜圈,有人会想要在中间插入一些东西。 (所有任务都在任务列表中,等等)
一路走来,所有这些类仅仅是同一事物的特殊名称吗?即它们是分层的,但是每个级别可能都有一些额外的属性?
因此,从不同的角度讲,我将简化为一个枚举和一个接口,在该接口中,子类将不是必需的东西委托给链。为了争辩,我称它们为文件夹。
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元树。
附带说明一下,我们可能需要考虑DDD(域驱动设计)总体概念,并确定主要参与者是谁。负责的最终所有者对象是什么?例如汽车的轮子。在对汽车进行建模的设计中,车轮也是对象,但它们归汽车所有。
也许对我们有用,也许不行。就像我说的,这只是黑暗中的一枪。
另一方面,《得墨meter耳法则》并非普遍适用,也不是硬性规定(当然,这更像是一个指导原则!)。