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

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

Dynamic container proof of concept

3 комментария

Итак, я обещал запилить proof of concept идеи касательно динамически изменяемых программ, так что давайте начнем. Для начала нужно сделать контейнер с набором типов и механизмом регистрации:

public class DynamicContainer
{
  private Dictionary<Type, Type> registry
    = new Dictionary<Type, Type>();
  public void Register<TRequested, TReturned>()
    where TReturned : TRequested
  {
    // todo
  }
}

Пока все достаточно банально, и мы даже не разбили контейнер на “билдер” (классический GoF паттерн) и сам контейнер, как это делает например Autofac. Но в какой-то момент нам придется таки обойти весь граф зависимостей (давайте представим что мы делаем только property injection) и записать куда-то эти результаты.

Это все очень легко делается. Просто добавляем очередной dictionary который держит список зависимостей для каждого типа. Кстати сюда же можно добавить ассоциативный контейнер в обратную сторону — специально для тех кому лень искать в нугетах bidirectional directionary:

private readonly Dictionary<Type, HashSet<Type>> dependencies
  = new Dictionary<Type, HashSet<Type>>();
private readonly Dictionary<Type, HashSet<Type>> affected
  = new Dictionary<Type, HashSet<Type>>();

Теперь, в момент регистрации любого компонента, эти структуры нужно заполнять. Давайте представим что у нас property injection реализован с помошью атрибута [Service]: тогда мы просто берем и для каждого типа находим все проперти с этим атрибутом, и добавляем все что надо в оба словаря:

public DynamicContainer Register(Type requested, Type returned)
{
  var props = returned.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic)
    .Where(p => CustomAttributeExtensions.GetCustomAttribute<ServiceAttribute>((MemberInfo) p) != null);
  foreach (var p in props)
  {
    dependencies.Set(returned, p.PropertyType);
    affected.Set(p.PropertyType, returned);
  }
  registry.Set(requested, returned);
  return this;
}

Здесь и далее, Set() — это upsert операция.

Теперь, в процессе создания типа, должны быть удовлетворены все его зависимости, то есть:

private object Resolve(Type type)
{
  var actualType = registry[type];
  var instance = Activator.CreateInstance(actualType);
  if (dependencies.ContainsKey(actualType))
  {
    foreach (var p in actualType.GetProperties(BindingFlags.Instance|BindingFlags.NonPublic)
      .Where(p => p.GetCustomAttribute<ServiceAttribute>() != null))
    {
      p.SetValue(instance, Resolve(p.PropertyType));
    }
  }
  return instance;
}

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

public T Resolve<T>()
{
  return (T) Resolve(typeof(T));
}

Теперь сценарий: у нас есть машина которая использует некий сервис…

public class Car
{
  [Service]
  private IAlarmService AlarmService { get; set; }
  public void TriggerAlarm()
  {
    Console.WriteLine("Triggering alarm⋮");
    AlarmService.Trigger();
  }
}

Сам по себе сервис имеет метод Alarm() который просто пишет в консоль:

public class AlarmService : IAlarmService
{
  public void Trigger()
  {
    Console.WriteLine("Alarm has been sent to HQ");
  }
}

Так вот, все это счастье можно спокойно использовать вот так:

var container = new DynamicContainer();
container
  .Register<IAlarmService, AlarmService>()
  .Register<Car>();
var car = container.Resolve<Car>();
car.TriggerAlarm(); // Alarm has been sent to HQ

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

var text = File.ReadAllText(@"..\..\AlarmService.cs");
text = text.Replace("has been sent", "has NOT been sent");
var ipc = new InProcessCompiler();
var newServiceType = ipc.Compile(text);
container.Register(newServiceType.GetInterfaces()[0],
  newServiceType);

Тут происходит следующее:

  • Мы вычитываем сорцы сервиса который хотим менять. Заметьте что это подразумевает что у нас one class per file.

  • Берем и меняем исходник. На этом этапе можно также сохранить эти сорцы в тот файл откуда взяли.

  • Создаем компилятор (это отдельный класс который просто использует компиляторный API чтобы что-то собрать) и компилируем сорцы.

  • На выходе у нас — System.Type. Просто берем и регистрируем его заного в контейнере.

Вот собственно и всё — реализация обновилась. Можно пересоздать объект Car и тут же воспользоваться новым сервисом:

car = container.Resolve<Car>();
car.TriggerAlarm(); // Alarm has NOT been sent to HQ

Такой вот proof of concept. Думаю вы поняли, что если держать реестр всех созданных контейнером объектов (как WeakReference-ы конечно же), то можно прямо в методе Register() пройтись по выданным объектам и обновить их сервисы, без какого либо пересоздания.&npsp;■

Реклама

Written by Dmitri

25 июля 2018 в 20:19

Опубликовано в .NET, Technology

комментария 3

Subscribe to comments with RSS.

  1. Обожаю эти заметки — «Как сделать ниструя из нихрена, написав при этом километр кода»! Вот кому оно нужно — эти контейнеры, фабрики, ХреньПоПроизводствуХрени… ? Ткни в любой проект — его спокойно можно переписать с «человеческой» структурой, сохраняя ту же гибкость. Развелось, блин, архитекторов! Начитаются идиотии всякой, а потом я**** … впрочем, не будем про *опы. :)

    Napisun

    25 июля 2018 at 21:31

    • Не ну я-то не виноват что вы не поняли что тут происходит.

      Dmitri

      25 июля 2018 at 21:33

  2. По личному опыту — концепция «динамических программ» (+ еще и компилируемых on-the-fly) звучит круто и на уровне proof-of-concept все выглядит просто и красиво. Но в реальной жизни сталкиваешься невероятным количеством засад и скрытых ограничений платформы. И чем дальше в лес, тем сложнее и нестабильнее становится решение и начинаешь загибаться под валом компромисов и сайд-эффектов. В качестве реально примера в мире .NET могу привести Orchard CMS v1 vs v2. В первой версии есть имено полностью динамическая система модульности (с компиляцией на лету). В v2 aka Orchard Core от нее отказались полностью, и это неспроста …

    Rustam Babadjanov

    2 сентября 2018 at 9:03


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

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

Логотип WordPress.com

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

Google+ photo

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

Фотография Twitter

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

Фотография Facebook

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

Connecting to %s

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