How to implement and extend Joshua's builder pattern in .net?

后端 未结 4 1633
粉色の甜心
粉色の甜心 2021-01-30 05:35
  • How can we implement the Builder pattern of Joshua\'s Effective Java in C#?

Below is the code I have tried, is there a better way to do this?



        
相关标签:
4条回答
  • 2021-01-30 06:15

    Edit: I used this again and simplified it to remove the redundant value-checking in setters.

    I recently implemented a version that is working out nicely.

    Builders are factories which cache the most recent instance. Derived builders create instances and clear the cache when anything changes.

    The base class is straightforward:

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

    The problem we are solving is more subtle. Let's say we have the concept of an Order:

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

    ReferenceNumber does not change after creation, so we model it read-only via the constructor:

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

    How do we reconstitute an existing conceptual Order from, say, database data?

    This is the root of the ORM disconnect: it tends to force public setters on ReferenceNumber and ApprovedDateTime for technical convenience. What was a clear truth is hidden to future readers; we could even say it is an incorrect model. (The same is true for extension points: forcing virtual removes the ability for base classes to communicate their intent.)

    A Builder with special knowledge is a useful pattern. An alternative to nested types would be internal access. It enables mutability, domain behavior (POCO), and, as a bonus, the "prototype" pattern mentioned by Jon Skeet.

    First, add an internal constructor to Order:

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

    Then, add a Builder with mutable properties:

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

    The interesting bit is the SetField calls. Defined by Builder, it encapsulates the pattern of "set the backing field if different, then clear the instance" that would otherwise be in the property setters:

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

    We use ref to allow us to modify the backing field. We also use the default equality comparer but allow callers to override it.

    Finally, when we need to reconstitute an Order, we use OrderBuilder with the implicit cast:

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

    This got really long. Hope it helps!

    0 讨论(0)
  • 2021-01-30 06:23

    The reason to use Joshua Bloch's builder pattern was to create a complex object out of parts, and also to make it immutable.

    In this particular case, using optional, named parameters in C# 4.0 is cleaner. You give up some flexibility in design (don't rename the parameters), but you get better maintainable code, easier.

    If the NutritionFacts code is:

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

    Then a client would use it as

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

    If the construction is more complex this would need to be tweaked; if the "building" of calories is more than putting in an integer, it's conceivable that other helper objects would be needed.

    0 讨论(0)
  • 2021-01-30 06:30

    In Protocol Buffers, we implement the builder pattern like this (vastly simplified):

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

    This isn't quite the same as the pattern in EJ2, but:

    • No data copying is required at build time. In other words, while you're setting the properties, you're doing so on the real object - you just can't see it yet. This is similar to what StringBuilder does.
    • The builder becomes invalid after calling Build() to guarantee immutability. This unfortunately means it can't be used as a sort of "prototype" in the way that the EJ2 version can.
    • We use properties instead of getters and setters, for the most part - which fits in well with C# 3's object initializers.
    • We do also provide setters returning this for the sake of pre-C#3 users.

    I haven't really looked into inheritance with the builder pattern - it's not supported in Protocol Buffers anyway. I suspect it's quite tricky.

    0 讨论(0)
  • 2021-01-30 06:30

    This blog entry might be of interest

    A neat variation on the pattern in C# is the use of an implicit cast operator to make the final call to Build() unnecessary:

    public class CustomerBuilder
    {
    
       ......     
    
       public static implicit operator Customer( CustomerBuilder builder ) 
       {  
          return builder.Build();
       } 
    }
    
    0 讨论(0)
提交回复
热议问题