Posts Tagged ‘Patterns’
Реализация fluent builder в C#
Сейчас многие библиотеки реализуют так называемые fluent builder’ы, то есть дают возможность пользователю создавать объекты через более предсказуемый и документированный интерфейс чем просто массивно перегруженные конструкторы. Такой способ позволяет лучше контролировать процесс создания объектов, предупреджать ошибки, проще документировать шаги инициализации, производить валидацию и в целом лучше “выказывать намерения” (reveal intentions) в плане используемости API.
Интерфейсы таких builder’ов выглядят очень красиво, но процесс создания инфраструктуры под эти сборщики весьма непрост, и очень хитро эксплуатирует различные возможности языка. О том, как создавать эффективные сборщики объектов я и хочу рассказать в этой статье.
Простой пример
Паттерн Builder в основном полезен для сложных объектов, но здесь и далее примеры будут упрощены, и начнем мы с самого тривиального. Допустим что у вас есть объект типа Person:
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
Чтобы добавить сборщик (PersonBuilder) для подобного объекта, сначала надо определить, куда его помещать. Тут есть несколько вариантов – мне на ум приходят как минимум четыре:
- Не делать отдельного сборщика, а заставить сам
Personсобирать себя, благо это можно реализовать. К сожалению, такой подход загрязняет API. - Сделать отдельный сборщик как вложенный
privateкласс. - Сделать сборщик обычным отдельным
internalклассом (а можно дажеpublic– особых рисков это не несет), а все подвластные ему структуры действительно сделать вложенными. - Сделать сборщик классом-контейнером методов расширений (т.е.
static), и сделать метод(ы) расширения для начала работы с конфигурацией.
Давайте возьмем 3й вариант. Представим на секунду, что объект Person имеет статический метод Create(), который возвращает некий PersonBuilder:
public class Person
{
public string Name {get;set;}
public int Age {get;set;}
public static PersonBuilder Create()
{
return new PersonBuilder(new Person());
}
}
Итак, мы видим первый трюк fluent builder’ов: создаваемый объект пробрасывается через все вызовы. Это полезно. Только вот наблюдательный читатель задаст вопрос: какого-такого Create() возвращает PersonBuilder а не Person? Спокойствие, мой падаван, ответ уже близок.
Класс PersonBuilder – это обычный класс который держит ссылку на Person. Помимо этого, он дает нам возможность управлять создаваемым объектом через методы (или свойства – об этом скоро) с помощью fluent-вызовов, т.е. методов которые кончаются на return this. Но самой замечательной особенностью является то, что наш builder имеет оператор неявного приведения к Person. Вот как все это выглядит:
public class PersonBuilder
{
private Person person;
public PersonBuilder(Person person)
{
this.person = person;
}
public PersonBuilder Called(string name)
{
person.Name = name;
return this;
}
public PersonBuilder Age(int age)
{
person.Age = age;
return this;
}
public static implicit operator Person(PersonBuilder pb)
{
return pb.person;
}
}
Использование этого сборщика тривиально:
Person me = Person.Create().Called("Dmitri").Age(25);
Итак, мы познакомились еще с одним правилом: любой конечный сборщик должен быть приводим к типу, который собирает. Под “конечным сборщиком” я подразумеваю не только PersonBuilder, но также любой специализированный сборщик, который PersonBuilder мог подставить. В следующей секции мы как раз об этом и поговорим.
Промежуточные объекты
Если использовать наш сборщик для сложных объектов, мы перегрузим API настолько, что у пользователя голова закружится. Это особенно критично если сам по себе объект сложный, т.к. намного более полезным было бы предложить пользователю конфигурировать различные аспекты по отдельности.
Давайте сделаем еще один пример: у нас есть все тот же Person, и у него есть два набора свойств (или два аггрегированных объекта – без разницы) – адрес и информация о месте работы. Перепишем Person:
public class Person
{
public string Name {get;set;}
public int Age {get;set;}
public string StreetAddress {get;set;}
public string PostCode {get;set;}
public string Country {get;set;}
public string CompanyName {get;set;}
public string Position {get;set;}
public int AnnualIncome {get;set;}
public static PersonBuilder Create()
{
return new PersonBuilder(new Person());
}
}
Для конфигурации адреса и работы отдельно создаются промежуточные сборщики (т.н. “подсборщики”), в нашем случае PersonAddressBuilder и PersonJobBuilder. Эти классы – обязательно вложенные. Возьмем первый класс – вот его базовое определение:
public class PersonAddressBuilder
{
private Person person;
public PersonAddressBuilder(Person person)
{
this.person = person;
}
public PersonAddressBuilder At(string streetAddress)
{
person.StreetAddress = streetAddress;
return this;
}
public PersonAddressBuilder WithPostCode(string postCode)
{
person.PostCode = postCode;
return this;
}
public PersonAddressBuilder In(string country)
{
person.Country = country;
return this;
}
}
Думаю намек понят – согласно правилу про которое я уже писал, Person пробрасывается и сюда. Пока что мы видим обычный fluent interface, но это только пока. Тем временем, PersonBuilder выбрасывает этот сборщик через свойство что, собственно, не запрещено:
public PersonAddressBuilder Lives
{
get
{
return new PersonAddressBuilder(person);
}
}
Вернемся к PersonAddressBuilder – как я уже писал, вы обязаны выбрасывать отсюда Person по желанию. Но помимо этого, вы должны дуплицировать все fluent-подвызовы корнегого builder’а в каждом подсборщике. От такого словоизвержения может начать глючить, поэтому вот сразу код:
public PersonJobBuilder Works
{
get
{
return new PersonJobBuilder(person);
}
}
public static implicit operator Person(PersonAddressBuilder builder)
{
return builder.person;
}
Ну вот собственно и всё. А второй сборщик реализован симметрично:
public class PersonJobBuilder
{
private Person person;
public PersonJobBuilder(Person person)
{
this.person = person;
}
public PersonJobBuilder At(string companyName)
{
person.CompanyName = companyName;
return this;
}
public PersonJobBuilder AsA(string position)
{
person.Position = position;
return this;
}
public PersonJobBuilder Earning(int dollarsPerMonth)
{
person.AnnualIncome = dollarsPerMonth;
return this;
}
public PersonAddressBuilder Lives
{
get
{
return new PersonAddressBuilder(person);
}
}
public static implicit operator Person(PersonJobBuilder builder)
{
return builder.person;
}
}
Мораль тут в том, что подсборщики внутренне позволяют пользователю переключаться между ними. Побочным эффектом является то, что они знают друг о друге, и неинформированность о новых подсборщиках может “сломать шаблон”. По-хорошему, для подобных целей нужно метапрограммирование.
Тем временем, у нас всё. Хотите посмотреть как это счастье использовать? Да вот так:
Person me = Person.Create()
.Lives.At("123 London Road").WithPostCode("SO17 1BJ").In("Southampton")
.Works.At("CRSI").AsA("VisitingResearcher").Earning(12345);
Сложные сценарии
Сушествует несколько более сложных сценариев для использования fluent-конфигураторов (сборщиков, builder’ов, называйте как хотите).
Во-первых, вполне реальна ситуация, когда количество вложенных сборщиков больше двух. Это породжает целый пласт проблем, т.к. сборщики каждого уровня должны быть “переключаемы”, но также нужно иметь возможность переключаться “наверх”, а не только вниз.
Во-вторых, существуют ситуации когда хочется создавать свои собственный объекты исключительно для конфигурации. Например, что если я хочу указать возраст не только как 25 но как 25.years + 7.months? Это возможно, но для этого нужно много методов расширения, и в результате у нас получится загрязнение API, много промежуточных объектов, много переопределенных операторов. Слава богу что большинство их этого можно вынести в отдельные классы, которые не мешаются с основным “контентом”.
Ну и наконец, осталась нераскрытой тема использования анонимных классов и Expression<T> – но это относится скорее к типам параметров к типам параметров чем к инфраструктуре. А про инфраструктуру пока все. Comments welcome. ■

