Как реализовать и расширить шаблон Joshua builder в .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) { }
    }
    

Ответ 1

В буферах протоколов мы реализуем такой шаблон построения (значительно упрощен):

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(), чтобы гарантировать неизменность. Это, к сожалению, означает, что он не может использоваться как своего рода "prototype" в том, как версия EJ2 может.
  • Мы используем свойства, а не геттеры и сеттеры, по большей части, которые хорошо вписываются в инициализаторы объектов С# 3.
  • Мы также предоставляем сеттерам, возвращающим this для пользователей до С# 3.

Я действительно не смотрел наследование с шаблоном построителя - он все равно не поддерживается в протокольных буферах. Я подозреваю, что это довольно сложно.

Ответ 2

Эта запись в блоге может представлять интерес

Явная вариация на шаблоне в С# заключается в использовании неявного оператора литья, чтобы сделать окончательный вызов 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 отключается: он стремится заставить публичные сеттеры на ReferenceNumber и ApprovedDateTime для удобства. То, что было ясной истиной, скрыто для будущих читателей; мы могли бы даже сказать, что это неправильная модель. (То же самое верно для точек расширения: форсирование virtual устраняет способность базовых классов сообщать о своих намерениях.)

A Builder со специальными знаниями - полезный образец. Альтернативой вложенным типам будет internal доступ. Он позволяет изменять, поведение домена (POCO) и, в качестве бонуса, шаблон "prototype", упомянутый 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)
};

Это получилось очень долго. Надеюсь, это поможет!

Ответ 4

Причиной использования шаблона построителя Джошуа Блоха было создание сложного объекта из частей, а также сделать его неизменным.

В этом конкретном случае использование опциональных именованных параметров в С# 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);

Если конструкция более сложная, ее нужно будет подстроить; если "построение" калорий больше, чем помещение целого числа, возможно, потребуются другие вспомогательные объекты.