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

后端 未结 4 1634
粉色の甜心
粉色の甜心 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 : 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;
        }
    }
    

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

    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!

提交回复
热议问题