Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

Fluent builder на Boo — генерация структур

9 комментариев

Продолжим тему fluent builder’ов, начатую в предыдущем посте. В этом посте я хочу посмотреть на то, как можно автогенерировать fluent builder’ы на основе некоторых спецификаций. Ведь не писать же ручками все эти промежуточные сборщики, операторы приведения, и т.п., правильно? Вот, я тоже так думаю.

Что у нас имеется

Давайте возьмем пример из предыдушего поста – есть Person, у него есть один набор полей которые определяют адрес, и другой, который определяет работу.

public class Person
{
  public string Name {get;set;}
  public int Age {get;set;}
  // address
  public string StreetAddress {get;set;}
  public string PostCode {get;set;}
  public string Country {get;set;}
  // job
  public string CompanyName {get;set;}
  public string Position {get;set;}
  public int AnnualIncome {get;set;}
  // builder method
  public static PersonBuilder Create()
  {
    return new PersonBuilder(new Person());
  }
}

Возьмем как данное то, что нам нужно создать (в билдере) следующие вещи:

  • Методы для индивидуального присваивания полей, например WithStreetAddress()
  • Свойства и вложенные билдеры для сегментированного присваивания полей (PersonAddressBuilder, PersonJobBuilder)
  • Методы для аггрегированной инициализации сегментов, т.е. например SetFullAddress(), SetFullJob

Эти аттрибуты я предлагаю генерировать, то есть вместо того чтобы писать их ручками, мы напишем код который будет устойчив к изменениям – так что если мы например добавим новый блок свойств, builder обновится автоматически. Как их генерировать? Есть несколько вариантов – кодогенерация, AOP, метапрограммирование. Я предлагаю использовать последний вариант и язык программирования Boo, который неплохо подходит для этой задачи.

Базовая инфраструктура

Давайте для начала перепишем класс Person на Boo:

class Person:
  [Property(Name)] name as string
  [Property(Age)] age as int
  [Property(StreetAddress)] streetAddress as string
  [Property(PostCode)] postCode as string
  [Property(Country)] country as string
  [Property(CompanyName)] companyName as string
  [Property(AnnualSalary)] annualSalary as int

Теперь, сделаем аттрибут Builder который будет применяться непосредственно к объекту Person и порождать из него PersonBuilder.

class Builder(AbstractAstAttribute):
  public override def Apply(node as Node):
    // code here :)

Первое, что нужно сделать – это добавить метод Create() в изначальный класс. Делается это вот как:

private def AddFactoryMethod(node as ClassDefinition):
  builderRef = ReferenceExpression(node.Name + "Builder")
  classRef = ReferenceExpression(node.Name)
  m = [|
    def Create() as $(builderRef):
      return $(builderRef)($(classRef)())
  |]
  node.Members.Add(m)

Чтобы это заработало, нужен минимальный класс PersonBuilder который мы в последствии будем расширять:

private def AddBuilderClass(node as ClassDefinition):
  classRef = ReferenceExpression(node.Name)
  fieldRef = ReferenceExpression(fieldName(node.Name))
  builderRef = ReferenceExpression(node.Name + "Builder")
  c = [|
    class $(builderRef):
      $(fieldRef) as $(classRef)
      public def constructor($(fieldRef) as $(classRef)):
        self.$(fieldRef) = $(fieldRef)
      static def op_Implicit(o as $(builderRef)):
        return o.$(fieldRef)
  |]
  AddIndividualFluentMethods(c, node)
  (node.ParentNode as Module).Members.Add(c)

Имея эту базовую инфраструктуру можно начать добавлять те фичи, список которых я привел ранее.

Методы для индивидуального присваивания полей

Это на самом деле просто – для нашего класса, нужно обойти все свойства и для каждого сгенерировать простенький fluent-метод. Вот как выглядит решение:

private def AddIndividualFluentMethods(builder as ClassDefinition, source as ClassDefinition):
  builderRef = ReferenceExpression(builder.Name)
  for prop in source.Members:
    p = prop as Property
    if p != null:
      methodName = ReferenceExpression("With" + p.Name)
      propRef = ReferenceExpression(p.Name)
      paramRef = ReferenceExpression(fieldName(p.Name))
      m = [|
        public def $(methodName)($paramRef as $(p.Type)) as $(builderRef):
          person.$(propRef) = $paramRef
          return self
      |]
      builder.Members.Add(m)

Решение на самом деле простое – мы берем класс, ищем в нем все члены свойства, а потом для каждого генерим метод WithXxx() который инициализирует соответствующее свойство и делает return this.

Комбинированное присваивание полей

Чтобы сделать такой «финт ушами», потребуется намного больше усилий. Первая проблема в том, как сказать аттрибуту какие свойства нужно объединять в группы. Лично мне проще определить все это отдельной строкой, например Address=StreetAddress,PostCode,Country;.... Для того чтобы эту строку передать в аттрибут Builder, мы добавляем ему конструктор с соответствующим параметром:

class Builder(AbstractAstAttribute):
  entries as Hash
  public def constructor(initString as StringLiteralExpression):
    entries = Hash()
    parts = /;/.Split(initString.Value)
    for part in parts:
      elements = /=/.Split(part)
      props = /,/.Split(elements[1])
      entries[elements[0]] = props

Получив данные, мы можем генерировать соответствующие методы. Делается это просто – мы обходим хэш-таблицу и для каждой группы эмитируем метод, который берет в качестве параметров все параметры, присваивает их и возвращает this:

private def AddCombinationFluentMethods(builder as ClassDefinition, source as ClassDefinition):
  for entry in entries:
    m = Method("With" + entry.Key)
    for p in entry.Value as (string):
      // look for this param in the class
      for sp in source.Members:
        prop = sp as Property
        if prop != null and p == prop.Name:
          mp = ParameterDeclaration(fieldName(prop.Name), prop.Type)
          m.Parameters.Add(mp)
          sourceRef = ReferenceExpression(fieldName(source.Name))
          propRef = ReferenceExpression(prop.Name)
          a = [|
            $sourceRef.$propRef = $mp
          |]
          m.Body.Add(a)
    m.Body.Statements.Add([| return self |])
    builder.Members.Add(m)

Тем самым, мы автогенерировали методы на подобии вот этого:

public PersonBuilder WithAddress(string streetAddress, string postCode, string country)
{
    this.person.StreetAddress = streetAddress;
    this.person.PostCode = postCode;
    this.person.Country = country;
    return this;
}

(Это я из Рефлектора вытащил.) В коде выше есть одна неловкость, связанная с тем, что мы записали в хэш-таблице только названия свойств но не их типы, что привело к некоторым проблемам. Впрочем, это некритично, ибо в плане производительности мы ничего толком не теряем – все поиски происходят на этапе компиляции.

Методы для аггрегированной инициализации сегментов

Ну и наконец закончим тем, что создадим сборщики для подсвойств. Напомню, что для этого нужно

  • Добавить поля в корневой сборщик
  • Пробросить в каждый сборщик инстанс собираемого объекта
  • В каждом сборщике реализовать набор нужных свойств
  • В каждом сборщике реализовать приведение типов
  • В каждом сборщике реализовать свойства других сборщиков :)

Итак, у нас начинается драма в 4-х частях. В первой части, мы добавляем новые классы PersonXxxBuilder:

// iterate over each set
for entry in entries:
  // create and a stub class
  classRef = ReferenceExpression(source.Name + entry.Key + "Builder")
  targetRef = ReferenceExpression(source.Name)
  outerFieldRef = ReferenceExpression(fieldName(source.Name))
  innerClass = [|
    class $classRef:
      $outerFieldRef as $targetRef
      public def constructor($(outerFieldRef) as $targetRef):
        self.$(outerFieldRef) = $(outerFieldRef)
      static def op_Implicit(o as $builder):
        return o.$(outerFieldRef)
  |]

Вторая задача – это добавление всех нужных setтеров для внутренних классов:

// spawn each setter for this class
for prop in entry.Value:
  for sp in source.Members:
    p2 = sp as Property
    if p2 != null and prop == p2.Name:
      methodRef = ReferenceExpression("With" + prop)
      fieldRef = ReferenceExpression(fieldName(prop))
      propRef = ReferenceExpression(prop as string)
      // let's do it
      setter = [|
        public def $methodRef($fieldRef as $(p2.Type)) as $classRef:
          $outerFieldRef.$propRef = $fieldRef
          return self
      |]
      innerClass.Members.Add(setter)

Теперь мы добавляем в корневой билдер нужные свойства

// ensure a property exists for this class in the root builder
prop = [|
  public $(ReferenceExpression(entry.Key as string)):
    get:
      return $classRef($(ReferenceExpression(fieldName(source.Name))))
|]
builder.Members.Add(prop)

Ну и наконец мы добавляем все «дивергентные» свойства:

// ensure inner builders expose divergent properties
for entry2 in entries:
  if entry.Key != entry2.Key:
    otherClassRef = ReferenceExpression(source.Name + entry2.Key + "Builder")
    prop = [|
      public $(ReferenceExpression(entry2.Key as string)):
        get:
          return $otherClassRef($outerFieldRef)
    |]
    innerClass.Members.Add(prop)
// add it all
builder.Members.Add(innerClass)

Заключение

Вот и всё!!! Компилятор на этом этапе сгенерирует все нужные структуры и у нас получится ровно то же, что и в первом посте – только при добавлении новых структур, дополнительные сборщики будет очень просто добавлять (или удалять). Я уже устал писать этот пост – если интересно посмотреть на исходный код, его можно найти тут. Удачи!

Реклама

Written by Dmitri

25 августа 2010 в 16:20

Опубликовано в .NET, C#, Design Patterns

комментариев 9

Subscribe to comments with RSS.

  1. А как подобный код встраивать в проекты на C#?

    Michel Beloshitsky

    26 августа 2010 at 20:10

    • Во-первых, можно добавлять assembly reference и делать ILmerge. Но если хочется C#, открываем Рефлектором сборку, копируем, вставляем.

      Dmitri

      26 августа 2010 at 21:56

  2. Спасибо за статью, полезный материал!

    nastatiazjxm

    28 августа 2010 at 2:55

  3. а если по старинке CodeSmith использовать? чем метапрограммирование в Boo лучше, чем в CodeSmith?

    Беко

    31 августа 2010 at 10:06

    • Кодогенераторы — тоже вариант, в принципе.

      Dmitri

      31 августа 2010 at 10:33

      • А вот интересно, когда в CodeSmith пишешь программу для генерации программы, это метапрограммирование для кодогенерации?

        Беко

        31 августа 2010 at 12:10

        • Это просто кодогенерация.

          Dmitri

          31 августа 2010 at 13:09

  4. Не подскажете ли, как пользоваться классом Builder?
    т.е. мы передаем в конструктор StringLiteralExpression


    aa = Builder(StringLiteralExpression("Address=StreetAddress,PostCode,Country"))

    а как дальше, я в ступоре)

    existen

    17 февраля 2011 at 15:08

    • Не так. Builder — это макрос, следовательно используется он вот так:

      [Builder]
      class MyClass:
        ...
      

      Dmitri

      17 февраля 2011 at 15:22


Оставить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: