Pull to refresh

PostSharp. Отложенная загрузка зависимостей

Reading time7 min
Views1.9K
Original author: Matthew D. Groves
Кусок кода, представленный ниже, вы наверняка писали не один раз. А что более вероятно – десятки раз. Обычно это пишется, когда необходимо использовать некий репозиторий, который будет предоставлять данные для вашего приложения. Если у вас мало времени, и вы торопитесь, это отличный способ получить что-либо таким образом, что это будет загружено в память только тогда, когда это вам понадобится, но не раньше (например, операция сохранения повлекла за собой обращение к подсистеме сериализации, однако до этого она не была нужна). И ведь на самом деле получается, что этот кусок кода с одной стороны одинаков, а с другой – его приходится писать не один раз.
Как правило, я и многие программисты, предпочитаем использовать в данном месте IoC контейнер, чтобы решать задачи такого рода. Однако это не всегда так просто сделать, особенно когда я программирую в рамках отсутствия Dependency Injection в библиотеке, которую я использую (WinForms, WebForms, …). Давайте разберемся, почему решая эту задачу без использования PostSharp, вы потратите гораздо больше времени и проделаете больше работы.


Итак, часто ли вам попадался такой код:

private IProductRepository _productRepository;
private IProductRepository ProductRepository
{
  get
  {
    if(_productRepository == null)
    {
      _productRepository = new ProductRepository();
    }
    return _productRepository;
  }
}

* This source code was highlighted with Source Code Highlighter.


В то время как свойство имеет тип IPropertyRepository, getter этого свойства все еще жестко завязан на тип ProductRepository. Если вам будет необходимо поменять “new ProductRepository()” на “new RevisedProductRepository()” или на “new ProductRepository(2011)”, вам необходимо будет пройтись по всем местам шаблонного кода и обновить его. Конечно же, руки сразу же потянутся проделать Find->Replace, Copy->Paste… Однако, всем вам знакома такая ситуация, правда? Во-первых, вы потратите уйму времени на замены, а во-вторых этим придется заниматься чуть ли не каждому члену вашей команды (когда придется делать очередные замены). И, поскольку, все уже активно используют IoC контейнеры, вам может показаться, что этот пример утрирован. Однако поверьте, такие примеры очень часто встречаются! Так или иначе, используя IoC контейнер, мы получим следующий код:

private IProductRepository _productRepository;
private IProductRepository ProductRepository
{
  get
  {
    if(_productRepository == null)
    {
      _productRepository = ObjectFactory.GetInstance<IProductRepository>();
    }
    return _productRepository;
  }
}

* This source code was highlighted with Source Code Highlighter.


Теперь, как вы видите, всю заботу о создании новых экземпляров берет на себя ObjectFactory. Если вам понадобилось создать новые экземпляры класса, добавить параметры конструктору и провести прочие операции во время создания своего объекта, это можно сделать в одном месте, не затрагивая другие части программы (замечу только что “ObjectFactory” – статический класс в StructureMap. Однако, вы можете использовать абсолютно любой IoC контейнер, какой вам больше нравится). Теперь наш код выглядит намного лучше, и команда разработки не должна сильно волноваться о реализации: только об интерфейсе. Это самая что ни есть настоящая инверсия зависимости, т.к. сейчас класс зависит только от интерфейса, не от реализации.
Код стал намного чище, однако все еще содержит проверки на “null” и ленивую инициализацию, которые будут находиться по всему нашему приложению (если мы довольно часто используем lazy loading, конечно). Также, нам не так просто поменять тот же IoC контейнер, если он нас перестал устраивать. И если вы в какой-то момент решили, что проверка на null недостаточна (например, ваша программа стала многозадачной), вам опять придется лезть по всему коду и менять, менять, менять… Посмотрим, как можно решить эту проблему окончательно, используя PostSharp:

[LoadDependency] private IProductRepository _productRepository;

[Serializable]
public sealed class LoadDependencyAttribute : LocationInterceptionAspect
{
  public override void OnGetValue(LocationInterceptionArgs args)
  {
    args.ProceedGetValue(); // fetches the field and populates the args.Value
    if (args.Value == null)
    {
      var locationType = args.Location.LocationType;
      var instantiation = ObjectFactory.GetInstance(locationType);

      if (instantiation != null)
      {
        args.SetNewValue(instantiation);
      }
      args.ProceedGetValue();
    }
  }
}

* This source code was highlighted with Source Code Highlighter.


Теперь у нас готов декларативный подход в использовании зависимостей. Вы всего лишь помечаете любое поле атрибутом “LoadDependency”, и оно будет проинициализировано IoC контейнером во время первого обращения (см. первую строчку). Таким образом, вы защитили себя от постоянного написания шаблонного кода для каждого свойства, которое будет использовать lazy-loading, проверки на null, потоко-безопасность и прочие-прочие изменения, которые вам пришлось бы делать, пишите вы каждое свойство по отдельности. Теперь все ваши действия будут находиться в одном классе.
Используя очень простой и маленький (всего-то 19 строчек) аспект, мы сжали результирующий код с 12 строк до одной. Мы убрали много бессмысленно-повторяющегося кода (DRY) и заменили жесткие зависимости чистыми интерфейсами (DIP). Мы вынесли lazy-loading в свой собственный класс, а разрешение зависимостей – в свой (SRP). Уменьшив, таким образом, количество зависимостей до минимума.
Этот очень простой пример на самом деле очень сильно помогает в моей повседневной разработке и делает ее намного проще.
Теперь, давайте предположим, что все зависимости известны на стадии компиляции. Также предположим, что зависимости не будут меняться во время работы приложения. В этом случае нам более не нужен Service Locator. Мы можем переопределить метод CompileTimeInitialize из LocationInterceptionAspect чтобы разрешить зависимости во время компиляции и сохранить тип в какое-то поле. Затем, во время работы приложения, вы можете использовать Activator.CreateInstance для того чтобы создать нужный объект.

[Serializable]
public sealed class LoadDependencyAttribute : LocationInterceptionAspect
{
  private Type _type;

  public override bool CompileTimeValidate(PostSharp.Reflection.LocationInfo locationInfo)
  {
    _type = DependencyMap.GetConcreteType(locationInfo.LocationType);
    if(_type == null)
    {
      Message.Write(SeverityType.Error, "002",
                    "A concrete type was not found for {0}.{1}",
                    locationInfo.DeclaringType, locationInfo.Name);
      return false;
    }
    return true;
  }

  public override void OnGetValue(LocationInterceptionArgs args)
  {
    args.ProceedGetValue();
    if (args.Value == null)
    {
      form.LogListBox.Items.Add("Instantiating UserService");
      args.SetNewValue(Activator.CreateInstance(_type));
      args.ProceedGetValue();
    }
  }
}

* This source code was highlighted with Source Code Highlighter.


Если у вас не сконфигурирован ни один тип, то вы получите ошибку на стадии компиляции, а не на стадии работы приложения, что очень удобно. Я надеюсь, что вы уже начинаете понимать что PostSharp – это очень мощное средство. Конечно же, я не заинтересован в том, чтобы использовать этот метод повсеместно, где это необходимо или не необходимо. Однако я уверен, что он полезен при многих обстоятельствах.
Еще один вариант улучшить полученный аспект – сделать проверку во время компиляции, что аспект корректно используется. Например, нет смысла использовать аспект LoadDependency к применительно к полю, если это поле не интерфейс. Потому что это означало бы, что наши зависимости hard-coded и необходимо опять все менять! :) Так давайте добавим пару лишних проверок:

public override bool CompileTimeValidate(PostSharp.Reflection.LocationInfo locationInfo)
{
  if(!locationInfo.LocationType.IsInterface)
  {
    Message.Write(SeverityType.Error, "001",
            "LoadDependency can only be used on Interfaces in {0}.{1}",
            locationInfo.DeclaringType, locationInfo.Name);
    return false;
  }

  _type = DependencyMap.GetConcreteType(locationInfo.LocationType);
  if(_type == null)
  {
    Message.Write(SeverityType.Error, "002",
            "A concrete type was not found for {0}.{1}",
            locationInfo.DeclaringType, locationInfo.Name);
    return false;
  }
  return true;
}

* This source code was highlighted with Source Code Highlighter.


Чтобы получить ошибки компиляции, выставим атрибут “LoadDependency” для поля, которое имеет любой тип, но не интерфейс:



Итак, с простым аспектом в купе с IoC контейнером, мы избавились от жестких зависимостей и шаблонного кода и сделали наше приложение легко поддерживаемым, легким в тестировании и чтении исходного кода. И теперь, что не менее важно, наше приложение полностью следует принципам SOLID. Теперь вы сохраните много времени, не только на стадиях разработки и тестирования, но и на стадии поддержки.

Остальные переводы и ссылки:
Tags:
Hubs:
+4
Comments13

Articles

Change theme settings