【问题标题】:How to implement and extend Joshua's builder pattern in .net?如何在 .net 中实现和扩展 Joshua 的构建器模式?
【发布时间】:2010-09-23 17:53:46
【问题描述】:

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

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

【问题讨论】:

    标签: c# .net


    【解决方案1】:

    在 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。

    我还没有真正研究过构建器模式的继承——协议缓冲区无论如何都不支持它。我怀疑这很棘手。

    【讨论】:

    • >> “它非常适合 C# 3 的对象初始值设定项。”在这种情况下,将创建为 new SomeMessage.Build(Name="GK", Age=0)。但它和写 new SomeMessage(Name="GK", Age=0) 一样好,对吧?在这种情况下,我们需要构建方法吗?它将如何帮助我们避免伸缩构造函数?
    • 不,它会写成 new SomeMessage.Builder { Name="GK", Age = 0 }。注意括号与大括号的区别。不同之处在于您只需要指定您想要的属性。请注意,在 C# 4 中,带有许多可选参数的单个构造函数将提供另一个选项。
    • 好的。现在我知道了。 new SomeMessage{Name="GK", Age=0} 是不可能的属性是私有的,只有 builder 可以访问它。
    • setter 是私有的,是的,为了不变性。当然,吸气剂是公开的。
    • 你不想给 SomeMessage 一个私有构造函数吗?
    【解决方案2】:

    This blog entry 可能会感兴趣

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

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

    【讨论】:

    • 我已经在一些生产系统中使用了博客文章中的代码(但略有修改),并认为它工作得很好
    【解决方案3】:

    编辑:我再次使用它并简化它以删除设置器中多余的值检查。

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

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

    基类很简单:

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

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

    【讨论】:

    • 感谢布莱恩的回复。这确实是该模式的一个非常干净的实现。 Jon、alasdairg 和 Jaime 也有一些类似/不同的实现。我个人更喜欢在类型中使用静态 Builder Method,它返回 builder 类。
    • 我仍在试图弄清楚如何扩展这样的构建器类。如果我们有一个 SpecialOrder(不好的名字!!),那么我们必须使用 SpecialOrderBuilder,但在这种情况下,用户(有人正在使用您的代码)应该知道每个构建器的实现。
    • 在上面的示例中,我重复了 MoreNutritionFacts.Builder 中的代码,这也不好。有没有更干净/正确的实现?正如乔恩所说,这似乎很棘手。
    • NutritionFactsBuilder 包含 NutritionFacts 的所有属性。如果您从中派生 MoreNutritionFactsBuilder,您将继承所有这些定义并且只需要覆盖 Create()。没有重复代码。
    • 另外,仅仅因为类是相关的并不意味着它们应该嵌套。关联模式允许更大的灵活性。想想 [Builder(typeof(NutritionFactsBuilder))] 关于 NutritionFacts 的定义。嵌套类型有什么好处?
    【解决方案4】:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-07-26
      • 2018-12-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多