构造一个(某种程度上)复杂的对象

时间:2010-08-24 16:56:09

标签: c# .net constructor dns

当我创建类时,简单的构造函数往往是常态。在我目前的一个项目,一个电影库,我有一个Movie域对象。它有许多属性,导致构造函数如下:

public Movie(string title, int year, Genre genre, int length, IEnumerable<string> actors)
{
    _title = title;
    _year = year;
    _genre = genre;
    _length = length;
    _actors = new List<string>(actors);
}

这并不可怕,但也不简单。是否值得使用工厂方法(static Movie CreateMovie(...)),或者可能是对象构建器?是否有用于实例化域类的典型模式?

更新:感谢您的回复。我最初可能是在思考这个问题,尽管我已经学到了一些在更复杂的情况下会有用的东西。我现在的解决方案是将标题作为唯一必需参数,其余作为命名/可选参数。这似乎是构建这个域对象的全面理想方式。

9 个答案:

答案 0 :(得分:4)

是的,使用工厂方法是一种典型模式,但问题是:您为什么需要它?这是关于工厂方法的Wikipedia says

  

与其他创作模式一样,它处理创建对象(产品)的问题,而不指定将要创建的确切对象类。工厂方法设计模式通过定义用于创建对象的单独方法来处理此问题,然后子类可以覆盖以指定将要创建的产品的派生类型。

因此,如果要返回Movie子类,工厂方法模式将有意义。如果这不是(也不会是)一个要求,用工厂方法替换公共构造函数实际上并没有任何用途。

对于您的问题中陈述的要求,您的解决方案对我来说非常好:所有必填字段都作为参数传递给构造函数。如果您的所有字段都不是必填字段,则可能需要添加默认初始值设定项并使用C# object initializer syntax

答案 1 :(得分:4)

如果您使用的是.NET 4.0 ,则可以使用optional/named parameters来简化接受多个参数的对象的创建,其中一些参数是可选的。当您想要避免许多不同的重载来提供有关对象的必要信息时,这很有用。

如果您不在.NET 4上,您可能需要使用Object Builder模式来汇编您的类型。对象构建器需要花费一些精力来实现,并与您保持同步 - 因此,这样做是否足够取决于您的情况。

我发现构建器模式在组合层次结构时最有效,而不是具有一堆属性的类型。在后一种情况下,我通常是重载或可选/命名参数。

答案 2 :(得分:3)

取决于。

如果这是该类的唯一构造函数,则意味着为了实例化该对象,需要所有属性。如果这符合您的业务规则,那就太好了。如果没有,可能会有点麻烦。例如,如果你想用电影为你的系统播种,但并不总是有演员,那么你可能会发现自己陷入困境。

您提到的CreateMovie()方法是另一种选择,以防您需要将内部构造函数与创建Movie实例的行为分开。

您可以使用许多选项来安排构造函数。使用那些允许你设计你的系统没有气味和许多原则(DRY,YAGNI,SRP。)

答案 3 :(得分:1)

你对自己的问题给出了一个很好的答案,这是工厂模式。使用工厂模式,您不需要用于封装的巨大构造函数,您可以在工厂函数中设置对象的成员并返回该对象。

答案 4 :(得分:1)

这是完全可以接受的,恕我直言。我知道静态方法有时不受欢迎,但我通常会将该代码放入一个返回类实例的静态方法中。我通常只对允许具有空值的对象执行此操作。

如果对象的值不能为null,则将它们作为参数添加到构造函数中,这样就不会出现任何无效的对象。

答案 5 :(得分:1)

我没有看到构造函数的接口有什么问题,也看不出静态方法会给你带来什么。我将拥有完全相同的参数,对吧?

参数似乎不是可选的,因此没有办法提供更少或更少的重载 使用可选参数。

从调用者的角度来看,它看起来像这样:

 Movie m = new Movie("Inception", 2010, Genre.Drama, 150, actors);

工厂的目的是为您提供一个可自定义的接口具体实例,而不仅仅是为您调用构造函数。这个想法是确切的类在构造时没有硬编码。这真的更好吗?

 Movie m = Movie.Create("Inception", 2010, Genre.Drama, 150, actors);

对我来说似乎差不多。唯一更好的是,如果Create()返回其他具体类而不是Movie

要考虑的一件事是如何改进这一点,以便调用代码易于理解。对我来说最明显的问题是,如果没有查看Movie的代码,那么150意味着什么并不明显。如果您愿意,有几种方法可以改进:

  1. 使用电影长度类型并构造该类型内联新MovieLength(150)
  2. 使用named parameters if you are using .NET 4.0
  3. (参见@ Heinzi的回答)使用Object Initializers
  4. 使用fluent interface
  5. 使用流畅的界面,您的通话将显示为

     Movie m = new Movie("Inception").
       MadeIn(2010).
       InGenre(Genre.Drama).
       WithRuntimeLength(150).
       WithActors(actors);
    
    坦率地说,所有这些对你的案子来说似乎有些过分。如果您使用的是.NET 4.0,则命名参数是合理的,因为它们不是那么多代码,并且会改善调用者的代码。

答案 6 :(得分:1)

我认为离开公共构造函数的方式没有错。以下是我在决定是否采用工厂方法时遵循的一些规则。

  • 初始化需要复杂的算法时,请使用工厂方法。
  • 初始化需要IO绑定操作时,请使用工厂方法。
  • 初始化时可能会使用工厂方法抛出在开发时无法防范的异常。
  • 当需要额外的废弃物以提高可读性时,请使用工厂方法。

因此,根据我自己的个人规则,我会按原样保留构造函数。

答案 7 :(得分:1)

如果您可以区分核心数据成员和配置参数,请创建一个构造函数,该构造函数不包含所有核心数据成员(甚至不是具有默认值的配置参数 - 为了便于阅读)。初始化配置参数以获得默认值(在方法体中)并提供setter。此时,如果您想要对象的常见配置,工厂方法可能会为您买单。

更好的是,如果你发现你有一个带有大量参数的对象,那么这个对象可能太胖了。您已经闻到了您的代码可能需要重构的事实。考虑分解你的对象。关于面向对象的好文献强烈反对小对象(例如Martin Fowler, Refactoring ; Bob Martin, Clean Code )。福勒解释了如何分解大型物体。例如,配置参数(如果有的话)可能表明需要更多的多态性,特别是如果它们是布尔值或枚举(重构“将条件转换为多态”)。

在提供更具体的建议之前,我需要看看你的对象的使用方式。福勒说,一起使用的变量应该成为他们自己的对象。因此,为了说明,如果您根据类型,年份和长度计算某些事物,而不是其他属性,则可能需要将它们分开到自己的对象中 - 减少必须参数的数量传递给你的构造函数。

答案 8 :(得分:0)

至于我 - 全部取决于您的域名模型。如果您的域模型允许您创建简单对象 - 您应该这样做。

但是我们经常会有很多复合对象,而且每个复合对象的创建过于复杂。这就是为什么我们正在寻找封装复合对象创建逻辑的最佳方法。实际上,我们只有上面描述的两种选择 - “工厂方法”和“对象生成器”。通过静态方法创建对象看起来有点奇怪,因为我们将对象创建逻辑放入对象中。反过来,对象生成器看起来很复杂。

我认为答案在于单元测试。当TDD非常有用时,情况确实如此 - 我们逐步制定域模型,并了解域模型复杂性的必要性。