如何在.net中实现和扩展Joshua的构建器模式?

  • 我们如何在C#中实现Joshua的Effective Java的Builder模式?

下面是我试过的代码,有更好的方法吗?

public class NutritionFacts { public static NutritionFacts.Builder Build(string name, int servingSize, int servingsPerContainer) { return new NutritionFacts.Builder(name, servingSize, servingsPerContainer); } public sealed class Builder { public Builder(String name, int servingSize, int servingsPerContainer) { } public Builder totalFat(int val) { } public Builder saturatedFat(int val) { } public Builder transFat(int val) { } public Builder cholesterol(int val) { } //... 15 more setters public NutritionFacts build() { return new NutritionFacts(this); } } private NutritionFacts(Builder builder) { } protected NutritionFacts() { } } 
  • 我们如何扩展这样的课程? 我们是否需要为每个派生类编写单独的构建器类?

     public class MoreNutritionFacts : NutritionFacts { public new static MoreNutritionFacts.Builder Build(string name, int servingSize, int servingsPerContainer) { return new MoreNutritionFacts.Builder(name, servingSize, servingsPerContainer); } public new sealed class Builder { public Builder(String name, int servingSize, int servingsPerContainer) {} public Builder totalFat(int val) { } public Builder saturatedFat(int val) { } public Builder transFat(int val) { } public Builder cholesterol(int val) { } //... 15 more setters public Builder newProperty(int val) { } public MoreNutritionFacts build() { return new MoreNutritionFacts(this); } } private MoreNutritionFacts(MoreNutritionFacts.Builder builder) { } } 

在Protocol Buffers中,我们实现了这样的构建器模式(大大简化):

 public sealed class SomeMessage { public string Name { get; private set; } public int Age { get; private set; } // Can only be called in this class and nested types private SomeMessage() {} public sealed class Builder { private SomeMessage message = new SomeMessage(); public string Name { get { return message.Name; } set { message.Name = value; } } public int Age { get { return message.Age; } set { message.Age = value; } } public SomeMessage Build() { // Check for optional fields etc here SomeMessage ret = message; message = null; // Builder is invalid after this return ret; } } } 

这与EJ2中的模式不完全相同,但是:

  • 在构建时不需要复制数据。 换句话说,当你设置属性时,你在真实对象上这样做 – 你还没有看到它。 这类似于StringBuilderfunction。
  • 调用Build()以保证不变性后,构建器变为无效。 遗憾的是,它不能像EJ2版本那样用作一种“原型”。
  • 在大多数情况下,我们使用属性而不是getter和setter – 这与C#3的对象初始化器非常吻合。
  • 我们还提供了为了预先C#3用户而返回this setter。

我还没有真正研究过构建模式的inheritance – 无论如何它都不支持Protocol Buffers。 我怀疑这很棘手。

此博客文章可能会引起您的兴趣

C#中模式的一个巧妙变化是使用隐式强制转换运算符来最终调用Build():

 public class CustomerBuilder { ...... public static implicit operator Customer( CustomerBuilder builder ) { return builder.Build(); } } 

编辑:我再次使用它并简化它以删除setter中的冗余值检查。

我最近实现了一个很好的版本。

构建器是缓存最新实例的工厂。 派生的构建器在任何更改时创建实例并清除缓存。

基类很简单:

 public abstract class Builder : IBuilder { public static implicit operator T(Builder builder) { return builder.Instance; } private T _instance; public bool HasInstance { get; private set; } public T Instance { get { if(!HasInstance) { _instance = CreateInstance(); HasInstance = true; } return _instance; } } protected abstract T CreateInstance(); public void ClearInstance() { _instance = default(T); HasInstance = false; } } 

我们正在解决的问题更加微妙。 假设我们有一个Order的概念:

 public class Order { public string ReferenceNumber { get; private set; } public DateTime? ApprovedDateTime { get; private set; } public void Approve() { ApprovedDateTime = DateTime.Now; } } 

ReferenceNumber在创建后不会更改,因此我们通过构造函数将其建模为只读:

 public Order(string referenceNumber) { // ... validate ... ReferenceNumber = referenceNumber; } 

我们如何从数据库数据中重新构建现有的概念性Order

这是ORM断开连接的根源:它倾向于强制使用ReferenceNumberApprovedDateTime上的公共setter以方便技术。 未来的读者隐藏着什么是明确的事实; 我们甚至可以说这是一个不正确的模型。 (对于扩展点也是如此:强制virtual删除了基类传达其意图的能力。)

具有特殊知识的Builder是一种有用的模式。 嵌套类型的替代方法是internal访问。 它支持可变性,域行为(POCO),以及作为奖励的Jon Skeet提到的“原型”模式。

首先,向Order添加internal构造函数:

 internal Order(string referenceNumber, DateTime? approvedDateTime) { ReferenceNumber = referenceNumber; ApprovedDateTime = approvedDateTime; } 

然后,添加一个具有可变属性的Builder

 public class OrderBuilder : Builder { private string _referenceNumber; private DateTime? _approvedDateTime; public override Order Create() { return new Order(_referenceNumber, _approvedDateTime); } public string ReferenceNumber { get { return _referenceNumber; } set { SetField(ref _referenceNumber, value); } } public DateTime? ApprovedDateTime { get { return _approvedDateTime; } set { SetField(ref _approvedDateTime, value); } } } 

有趣的是SetField调用。 由Builder定义,它封装了“设置支持字段,如果不同,然后清除实例”的模式,否则将在属性设置器中:

  protected bool SetField( ref TField field, TField newValue, IEqualityComparer equalityComparer = null) { equalityComparer = equalityComparer ?? EqualityComparer.Default; var different = !equalityComparer.Equals(field, newValue); if(different) { field = newValue; ClearInstance(); } return different; } 

我们使用ref来允许我们修改支持字段。 我们还使用默认的相等比较器,但允许调用者覆盖它。

最后,当我们需要重新构建一个Order ,我们将OrderBuilder与隐式OrderBuilder一起使用:

 Order order = new OrderBuilder { ReferenceNumber = "ABC123", ApprovedDateTime = new DateTime(2008, 11, 25) }; 

这真的很长。 希望能帮助到你!

使用Joshua Bloch的构建器模式的原因是用部件创建一个复杂的对象,并使其不可变。

在这种特殊情况下,在C#4.0中使用可选的命名参数更清晰。 您放弃了一些设计灵活性(不要重命名参数),但是您可以更轻松地获得更好的可维护代码。

如果NutritionFacts代码是:

  public class NutritionFacts { public int servingSize { get; private set; } public int servings { get; private set; } public int calories { get; private set; } public int fat { get; private set; } public int carbohydrate { get; private set; } public int sodium { get; private set; } public NutritionFacts(int servingSize, int servings, int calories = 0, int fat = 0, int carbohydrate = 0, int sodium = 0) { this.servingSize = servingSize; this.servings = servings; this.calories = calories; this.fat = fat; this.carbohydrate = carbohydrate; this.sodium = sodium; } } 

然后客户端将其用作

  NutritionFacts nf2 = new NutritionFacts(240, 2, calories: 100, fat: 40); 

如果结构更复杂,则需要进行调整; 如果卡路里的“构建”不仅仅是整数,那么可以想象其他辅助对象是必需的。