将构建器和流体接口与对象初始化器一起使用是否有意义?


10

在Java和C#中,您可以创建具有可在初始化时设置的属性的对象,方法是定义带参数的构造函数,构造对象后定义每个属性,或使用builder / fluid接口模式。但是,C#3引入了对象和集合初始化程序,这意味着构建器模式在很大程度上没有用。在没有初始化程序的语言中,可以实现一个生成器,然后像这样使用它:

Vehicle v = new Vehicle.Builder()
                    .manufacturer("Toyota")
                    .model("Camry")
                    .year(1997)
                    .colour(CarColours.Red)
                    .addSpecialFeature(new Feature.CDPlayer())
                    .addSpecialFeature(new Feature.SeatWarmer(4))
                    .build();

相反,在C#中,可以这样写:

var vehicle = new Vehicle {
                Manufacturer = "Toyota",
                Model = "Camry",
                Year = 1997,
                Colour = CarColours.Red,
                SpecialFeatures = new List<SpecialFeature> {
                    new Feature.CDPlayer(),
                    new Feature.SeatWarmer { Seats = 4 }
                }
              }

...消除了对上一个示例中所示的构建器的需求。

根据这些示例,构建器是否仍在C#中有用,还是被初始化程序完全取代了?


are builders useful为什么我总是看到这些问题?生成器是一种设计模式-当然,它可能会作为一种语言功能公开,但归根结底,这只是一种设计模式。设计模式不能为“错误”。它可能会或可能不会满足您的用例,但这不会使整个模式错误,仅会导致应用程序错误。请记住,设计模式最终会解决特定的问题-如果您不面对问题,那么为什么还要尝试解决呢?
VLAZ '16

@vlaz我并不是说构建器是错误的-实际上,我的问题是询问是否存在构建器用例,因为初始化器是您将使用构建器的最常见情况的实现。显然,人们回答说,构建器对于设置私有字段仍然有用,这反过来又回答了我的问题。
svbnet

3
@vlaz澄清一下:我的意思是,初始化程序已大大取代了“传统的”构建器/流体接口模式,即编写内部构建器类,然后在该类内部编写链接设置方法,以创建该父类的新实例。我并不是说初始化程序可以替代特定的构建器模式;我说的是,初始化程序使开发人员不必实施特定的构建器模式,除了答案中提到的用例,这也不会使构建器模式无用。
svbnet

5
@vlaz我不明白您的担心。“您是在说设计模式没有用”-不,乔在设计模式是否有用。那是一个坏的,错误的或错误的问题?您认为答案很明显吗?我认为答案并不明显。对我来说,这似乎是一个好问题。
Tanner Swett

2
@vlaz,单例和服务定位器模式错误。Ergo设计模式可能是错误的。
David Arno

Answers:


12

正如@ user248215提到的,真正的问题是不可变性。在C#中使用生成器的原因是为了维护初始化程序的明确性,而不必公开可设置的属性。这不是封装的问题,这就是为什么我编写了自己的答案。封装是相当正交的,因为调用设置器并不意味着设置器实际执行的操作或将您绑定到其实现上。

C#的下一个版本8.0可能会引入一个with关键字,该关键字将使不可变的对象可以清晰,简洁地初始化,而无需编写构建器。

与构造器相比,与构造器相比,您可以执行的另一件有趣的事情是,根据所调用方法的顺序,它们可以导致不同类型的对象。

例如

value.Match()
    .Case((DateTime d) => Console.WriteLine($"{d: yyyy-mm-dd}"))
    .Case((double d) => Console.WriteLine(Math.Round(d, 4));
    // void

var str = value.Match()
    .Case((DateTime d) => $"{d: yyyy-mm-dd}")
    .Case((double d) => Math.Round(d, 4).ToString())
    .ResultOrDefault(string.Empty);
    // string

只是为了澄清上面的示例,它是一个模式匹配库,它使用构建器模式通过指定大小写来构建“匹配”。通过调用Case传递给函数的方法来追加大小写。如果value可分配给函数的参数类型,则将其调用。您可以在GitHub上找到完整的源代码,并且由于很难以纯文本格式阅读XML注释,因此这里是SandCastle构建文档的链接(请参阅“ 备注”部分)


我看不到如何不能使用IEnumerable<Func<T, TResult>>成员来完成对象初始化。
卡雷斯(Caleth)'17

@Caleth这实际上是我尝试过的东西,但是这种方法存在一些问题。它不允许使用条件子句(未显示但在链接的文档中使用和演示了该条件子句),并且不允许进行类型推断TResult。最终,类型推断与它有很大关系。我还希望它“看起来”像控件结构。我也不想使用初始化器,因为那样会允许突变。
阿鲁安·哈达德

12

对象初始化程序要求这些属性必须可由调用代码访问。嵌套的构建器可以访问该类的私有成员。

如果要使Vehicle不可变(通过将所有设置器设为私有),则可以使用嵌套的生成器来设置私有变量。


0

它们都有不同的用途!!!

构造函数可以初始化标记的字段readonly以及私有成员和受保护成员。但是,您在构造函数内部可以做的工作受到一些限制。this例如,您应该避免传递给任何外部方法,并且应避免调用虚拟成员,因为它们可能在尚未构造自身的派生类的上下文中执行。另外,保证构造函数可以运行(除非调用者做的事情很不正常),因此,如果在构造函数中设置字段,则该类其余部分的代码可以假定这些字段不会为空。

初始化程序在构造后运行。它们只允许您将其称为公共财产。无法设置私有和/或只读字段。按照惯例,设置属性的行为应该非常有限,例如,它应该是幂等的,并且副作用也有限。

生成器方法是实际方法,因此允许多个参数,并且按照惯例可能会有副作用,包括对象创建。尽管他们无法设置只读字段,但是他们几乎可以做其他任何事情。而且,方法可以实现为扩展方法(几乎与所有LINQ功能类似)。

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.