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

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

Archive for the ‘Design Patterns’ Category

Использование ImpromptuInterface в реализациях паттернов проектирования

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

Вы наверняка уже знаете, что если отнаследоваться от DynamicObject, можно получить разные плюшки от DLR, такие как например возможность вызывать методы класса даже если у класса их нет, и все такое. Но знаете ли вы что можно «вывесить» этот ваш DynamicObject как абсолютно любой интерфейс? Зачем это надо, спросите вы — ну, для того чтобы реализовывать всякие паттерны проектирования. Вот парочка примеров.

Как это работает?

После добавления NuGet пакета ImpromptuInterface, у вас появится extension-метод ActLike<T>(), который будет доступен на всех объектах типа object. Параметр T в данном случае – это именно тип интерфейса который хочется вывесить наружу.

За кулисами этого вызова создается, конечно же, динамическая прокся, которая является одновременно и проксёй для оригинального объекта и при этом реализует нужный интерфейс.

А теперь давайте посмотрим для чего это надо.

Паттерн Null Object

Самый простой пример — это паттерн Null Object, т.е. объект, который ровным счетом ничего не делает.

Вот например, у вас есть интерфейс логирования

public interface ILog
{
  void Info(string msg);
  void Warn(string msg);
}

Потом вы используете этот механизм в каком-то другом классе, например логируете операции с банковским счетом:

public class BankAccount
{
  private ILog log;
  private int balance;
  public BankAccount(ILog log)
  {
    this.log = log;
  }
  public void Deposit(int amount)
  {
    balance += amount;
    // check for null everywhere
    log?.Info($"Deposited ${amount}, balance is now {balance}");
  }
  public void Withdraw(int amount)
  {
    if (balance >= amount)
    {
      balance -= amount;
      log?.Info($"Withdrew ${amount}, we have ${balance} left");
    }
    else
    {
      log?.Warn($"Could not withdraw ${amount} because balance is only ${balance}");
    }
  }
}

Представьте теперь, что логирование вам нафиг не сдалось. Что делать? Передавать null нельзя, будут NRE везде где вызовы, ведь safe call operator ?. повсеместно разработчики, как правило, не используют. Ждем non-nullable types в C#8 или позже.

Один вариант — просто сделать фейковый класс:

public sealed class NullLog : ILog
{
  public void Info(string msg)
  {
    
  }
  public void Warn(string msg)
  {
    
  }
}

Это и есть «каноническая» реализация паттерна Null Object, но она получается легко только когда членов у класса мало, или же ReSharper под рукой. В целом, не хочется писать всю эту муть, особенно если вас не так сильно волнует перформанс.

Итак, третий, «ядерный» вариант с использование ImpromptuInterface. Делаем класс со следующей сигнатурой:

public class Null<T> : DynamicObject where T:class
{
  ⋮

T параметр выше – как раз тип интерфейса. Теперь можно сделать из класса этакий псевдо-singleton (на самом деле нет, создаем каждый раз):

⋮
public static T Instance 
{
  get
  {
    if (!typeof(T).IsInterface)
      throw new ArgumentException("I must be an interface type");
    return new Null<T>().ActLike<T>();
  }
}
⋮

Магия в том, что мы делаем Null<T>, сохраняя тип аргумента, но вывешиваем его не как Null<T> а просто как T.

А что дальше? Дальше нам нужно перехватывать вызовы методов, например:

  ⋮
  public override bool TryInvokeMember(InvokeMemberBinder binder, 
    object[] args, out object result)
  {
    result = Activator.CreateInstance(binder.ReturnType);
    return true;
  }
}

Код выше смотрит на тип возвращаемого значения метода и берет его дефолтную реализацию, т.е. вызывает new T() через System.Activator — это конечно же чревато тем, что если у класса нет дефолтного конструктора, то result будет равен null что очень грустно но не критично если вы не будете пытаться этим результатом пользоваться.

Вот собственно и всё, наш null object готов:

var log = Null<ILog>.Instance;
var ba = new BankAccount(log);
ba.Deposit(100); // логирования не будет

Динамическая Прокси

А теперь давайте представим «обратный» сценарий: у вас есть класс банковского счета, вы хотите добавить логирование «со стороны».

public interface IBankAccount
  {
    void Deposit(int amount);
    bool Withdraw(int amount);
    string ToString();
  }
  public class BankAccount : IBankAccount
  {
    private int balance;
    private int overdraftLimit = -500;
    public void Deposit(int amount)
    {
      balance += amount;
      WriteLine($"Deposited ${amount}, balance is now {balance}");
    }
    public bool Withdraw(int amount)
    {
      if (balance - amount >= overdraftLimit)
      {
        balance -= amount;
        WriteLine($"Withdrew ${amount}, balance is now {balance}");
        return true;
      }
      return false;
    }
    public override string ToString()
    {
      return $"{nameof(balance)}: {balance}";
    }
  }

Допустим что вы хотите логировать количество методов, которые вызываются на BankAccount. Для этого мы снова делаем динамическую проксю, только в этот раз мы уже агрегируем нужный нам объект:

public class Log<T> : DynamicObject where T : class, new()
{
  private readonly T subject;
  private Dictionary<string, int> methodCallCount =
    new Dictionary<string, int>();
  protected Log(T subject)
  {
    this.subject = subject;
  }
  ⋮

Далее можно сварганить фабричный метод, который будет делать тип Log<T> и выдывать его как интерфейс I. Например:

⋮
public static I As<I>() where I : class
{
  if (!typeof(I).IsInterface)
    throw new ArgumentException("I must be an interface type");
  return new Log<T>(new T()).ActLike<I>();
}
⋮

Теперь нам нужно сделать так, чтобы при вызове метода, метод-то вызывался, но кол-во методов тоже записывалось. Для этого нужно перегрузить TryInvokeMember():

⋮
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
  try
  {
    // logging
    WriteLine($"Invoking {subject.GetType().Name}.{binder.Name} with arguments [{string.Join(",", args)}]");
    // more logging
    if (methodCallCount.ContainsKey(binder.Name)) methodCallCount[binder.Name]++;
    else methodCallCount.Add(binder.Name, 1);
    result = subject.GetType().GetMethod(binder.Name).Invoke(subject, args);
    return true;
  }
  catch
  {
    result = null;
    return false;
  }
}
⋮

Что делает код выше? Ну, помимо логирования количества вызовов, он через reflection вызывает нужный метод на объекте и сохраняет результат. Если вдруг падает исключение — что поделать, бывает, пишет в результат null, и всё.

Ну и наконец, если вам вдруг надо чтобы работал честный ToString() на объекте, но он декорировал существующий ToString() того типа, на котором ведется логирование, то это можно сделать вот так:

  ⋮
  public string Info
  {
    get
    {
      var sb = new StringBuilder();
      foreach (var kv in methodCallCount)
        sb.AppendLine($"{kv.Key} called {kv.Value} time(s)");
      return sb.ToString();
    }
  }
  
  // will not be proxied automatically
  public override string ToString()
  {
    return $"{Info}{subject}";
  }
}

Вот собственно и всё, теперь можно юзать вот эту проксю как-то вот так:

var ba = Log<BankAccount>.As<IBankAccount>();
ba.Deposit(100);
ba.Withdraw(50);
WriteLine(ba);

Заключение

Возможность вывесить любой интерфейс из DynamicObject-а бывает очень удобной. Идинственное что за эту возможность придется платить перформансом, так что класть это в продакшн или нет – решать вам. Но в тестировании бывает полезно.

Если вам интересны паттерны, у меня есть более 10 часов видосиков по паттернам в C#/.NET. Enjoy!

Реклама

Written by Dmitri

4 мая 2017 at 12:41

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

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 at 16:20

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