Feel Good.

26 апреля 2010

Используем Fluent Builder

Продолжая тему Fluent interface, решил написать про Fluent builder. Основная цель Fluent builder - упростить процесс конструирования объектов, используя для этого цепочки методов (set-методы).

Разберемся на простом примере, для этого рассмотрим сущность, описываемую классом 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");



Ссылки:
  1. Fluent interface
  2. Используем Fluent Filters

Progg it

11 комментариев:

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

    ОтветитьУдалить
  2. @Sergey Litvinov

    Спасибо.
    Интересно, я вот думал, как можно избежать лишнего дублирования свойств в самом Builder-е?

    Или хотя, можно поразмышлять, как решить данную задачу на AOP.

    ОтветитьУдалить
  3. Все хорошо, только теперь кроме создания самого объекта, нужно еще и объект-Builder создавать. Нужно ли оно?

    По мне, так можно создать универсальный объект и инициализировать нужные свойства рефлексией, если уж Fluent-интерфейс делать. Производительности приложению это не добавит, зато не нужно будет каждый раз создавать свой объект Builder.

    ОтветитьУдалить
  4. @Sergun

    Согласен, красота кода и производительность не всегда идут рядом. Идея интересная, но реализация хромает.

    ОтветитьУдалить
  5. Мне кажется AOP можно добавить, но это затруднит понимание кода. А рефлексией - оно будет терять смысл так как запись:
    Post.Name = "sss"; или ObjBuilder(postInst).Set("Name", "sss") - делает вид еще хуже. Да и вместо рефлексии нужно использовать Expressions-ы так как они почти не теряют в перфомансе по сравнению с обычными сеттерами\геттерами.

    ОтветитьУдалить
  6. можно, конечно, извратиться и t4 сюда применить :)

    ОтветитьУдалить
  7. @outcoldman

    Интересно, тогда уж можно копнуть в сторону создания Custom MSLinqToSQLGenerator :)

    ОтветитьУдалить
  8. Чаще всего билдер очень помогает при конструировании неизменяемых объектов со сложным конструктором.

    ОтветитьУдалить
  9. Новые возможности откроются, если использовать этот билдер в динамическом (dynamic) подходе.
    Так можно написать этакий универсальный билдер, который сможет сотворять любые объекты.

    Также эту шнягу хорошо использовать во всяких интересных сценариях кодогенерации.

    ОтветитьУдалить
  10. >>> Dianbi комментирует... Чаще всего билдер очень помогает при конструировании неизменяемых объектов со сложным конструктором.
    Илюх, собственно это и есть основная задача шаблона builder - конструирование сложных immutable объектов (грубо говоря у которых количество аргументов конструктора больше 3-4, и (или) тип аргументов конструктора одинаковый). Я незнаю есть ли в C# параметры по-умолчанию, в яве нет - вот и используем билдер чтобы избежать 5 конструкторов с разным набором параметров. Также внутри вызова build() можно проверить что все необходимые поля установлены, проверить ограничения и т.д.

    ОтветитьУдалить
  11. @Alexander Yastrebov
    В C# с новой версией 4.0 уже появились параметры по-умолчанию.
    По поводу конструктора с кучей параметров, в предыдущей версии ввели "синтаксический сахар", пример:
    Person p = new Person
    {
    Name = "Ilya",
    Age = 24
    // Через запятую выставляем
    // public свойства класса в
    // произвольном порядке.
    // При этом не обязательно
    // задавать все свойства.
    // Проверка свойств, может
    // находиться в setter-ах.
    }
    Builder полезен еще тем, что позволяет задать группу, например:
    PostBuilder SetAandB(int a, string b){..}
    если a и b должны быть выставлены одновременно.

    ОтветитьУдалить