Как реализовать и расширить шаблон 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);
Если конструкция более сложная, ее нужно будет подстроить; если "построение" калорий больше, чем помещение целого числа, возможно, потребуются другие вспомогательные объекты.