В данном посте я бы хотел показать, как построить 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, что видно в контекстном окне 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ли у кого-либо есть замечания или вопросы пишите.
А какие преимущества вашего билдера перед тем, чтобы просто создать месадж, указывая все нужные параметры в конструкторе? Минус я уже вижу - гора интерфейсов, перегруженный API..
ОтветитьУдалитьМне одному кажется, что такая реализация 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;
@Артем Во-первых это читабильность, во -вторых представьте себе класс с 20 свойствами вы необходимо всегда по разному создавать объекты данного класса, прийдется делать кучу перегруженных конструкторов с различным числом параметров.Fluent Builder позволяет создавать объекты с заданием значений различных свойств, которые необходимы на дынный момент.
ОтветитьУдалитьВ-третьих при задании значения свойствам может быть дополнительная логика которую и логично помешать в методы Builder-a. На счет большого количества интерфейсов в данном случае согласен, что интерфесов много, только когда необхомо как я писал ограничить порядок вызовом методов для правильного создания объектов. Естественно пример из статьи очень простой но в реальном проекте я это применил Fluent Builder подошел как нельзя лучше. Причем это еще и привело к сокращению кода. Если интересно могу показать и данный код.
@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
Может я чего не понял, тогда поясните
@Vadim Можно конечно реализовать метод Send который при каждом его вызове бы возвращал новый экземпляр Message, но это надо по другому реализовать EmailBuilder. Для этого не надо создавать объект Message до метода Send и не хранить ссылки на этот объект, а все поля соответсвующие классу Message продублировать в EmailBuilder, и уже при вызове метода Send() создать Message Суть статьи не в этом конечно была. Покажите как бы Вы реализовали.
ОтветитьУдалить"представьте себе класс с 20 свойствами вы необходимо всегда по разному создавать объекты данного класса, прийдется делать кучу перегруженных конструкторов с различным числом параметров"
ОтветитьУдалить1. Есть же необязательные параметры
2. Ничто мне не мешает создать билдер и там прописывать способы создания сообщений
3. если делать как вы показываете, то это будет как минимум 20 дополнительных интерфейсов, что уже совсем не радует
@Артем 1. Даже с необязательными код остается не читабельным. При построении объекта может выпонятся дополнительная логика и перегружать конструктор ей это не хорошо. Еще раз повторю что пример который я привел очень простой где в каждом методе устанавливается только свойство
ОтветитьУдалить2. Смотря какой билдер. Fluent позволяет на много читабельнее и удобнее писать код. Приведите пример как бы Вы реализовали со всеми ограничениями объект Message.
3. Все зависит от ситуации если 20 поле то не значит что интерфейсов 20 может быть
Вообщем у всего есть свои плюсы и минусы но данный подход позволяет сделать код удобнее для понимания и чтения
Я бы еще добавил про использование базового IFluentSyntax с [EditorBrowsable(EditorBrowsableState.Never)] на object-specific методах и про то, что это все выгодно для кода, который часто потребляется и редко изменяется.
ОтветитьУдалитьВообще самым красивым примером fluent интерфейса является стандартная библиотека потокового ввода-вывода в C++. Это ответ скептикам на вопрос "зачем"... Посмотрите там - поймете.
ОтветитьУдалить@Nikita Govorov согласен с Вами данную технику очень удобно использовать при создании библиотек да и не только, не даром есть такие библиотеки как Fluent Nhibernate, Castle Windsor, AutoMapper, NUnint, Entity Framework Code First и т. д реализованные ввиде Fluent API.
ОтветитьУдалитьПо поводу FluentSyntax спасибо полезная вешь избавляет от не нужных методов типа ToString().
@Алексей Коротаев - Спасибо за пример, также добавлю, библиотеки которые я привел в комментарии выше тоже достаточно хороши как примеры Fluent API.
ОтветитьУдалитьМетод Send() вобще сбивает с толку. Судя по названию, он должен отправлять сообщение. Т.е. для меня вобще не очевидно и не логично, что он возвращает объект Message. Правильнее (только на мой взгляд) было бы реализовать его таким образом: ISendingResult Send(ISMTPServer server), возвращая результат отправки сообщения (код ошибки, например).
ОтветитьУдалить@tan4eg Да думаю Вы правы надо было бы переименовать метод из Send в Build.
ОтветитьУдалить@tan4eg Я изменил название метода, чтобы никого не смущало
ОтветитьУдалить