четверг, 15 сентября 2011 г.

Построение Progressive Fluent Interface

            В данном посте я бы хотел показать, как  построить Progressive Fluent Interface в применении к паттерну Fluent Builder.
            Применение Fluent Interface с паттерном Builder позволяет создавать выразительное и читабельное  API для построения сложных объектов.
  Подробнее том что такое Fluent Interface и Fluent Builder можно прочесть в статье Martin Fowler Fluent Interface и  здесь.

            Построение Progressive Interface рассмотрим на примере Fluent Builder для построения Email сообщений вот код данного класса
    public class EmailBuilder : IEmailBuilder
    {
        private readonly Message message;

        private EmailBuilder(Message message)
        {
            this.message = message;
        }

        public static IEmailBuilder From(string from)
        {
            return new EmailBuilder(new Message(from));
        }

        public IEmailBuilder To(string to)
        {
            message.To.Add(to);
            return this;
        }

        public IEmailBuilder Subject(string subject)
        {
            message.Subject = subject;
            return this;
        }

        public IEmailBuilder Body(string body)
        {
            message.Body = body;
            return this;
        }

        public IEmailBuilder Cc(string cc)
        {
            message.Cc.Add(cc);
            return this;
        }

        public Message Build()
        {
            return message;
        }
    }
Данный класс и является  Fluent Builder-ом и реализует интерфейс

    public interface IEmailBuilder
    {
        IEmailBuilder To(string to);
        IEmailBuilder Subject(string subject);
        IEmailBuilder Body(string body);
        IEmailBuilder Cc(string cc);
        Message Build();
    }

Вот код класса Message

    public class Message
    {
        public Message(string from)
        {
            From = from;
            To = new List<string>();
            Cc = new List<string>();
        }

        protected string From { get; set; }

        public string Subject { get; set; }

        public string Body { get; set; }

        public List<string> To { get; set; }

        public List<string> Cc { get; set; }
    }

Теперь для построения объекта Message достаточно написать так

            Message message = EmailBuilder.From("one@gmail.com")
                .To("some@gmail.com")
                .Cc("two@gmail.com")
                .Subject("Hello")
                .Body("Hello everyone!")
                .Build();

что достаточно удобно и очень читабельно, в чем собственно и преимущество использования Fluent Interface

Но на данный момент есть небольшое неудобство  после того как мы написали
EmailBuilder.From("one@gmail.com").

становятся доступными все методы класса EmailBuilder, что видно в контекстном окне Intellisense

                  

 например после метода From можно сразу написать вот так

  Message message = EmailBuilder.From("one@gmail.com").Body("some text").Build();

Что не очень хорошо и не удобно при работе с Intellisense, так же хотелось бы иметь определенную последовательность вызовов методов класса построителя. Данная проблема решается путем построения Progressive Interface следующим образом.

            Для начала надо определить в какой очередности будут вызываться методы, так пусть
после From() можно вызвать только To(), а после To() можно опять To() или Cc() или Subject(), далее чтобы это реализовать необходимо исходный интерфейс IEmailBuilder разделить на несколько интерфейсов

    public interface IEmailBuilderPostFrom
    {
        IEmailBuilderPostTo To(string to);
    }

    public interface IEmailBuilderPostTo : IEmailBuilderPostFrom
    {
        IEmailBuilderPostCc Cc(string cc);
        IEmailBuilderPostSubject Subject(string subject);
    }

IEmailBuilderPostTo наследуется от IEmailBuilderPostFrom, для того чтобы после метода To() можно было опять вызывать метод To().
( интерфейсы IEmailBuilderPostCc и IEmailBuilderPostSubject будут описаны ниже)
Соответствующие методы класса EmailBuilder будут выглядеть следующим образом
        public static IEmailBuilderPostFrom From(string from)
        {
            return new EmailBuilder(new Message(from));
        }

        public IEmailBuilderPostTo To(string to)
        {
            message.To.Add(to);
            return this;
        }

Аналогично для следующих методов после Сс() можно вызывать Сс() и Sudject(), а после Subject() метод  Body(), после Body() метод Build() который и вернет объект Message.

    public interface IEmailBuilderPostCc
    {
        IEmailBuilderPostCc Cc(string cc);
        IEmailBuilderPostSubject Subject(string subject);
    }

    public interface IEmailBuilderPostSubject
    {
        IEmailBuilderPostBody Body(string body);

    }
    public interface IEmailBuilderPostBody
    {
        Message Build();
    }


 В общем идея простая и ясная. В итоге получим следующую реализацию класса EmailBuilder
    public class EmailBuilder : IEmailBuilderPostTo, IEmailBuilderPostCc, IEmailBuilderPostSubject, IEmailBuilderPostBody
    {
        private readonly Message message;

        private EmailBuilder(Message message)
        {
            this.message = message;
        }

        public static IEmailBuilderPostFrom From(string from)
        {
            return new EmailBuilder(new Message(from));
        }

        public IEmailBuilderPostTo To(string to)
        {
            message.To.Add(to);
            return this;
        }

        public IEmailBuilderPostSubject Subject(string subject)
        {
            message.Subject = subject;
            return this;
        }

        public IEmailBuilderPostBody Body(string body)
        {
            message.Body = body;
            return this;
        }

        public IEmailBuilderPostCc Cc(string cc)
        {
            message.Cc.Add(cc);
            return this;
        }

        public Message Build()
        {
            return message;
        }
    }



Теперь при создании объектов Message с помощью  EmailBuilder можно будет  вызывать методы только в определенной последовательности.
Надеюсь данный пост будет полезным. Еcли у кого-либо есть замечания или вопросы пишите.

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

  1. А какие преимущества вашего билдера перед тем, чтобы просто создать месадж, указывая все нужные параметры в конструкторе? Минус я уже вижу - гора интерфейсов, перегруженный API..

    ОтветитьУдалить
  2. Мне одному кажется, что такая реализация Fluent приведет только к ошибкам?

    Тест

    var message = EmailBuilder.From("one@gmail.com")
    .To("some@gmail.com")
    .Cc("two@gmail.com");
    var message1 = message.Subject("Message 1");
    var message2 = message.Subject("Message 2");
    var test = message1.Subject == message2.Subject;

    ОтветитьУдалить
  3. @Артем Во-первых это читабильность, во -вторых представьте себе класс с 20 свойствами вы необходимо всегда по разному создавать объекты данного класса, прийдется делать кучу перегруженных конструкторов с различным числом параметров.Fluent Builder позволяет создавать объекты с заданием значений различных свойств, которые необходимы на дынный момент.
    В-третьих при задании значения свойствам может быть дополнительная логика которую и логично помешать в методы Builder-a. На счет большого количества интерфейсов в данном случае согласен, что интерфесов много, только когда необхомо как я писал ограничить порядок вызовом методов для правильного создания объектов. Естественно пример из статьи очень простой но в реальном проекте я это применил Fluent Builder подошел как нельзя лучше. Причем это еще и привело к сокращению кода. Если интересно могу показать и данный код.

    ОтветитьУдалить
  4. @Vadim ну во-первых так как Вы написали в объектах
    var message1 и message2 будет ссылка не на сам объект Message, а на EmailBulider,
    и написать так не получится message1.Subject
    Если написать так как я понял от Вас
    var message1 = message.Subject("Message 1").Body("").Send();
    var message2 = message.Subject("Message 2").Body("").Send();
    Assert.That(message1.Subject, Is.EqualTo(message2.Subject));
    message1.Subject будет равнятся message2.Subject так как Вы использовали один и тот же объект Builder
    Может я чего не понял, тогда поясните

    ОтветитьУдалить
  5. @Vadim Можно конечно реализовать метод Send который при каждом его вызове бы возвращал новый экземпляр Message, но это надо по другому реализовать EmailBuilder. Для этого не надо создавать объект Message до метода Send и не хранить ссылки на этот объект, а все поля соответсвующие классу Message продублировать в EmailBuilder, и уже при вызове метода Send() создать Message Суть статьи не в этом конечно была. Покажите как бы Вы реализовали.

    ОтветитьУдалить
  6. "представьте себе класс с 20 свойствами вы необходимо всегда по разному создавать объекты данного класса, прийдется делать кучу перегруженных конструкторов с различным числом параметров"
    1. Есть же необязательные параметры
    2. Ничто мне не мешает создать билдер и там прописывать способы создания сообщений
    3. если делать как вы показываете, то это будет как минимум 20 дополнительных интерфейсов, что уже совсем не радует

    ОтветитьУдалить
  7. @Артем 1. Даже с необязательными код остается не читабельным. При построении объекта может выпонятся дополнительная логика и перегружать конструктор ей это не хорошо. Еще раз повторю что пример который я привел очень простой где в каждом методе устанавливается только свойство
    2. Смотря какой билдер. Fluent позволяет на много читабельнее и удобнее писать код. Приведите пример как бы Вы реализовали со всеми ограничениями объект Message.
    3. Все зависит от ситуации если 20 поле то не значит что интерфейсов 20 может быть
    Вообщем у всего есть свои плюсы и минусы но данный подход позволяет сделать код удобнее для понимания и чтения

    ОтветитьУдалить
  8. Я бы еще добавил про использование базового IFluentSyntax с [EditorBrowsable(EditorBrowsableState.Never)] на object-specific методах и про то, что это все выгодно для кода, который часто потребляется и редко изменяется.

    ОтветитьУдалить
  9. Вообще самым красивым примером fluent интерфейса является стандартная библиотека потокового ввода-вывода в C++. Это ответ скептикам на вопрос "зачем"... Посмотрите там - поймете.

    ОтветитьУдалить
  10. @Nikita Govorov согласен с Вами данную технику очень удобно использовать при создании библиотек да и не только, не даром есть такие библиотеки как Fluent Nhibernate, Castle Windsor, AutoMapper, NUnint, Entity Framework Code First и т. д реализованные ввиде Fluent API.
    По поводу FluentSyntax спасибо полезная вешь избавляет от не нужных методов типа ToString().

    ОтветитьУдалить
  11. @Алексей Коротаев - Спасибо за пример, также добавлю, библиотеки которые я привел в комментарии выше тоже достаточно хороши как примеры Fluent API.

    ОтветитьУдалить
  12. Метод Send() вобще сбивает с толку. Судя по названию, он должен отправлять сообщение. Т.е. для меня вобще не очевидно и не логично, что он возвращает объект Message. Правильнее (только на мой взгляд) было бы реализовать его таким образом: ISendingResult Send(ISMTPServer server), возвращая результат отправки сообщения (код ошибки, например).

    ОтветитьУдалить
  13. @tan4eg Да думаю Вы правы надо было бы переименовать метод из Send в Build.

    ОтветитьУдалить
  14. @tan4eg Я изменил название метода, чтобы никого не смущало

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