在构造函数中合法的“实际工作”?


23

我正在设计,但是一直遇到障碍。我有一个特定的类(ModelDef),它实际上是通过解析XML模式(例如DOM)构建的复杂节点树的所有者。我想遵循良好的设计原则(SOLID),并确保生成的系统易于测试。我打算使用DI来将依赖项传递到ModelDef的构造函数中(以便在测试过程中可以根据需要轻松地将其替换掉)。

不过,我正在努力的是创建节点树。该树将完全由简单的“值”对象组成,而这些对象无需独立测试。(但是,我仍然可以将抽象工厂传递到ModelDef中,以帮助创建这些对象。)

但是我一直在读,构造函数不应该做任何实际的工作(例如Flaw:Constructor可以进行实际工作)。如果“实际工作”意味着构造重量较重的相关对象,而以后可能希望将其存根进行测试,则这对我来说非常有意义。(这些应通过DI传递。)

但是像该节点树这样的轻量级对象呢?必须在某个地方创建树,对不对?为什么不通过ModelDef的构造函数(例如使用buildNodeTree()方法)呢?

我真的不想在ModelDef之外创建节点树,然后再通过(通过构造函数DI)将其传递进去,因为通过解析架构来创建节点树需要大量复杂的代码-需要进行彻底测试的代码。我不想将其委托给“胶合”代码(这应该是相对琐碎的,并且可能不会被直接测试)。

我曾考虑过将代码创建节点树放在一个单独的“构建器”对象中,但是犹豫称它为“构建器”,因为它与“构建器模式”并不完全匹配(后者似乎与消除伸缩无关。构造函数)。但是,即使我将其称为其他名称(例如NodeTreeConstructor),也只是为了避免让ModelDef构造函数构建节点树而感到有些破绽。它必须建在某个地方。为什么不在要拥有它的对象中?


7
但是,您应该始终警惕诸如此类的笼统声明。一般的经验法则是,代码应清晰,功能正常,易于测试,重用和维护,无论采用哪种方式,具体取决于您的情况。如果您除了尝试遵循这样的“规则”而没有获得代码复杂性和困惑,那么这不是适合您情况的规则。所有这些“模式”和语言功能都是工具。为您的特定工作选择最合适的。
杰森C

Answers:


26

并且,除了罗斯·帕特森(Ross Patterson)提出的建议外,请考虑与这一相反的立场:

  1. 采取格言,例如“不要在您的构造函数中进行任何实际工作”。

  2. 实际上,构造函数不过是静态方法而已。因此,从结构上讲,它们之间确实没有太大区别:

    a)一个简单的构造函数和一堆复杂的静态工厂方法,以及

    b)一个简单的构造函数和一堆更复杂的构造函数。

对于在构造函数中进行任何实际工作的负面情绪的很大一部分来自C ++历史上的某个时期,当时有人在争论是否在构造函数内引发异常时对象将保留在什么状态,以及是否在这种情况下,应该调用析构函数。C ++历史的那部分已经结束,问题已经解决,而在Java之类的语言中,从来没有这样的问题开始。

我的意见是,如果您只是避免new在构造函数中使用(正如您打算使用依赖注入所表明的那样),那应该没问题。我嘲笑诸如“构造函数中的条件或循环逻辑是缺陷的警告信号”之类的语句。

除此之外,我个人将XML解析逻辑从构造函数中删除,这并不是因为在构造函数中包含复杂的逻辑是邪恶的,而是因为遵循“关注点分离”原则是一件好事。因此,我将把XML解析逻辑完全移到某个单独的类中,而不是移到属于您的ModelDef类的某些静态方法中。

修正案

我想如果您有一个方法ModelDef可以ModelDef从XML 创建一个外部方法,则需要实例化一些动态的临时树数据结构,通过解析XML来填充它,然后创建ModelDef该结构作为构造函数参数进行新的传递。因此,可以将其视为“构建器”模式的应用。您想要做的与StringStringBuilder对之间有一个非常相似的类比。但是,由于一些我不清楚的原因,我发现此问题与答案似乎不一致:Stackoverflow-StringBuilder和Builder Pattern。因此,为了避免在这里就是否StringBuilder实施“构建器”模式进行长时间的辩论,我想说,随时可以从中受到启发StrungBuilder 致力于提供适合您需求的解决方案,并推迟将其称为“ Builder”模式的应用程序,直到解决了所有细节。

看到这个全新的问题:程序员SE:“ StringBuilder”是否是Builder设计模式的应用程序?


3
@RichardLevasseur我只是记得它是90年代初至中期在C ++程序员中引起关注和辩论的话题。如果您查看这篇文章:gotw.ca/gotw/066.htm,您会发现它相当复杂,并且相当复杂的东西确实会引起争议。我不确定,但是我认为在90年代初期,这些东西的一部分甚至还没有标准化。但是抱歉,我无法提供良好的参考。
Mike Nakis 2015年

1
@Gurtz我会将此类视为特定于应用程序的“ xml实用程序”,因为xml文件的格式(或文档的结构)可能与您正在开发的特定应用程序相关,无论重用您的“ ModelDef”。
Mike Nakis 2015年

1
@Gurtz因此,我可能会让它们成为“应用程序”主类的实例方法,或者如果麻烦太多,则使它们成为某些助手类的静态方法,其方式与Ross Patterson所建议的非常相似。
Mike Nakis 2015年

1
@Gurtz表示歉意,未在较早前专门解决“构建器”方法。我修改了答案。
Mike Nakis 2015年

3
@Gurtz可能,但出于学术好奇心,这并不重要。不要被“模式反模式”所吸引。模式实际上只是用来方便地向他人描述常见/有用编码技术的名称。做您需要做的事,以后如果需要描述,请在上面贴上标签。只要您的代码有意义,就可以实现“某种程度上类似于构建器模式”的东西。在学习新技术时专注于模式是合理的,只是不要陷入以为您所做的一切都必须是某种命名模式的陷阱。
Jason C

9

您已经给出了在ModelDef构造函数中不执行此工作的最佳理由:

  1. 将XML文档解析为节点树没有“轻量级”的东西。
  2. 没有什么明显的ModelDef说只能从XML文档创建的。

这听起来像你的类应该有各种各样的像静态方法ModelDef.FromXmlString(string xmlDocument)ModelDef.FromXmlDoc(XmlDoc parsedNodeTree)等。


谢谢回复!关于静态方法的建议。这些是创建ModelDef实例的静态工厂(来自各种xml源)吗?还是他们将负责加载已经创建的ModelDef对象?如果是后者,我会担心只对对象进行部分初始化(因为ModelDef需要将节点树完全初始化)。有什么想法吗?
Gurtz 2015年

3
对不起,Ross表示的是静态工厂方法,它们返回完全构造的实例。完整的原型将是这样的public static ModelDef createFromXmlString( string xmlDocument )。这是相当普遍的做法。有时我也这样做。我的建议是,我可以只构造函数,这是我的标准响应类型,在这种情况下,我怀疑在没有充分理由的情况下,其他方法会被视为“非犹太”。
Mike Nakis 2015年

1
@ Mike-Nakis,感谢您的澄清。因此,在这种情况下,静态工厂方法将构建节点树,然后将其传递到ModelDef的构造函数(可能是私有的)中。说得通。谢谢。
Gurtz

@Gurtz就是这样。
罗斯·帕特森

5

我以前听说过“规则”。以我的经验,它是对也是错。

在更“经典”的面向对象中,我们谈论封装状态和行为的对象。因此,对象构造函数应确保将对象初始化为有效状态(如果提供的参数不能使对象有效,则发出错误信号)。确保将对象初始化为有效状态对我来说确实听起来像是真正的工作。如果您有一个仅允许通过构造函数将其初始化为有效状态的对象,并且该对象正确封装了该状态,那么每个更改状态的方法还可以检查是否不会将状态更改为不良状态,因此这种想法是有好处的...然后该对象实质上保证了它“始终有效”。这是一个非常不错的酒店!

因此,当我们尝试将所有内容分解成小块进行测试和模拟时,问题就到了。因为如果确实正确封装了一个对象,那么您将无法真正进入那里并用模拟的FooBarService替换FooBarService,并且您(可能)不能随意更改值以适合您的测试(否则,可能需要比简单的分配要多得多的代码)。

因此,我们得到了另一个“思想流派”,即SOLID。在这种思想流派中,很可能我们不应该在构造函数中进行实际工作。SOLID代码通常(但并非总是)更易于测试。但也可能很难推理。我们将代码分成单一的责任分解成多个小对象,因此大多数对象不再封装其状态(并且通常包含状态或行为)。验证代码通常会提取到验证器类中,并与状态保持独立。但是现在我们失去了凝聚力,我们再也无法确定我们的对象在获得它们时是否有效以及是否完全在尝试对对象执行某些操作之前,请确保必须始终验证我们认为对对象具有的前提是正确的。(当然,通常,您在一层进行验证,然后假设该对象在较低层是有效的。)但是测试起来更容易!

那么谁是对的?

真的没有人。两种思想流派各有千秋。当前SOLID十分流行,每个人都在谈论SRP和Open / Closed以及所有这些爵士乐。但是,仅仅因为某些东西很流行并不意味着它是每个应用程序的正确设计选择。因此,这取决于。如果您在严格遵循SOLID原则的代码库中工作,那么可以,在构造函数中进行实际工作可能不是一个好主意。但否则,请查看情况并尝试使用您的判断。您的对象通过在构造函数中完成工作而获得哪些属性,它失去了哪些属性?它与您的应用程序的整体体系结构的匹配程度如何?

构造函数中的实际工作不是反模式,在正确的地方使用时可能相反。但是应该清楚地记录下来(可以抛出异常,如果有的话可以抛出异常),并且与任何设计决策一样,它应该适合当前代码库中使用的通用样式。


这是一个了不起的答案。
jrahhali

0

这个规则有一个根本的问题,就是“真正的作品”是什么?

您可以从问题中发布的原始文章中看到,作者试图定义什么是“实际作品”,但存在严重缺陷。要使做法良好,就必须制定明确的原则。我的意思是,关于软件工程,这个想法应该是可移植的(与任何语言无关),经过测试和证明的。那篇文章中讨论的大多数内容都不符合该第一个标准。这是作者在那篇文章中提到的“真实作品”的构成要素以及为什么它们不是不好的定义的一些指标。

使用new关键字。该定义从根本上来说是有缺陷的,因为它是特定于领域的。某些语言不使用new关键字。最终,他暗示的是它不应该构造其他对象。但是,在许多语言中,即使最基本的值本身也是对象。因此,构造函数中分配的任何值也将构造一个新对象。这就使这种想法仅限于某些语言,并且对于“真正的作品”的构成没有很好的指示。

构造函数完成后,对象未完全初始化。这是一个很好的规则,但是也与该文章中提到的其他一些规则相矛盾。怎么可能违背他人一个很好的例子被提及的问题是把我带到这里。在这个问题中,有人担心sort由于该原理而在看起来像JavaScript的构造函数中使用该方法。在此示例中,人员正在创建一个对象,该对象表示其他对象的排序列表。出于讨论目的,假设我们有一个未排序的对象列表,并且需要一个新的对象来表示一个已排序的列表。我们需要这个新对象,因为我们软件的某些部分希望对列表进行排序,然后让我们调用该对象SortedList。此新对象接受未排序的列表,并且生成的对象应代表现在已排序的对象列表。如果我们遵循该文档中提到的其他规则,即没有静态方法调用,没有控制流结构,仅是赋值,那么生成的对象将不会在有效状态下被构造,从而破坏了该对象完全初始化的另一条规则。构造函数完成后。为了解决这个问题,我们需要做一些基础的工作,以使未排序列表在构造函数中排序。这样做会破坏其他三个规则,从而使其他规则不相关。

最终,这种在构造函数中不执行“实际工作”的规则是不明确的和有缺陷的。试图定义什么“实际工作”是徒劳的。该文章中的最佳规则是,当构造函数完成时,应将其完全初始化。还有很多其他最佳实践,这些最佳实践会限制在构造函数中完成的工作量。其中大多数可以总结为SOLID原则,而这些相同的原则也不会阻止您在构造函数中进行工作。

PS。我不得不说,虽然我在这里断言在构造函数中完成某些工作没有错,但也不是要进行大量工作的地方。SRP建议构造函数应做足够的工作以使其有效。如果您的构造函数有太多的代码行(我知道很主观),则可能违反了该原理,并且可能会分解为更小的定义更好的方法和对象。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.