Разберемся на простом примере, для этого рассмотрим сущность, описываемую классом Post:
partial class Post
{
public string Title { get; set; }
public string Author { get; set; }
public DateTime PublishedAt { get; set; }
public IList<string> Links { get; set; }
}
Чаще всего, простая инициализация экземпляра объекта Post свелась бы к выполнению следующих нехитрых действий:
// Построим объект Post
// Вариант 1
Post oldpost = new Post();
oldpost.Author = "Вася Петров";
oldpost.Title = "Заголовок";
oldpost.PublishedAt = DateTime.Now;
oldpost.Links = new List<string>
{
"http://handcode.ru",
"http://google.com"
};
//
// ИЛИ
//
// Построим объект Post
// Вариант 2
Post simplepost = new Post
{
Author = "Вася Петров",
Title = "Заголовок",
PublishedAt = DateTime.Now,
Links = new List<string>
{
"http://handcode.ru",
"http://google.com"
}
};
Отметьте что второй вариант смотрится гораздо лучше первого, но все равно что-то не то, например: явное DateTime.Now или создание List. Что если переложить обязанность инициализации экземпляра на сторонний класс Builder? И наделить класс Builder fluent-свойством. Попробуем. Для этого реализуем класс PostBuilder:
class PostBuilder
{
// Все свойства закрыты
string Title { get; set; }
string Author { get; set; }
DateTime PublishedAt { get; set; }
IList<string> Links { get; set; }
// Здесь можем инициализировать default-значения
public PostBuilder()
{
Links = new List<string>();
}
public PostBuilder WithTitle(string title)
{
Title = title;
// Возвращаем сами себя
// Принцип fluent interface
return this;
}
// Имя метода задаем как причастие
// Пост написан %author%
public PostBuilder WrittenBy(string author)
{
Author = author;
return this;
}
// Метод, с "говорящим" именем
// Никаких "лишних" аргументов
// Подобных функций может быть много
public PostBuilder WrittenByMe()
{
// "Me" определяется в одном месте, здесь.
return WrittenBy("Я");
}
public PostBuilder PublishedNow()
{
PublishedAt = DateTime.Now;
return this;
}
public PostBuilder AddLink(string link)
{
Links.Add(link);
return this;
}
// Для неявного приведения типа
public static implicit operator Post(PostBuilder builder)
{
// Само построение объекта. Нечто вроде flush метода.
return new Post
{
Title = builder.Title,
Author = builder.Author,
PublishedAt = builder.PublishedAt,
Links = builder.Links
};
}
}
И для удобства использования нашего Builder, не захламляя исходный класс Post, реализуем к нему partial:
// Вот зачем модификатор partial
partial class Post
{
// Возвращает "билдер" для построения Post
public static PostBuilder Builder()
{
return new PostBuilder();
}
}
И наконец, пример использования:
// Построим объект Post через
// объект fluent builder:
Post fluentpost = Post.Builder()
.WrittenBy("Вася Петров")
//.WrittenByMe()
.WithTitle("Заголовок")
.PublishedNow()
.AddLink("http://handcode.ru")
.AddLink("http://google.com");
Ссылки:
Fluent подход конечно более понятен, но он увеличивает кол-во кода который нужно написать, и так же делать для каждой бизнес сущности такой билдер - может быть проблемно, при большом кол-ве сущностей.
ОтветитьУдалитьСпасибо за статью
@Sergey Litvinov
ОтветитьУдалитьСпасибо.
Интересно, я вот думал, как можно избежать лишнего дублирования свойств в самом Builder-е?
Или хотя, можно поразмышлять, как решить данную задачу на AOP.
Все хорошо, только теперь кроме создания самого объекта, нужно еще и объект-Builder создавать. Нужно ли оно?
ОтветитьУдалитьПо мне, так можно создать универсальный объект и инициализировать нужные свойства рефлексией, если уж Fluent-интерфейс делать. Производительности приложению это не добавит, зато не нужно будет каждый раз создавать свой объект Builder.
@Sergun
ОтветитьУдалитьСогласен, красота кода и производительность не всегда идут рядом. Идея интересная, но реализация хромает.
Мне кажется AOP можно добавить, но это затруднит понимание кода. А рефлексией - оно будет терять смысл так как запись:
ОтветитьУдалитьPost.Name = "sss"; или ObjBuilder(postInst).Set("Name", "sss") - делает вид еще хуже. Да и вместо рефлексии нужно использовать Expressions-ы так как они почти не теряют в перфомансе по сравнению с обычными сеттерами\геттерами.
можно, конечно, извратиться и t4 сюда применить :)
ОтветитьУдалить@outcoldman
ОтветитьУдалитьИнтересно, тогда уж можно копнуть в сторону создания Custom MSLinqToSQLGenerator :)
Чаще всего билдер очень помогает при конструировании неизменяемых объектов со сложным конструктором.
ОтветитьУдалитьНовые возможности откроются, если использовать этот билдер в динамическом (dynamic) подходе.
ОтветитьУдалитьТак можно написать этакий универсальный билдер, который сможет сотворять любые объекты.
Также эту шнягу хорошо использовать во всяких интересных сценариях кодогенерации.
>>> Dianbi комментирует... Чаще всего билдер очень помогает при конструировании неизменяемых объектов со сложным конструктором.
ОтветитьУдалитьИлюх, собственно это и есть основная задача шаблона builder - конструирование сложных immutable объектов (грубо говоря у которых количество аргументов конструктора больше 3-4, и (или) тип аргументов конструктора одинаковый). Я незнаю есть ли в C# параметры по-умолчанию, в яве нет - вот и используем билдер чтобы избежать 5 конструкторов с разным набором параметров. Также внутри вызова build() можно проверить что все необходимые поля установлены, проверить ограничения и т.д.
@Alexander Yastrebov
ОтветитьУдалитьВ C# с новой версией 4.0 уже появились параметры по-умолчанию.
По поводу конструктора с кучей параметров, в предыдущей версии ввели "синтаксический сахар", пример:
Person p = new Person
{
Name = "Ilya",
Age = 24
// Через запятую выставляем
// public свойства класса в
// произвольном порядке.
// При этом не обязательно
// задавать все свойства.
// Проверка свойств, может
// находиться в setter-ах.
}
Builder полезен еще тем, что позволяет задать группу, например:
PostBuilder SetAandB(int a, string b){..}
если a и b должны быть выставлены одновременно.