缺陷:构造函数执行实际工作

时间:2014-05-13 05:36:36

标签: oop unit-testing design-patterns constructor

我有一个代表一组数字的类。构造函数有三个参数:startValueendValuestepSize。 该类负责保存包含startSize之间的所有值的列表,并考虑stepSize。

示例:startValue:3,endValue:1,stepSize = -1,Collection = {3,2,1}

我目前正在构造函数中创建有关该对象的集合和一些信息字符串。公共成员是只读信息字符串和集合。

我的构造函数目前做了三件事:

  • 检查参数;这可能会从构造函数

  • 中抛出异常
  • 将值填入集合

  • 生成信息字符串

我可以看到我的构造函数真正起作用,但我该如何解决这个问题,或者,我应该解决这个问题吗?如果我将“方法”移出构造函数,那就像拥有init函数并留下一个未完全初始化的对象。我的对象的存在是否值得怀疑?或者在构造函数中完成一些工作并不是那么糟糕,因为仍然可以测试构造函数,因为没有创建对象引用。

对我来说它看起来不对,但似乎我找不到解决方案。我也考虑了一个建设者,但我不确定这是否正确,因为你不能在不同类型的创作之间做出选择。但是,单个单元测试的责任较小。

我在C#中编写代码,但我更喜欢通用解决方案,这就是文本不包含代码的原因。

编辑:感谢您编辑我的不良文本(:我更改了标题,因为它代表了我的意见,编辑的标题没有。我不是在问实际工作是否是一个缺陷。对我而言,它是。看看这个参考文献。

http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/

博客很好地陈述了问题。我仍然无法找到解决方案。

6 个答案:

答案 0 :(得分:4)

促使你的构造者保持轻量化的概念:

  • 控制反转(依赖注入)
  • 单一责任原则(适用于构造函数而非类)
  • 延迟初始化
  • 测试
  • K.I.S.S。
  • D.R.Y。

链接到原因的参数:

如果检查构造函数中的参数,如果这些参数来自任何其他来源(setter,constructor,parameter object),则无法共享验证代码

如果将值填充到集合中或在构造函数中生成信息字符串,则代码无法与其他构造函数共享,您可能需要稍后添加。

除了无法共享之外,还有延迟,直到真正需要(懒惰的初始化)。还有覆盖继承,它提供了更多的选项,许多方法只做一件事,而不是做一切构造函数。

您的构造函数只需要将您的类置于可用状态。它不必完全初始化。但是完全可以自由地使用其他方法来完成实际工作。这并没有利用" lazy init"理念。有时你需要它,有时候你不需要它。

请记住构造函数执行或调用的任何内容都会被用户/测试人员扼杀。

修改

你仍然没有接受答案,我已经睡了一觉,所以我会尝试一下设计。一个好的设计是灵活的,所以我可以假设我不确定信息字符串是什么,或者我们的对象是否需要通过作为一个集合来表示一组数字(因此提供迭代器,size(),add(),remove()等)或仅由集合支持,并提供对这些数字的一些狭窄的专用访问(例如不可变)。

这个小家伙是参数对象模式

/** Throws exception if sign of endValue - startValue != stepSize */
ListDefinition(T startValue, T endValue, T stepSize);

T可以是int或long或short或char。玩得开心,但要保持一致。

/** An interface, independent from any one collection implementation */
ListFactory(ListDefinition ld){
    /** Make as many as you like */
    List<T> build();
}

如果我们不需要缩小对收藏品的访问权限,我们就完成了。如果我们这样做,请在暴露之前将其包裹在立面中。

/** Provides read access only.  Immutable if List l kept private. */
ImmutableFacade(List l);

哦等等,要求改变,忘记了信息字符串&#39;。 :)

/** Build list of info strings */
InformationStrings(String infoFilePath) {
     List<String> read();
}

不知道这是不是你的想法,但是如果你想要用两倍数来计算行数,你就可以了。 :)

/** Assuming information strings have a 1 to 1 relationship with our numbers */
MapFactory(List l, List infoStrings){
    /** Make as many as you like */
    Map<T, String> build();
}

所以,是的,我使用构建器模式将所有这些连接在一起。或者您可以尝试使用一个对象来完成所有这些操作。由你决定。但我认为你会发现这些构造函数中很少有人能做很多事情。

<强> EDIT2
我知道这个答案已被接受,但我已经意识到还有改进的余地,我无法抗拒。上面的ListDefinition通过使用getter,ick暴露它的内容来工作。有一个&#34;告诉,不要问&#34;没有充分理由在这里违反的设计原则。

ListDefinition(T startValue, T endValue, T stepSize) {
    List<T> buildList(List<T> l);
}

这让我们构建任何类型的列表实现,并根据定义进行初始化。现在我们不需要ListFactory。 buildList是我称之为分流的东西。它在使用它之后返回它接受的相同引用。它只是允许您跳过给新的ArrayList命名。现在制作一个列表如下:

ListDefinition<int> ld = new ListDefinition<int>(3, 1, -1);
List<int> l = new ImmutableFacade<int>(  ld.buildList( new ArrayList<int>() )  );

哪个工作正常。有点难以阅读。那么为什么不添加静态工厂方法:

List<int> l = ImmutableRangeOfNumbers.over(3, 1, -1);

这不接受依赖注入,但它建立在类的基础之上。它实际上是依赖注入容器。这使它成为基础类的流行组合和配置的一个很好的简写。你不必为每个组合制作一个。现在,您可以将所需的任何组合放在一起。

嗯,这是我的2美分。我会找到别的东西来痴迷。欢迎反馈。

答案 1 :(得分:3)

就凝聚力而言,没有“真正的工作”,只有符合班级/方法责任的工作。

构造函数的职责是创建一个类的实例。还有一个有效实例。我很擅长将验证部分保持为内在,因此每次查看类时都可以看到不变量。换句话说,该类“包含自己的定义”。

但是,有些情况下,对象是多个其他对象的复杂集合,包括条件逻辑,非平凡验证或其他创建子任务。这是当我将对象创建委托给另一个类(工厂或构建器模式)并限制构造函数的可访问范围时,但在做之前我会三思而行。

在你的情况下,我看到没有条件(除了参数检查),没有复杂对象的组合或检查。构造函数完成的工作与类紧密相关,因为它基本上只填充其内部。虽然您可能(并且应该)将原子的,明确的构造步骤提取到同一个类中的私有方法中,但我认为不需要单独的构建器类。

答案 2 :(得分:1)

构造函数是一个特殊的成员函数,与构造函数一样,但毕竟它是一个成员函数。因此,它被允许做事。

考虑例如c++ std::fstream。它在构造函数中打开一个文件。可以抛出异常,但不必。

只要你能测试这门课,那就很好。

答案 3 :(得分:1)

确实如此,构造者应该做最少的工作,以达到一个目标 - 成功创造有效对象。无论需要什么都可以。但不是更多。

在您的示例中,在构造函数中创建此集合是完全有效的,因为类的对象表示一组数字(您的单词)。如果一个对象是一组数字,你应该在构造函数中清楚地创建它!相反 - 构造者不会执行它所做的 - 一个新的,有效的对象构造。

这些信息字符串引起了我的注意。他们的目的是什么?你究竟做了什么?这听起来像是一种periferic,可以留待以后通过一种方法暴露出来,比如

String getInfo()

或类似。

答案 4 :(得分:0)

如果你想使用微软的.NET Framework就是一个例子,它在语义和通用实践方面都是完全有效的,因为构造函数可以做一些真正的工作。

Microsoft执行此操作的一个示例是System.IO.FileStream的实现。此类对路径名执行字符串处理,打开新文件句柄,打开线程,绑定各种事物,并调用许多系统函数。实际上,构造函数大约有1,200行代码。

我相信您创建列表的示例绝对精确且有效。我会确保你尽可能经常失败。假如你的最小尺寸高于最大尺寸,那么你可能会陷入一个写入不良的循环条件的无限循环,从而耗尽所有可用的内存。

外卖是“它取决于”,你应该用你最好的判断。如果您想要的只是第二意见,那么我说你很好。

答案 5 :(得分:0)

在构造函数中执行“实际工作”并不是一个好习惯:您可以初始化类成员,但不应该在构造函数中调用其他方法或执行更多“繁重工作”。

如果你需要进行一些需要运行大量代码的初始化,一个好的做法是在init()方法中进行,这将在构造对象后调用。

在构造函数中执行繁重操作的原因是:如果发生了一些不好的事情,并且无声地失败,你最终会遇到一个混乱的对象,这将是一场噩梦。调试并意识到问题的来源。

在上面描述的情况下,我只会在构造函数中进行赋值,然后在两个单独的方法中,我将实现验证并生成字符串信息。

以这种方式实现它也符合SRP:“单一责任原则”,它表明任何方法/功能都应该做一件事,而且只做一件事。