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

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

Использование 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 в 12:41

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

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

Subscribe to comments with RSS.

  1. Чем это лучше/хуже, скажем Castle DynamicProxy? необязательно Castle, реализаций dynamic proxy полно

    Podgorodnichenko Denis

    4 мая 2017 at 12:56

    • Ну, например, насколько я помню, DynamicProxy требует чтобы все члены класса были виртуальными.

      Dmitri

      7 мая 2017 at 10:52

      • Дмитрий, вы правы, но у нас же интерфейсы ))

        using Castle.DynamicProxy;
        namespace DynamicProxy
        {
            public interface ISomething
            {
                void Foo();
            }
            public class Interceptor : IInterceptor
            {
                public void Intercept(IInvocation invocation)
                {
                    Console.WriteLine("Hello world");
                }
            }
        
            class Program
            {
                static void Main(string[] args)
                {
                    var prox = new ProxyGenerator().CreateInterfaceProxyWithoutTarget(new Interceptor());
                    prox.Foo();
                }
            }
        }
        

        Podgorodnichenko Denis

        30 мая 2017 at 10:22

  2. По-моему, статические языки для того и придуманы, чтобы всякое распутство типа JS не лезло и не поганило платформу. DynamicObject — обычное заигрывание с динамикой, хотя C#/.NET прекрасно существует и без них. Придумывать (как в этой статье) какие-то жалкие юз-кейсы для оправдания всовывания этой динамики в .NET — нафик не нужно, мы не тупые и понимаем, чем всё это чревато.

    Vincent (@Dajte2)

    4 мая 2017 at 22:49

    • У меня целый продукт (X2C) написан на динамических визиторах. Клиенты обрабатывают порой гигантские таблицы. На динамиках. Да, это небыстро, но и не убийственно медленно.

      Dmitri

      4 мая 2017 at 23:56

  3. Мда, зависимость BankAccount от логгера уже попахивает трешем.

    Лично я абсолютный сторонник того что система логгирования (и, возможно, мониторинг) ортогональны обычным зависимостям и не должны ни инжектиться, ни заменяться моками а строго конфигурироваться.

    Андрей

    5 мая 2017 at 4:11


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

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

Логотип WordPress.com

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

Фотография Twitter

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

Фотография Facebook

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

Google+ photo

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

Connecting to %s

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