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

时间:2008-11-24 09:12:31

标签: c# .net

以下是我尝试的代码,有更好的方法吗?

    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) { }
    }
    

4 个答案:

答案 0 :(得分:22)

在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中的模式不完全相同,但是:

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

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

答案 1 :(得分:4)

可能会感兴趣

This blog entry

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

public class CustomerBuilder
{

   ......     

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

答案 2 :(得分:2)

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

我最近实现了一个运行良好的版本。

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

基类很简单:

public abstract class Builder<T> : IBuilder<T>
{
    public static implicit operator T(Builder<T> 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上的公共设置者。未来的读者隐藏着什么是明确的事实;我们甚至可以说这是一个不正确的模型。 (对于扩展点也是如此:强制virtual删除了基类传达其意图的能力。)

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

首先,将internal构造函数添加到Order

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

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

public class OrderBuilder : Builder<Order>
{
    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<TField>(
        ref TField field,
        TField newValue,
        IEqualityComparer<T> equalityComparer = null)
    {
        equalityComparer = equalityComparer ?? EqualityComparer<TField>.Default;

        var different = !equalityComparer.Equals(field, newValue);

        if(different)
        {
            field = newValue;

            ClearInstance();
        }

        return different;
    }

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

最后,当我们需要重新构建Order时,我们会使用OrderBuilder隐式转换:

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

这真的很长。希望它有所帮助!

答案 3 :(得分:0)

使用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);

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