Pull to refresh

StructureMap — краткий справочник для работы (2/3)

Reading time10 min
Views3.2K
Продолжение первого поста о StructureMap

В первой части были освещены темы:
  • Установка
  • Регистрация (Основа, Профили, Плагины, Сканирование, Внедрение)

В этой части пойдет речь о:
  • Конструкторы (Простые типы, Конструктор по умолчанию, Составные типы, Приведение типов, Задание аргументов)
  • Свойства (Простое задание свойств, Встроенное задание свойств, Задание свойств фреймворком, Допостроение существующих классов)
  • Время жизни


Конструкторы


Очень важный вопрос в реальном программировании применительно к разрешению зависимостей в IoC контейнерах, как быть с классами, у которых несколько конструкторов. Как их инициализировать, как задавать параметры, как дорабатывать и прочее и прочее. Надеюсь что на большинство вопросов ниже будет дан ответ. StructureMap действительно мощная и гибкая штука.

Перед тем как начать описывать возможности фреймворка, надо поговорить о тестовых классах. В этот раз они будут сложнее. Наследование, конструкторы с простыми типами, с составными.



Итак, пусть у нас будут следующие классы:
public interface IClassA : IClass {
     int A { get; set; }
}

public interface IClassB : IClass {}

public class ClassA : IClassA {
    public int A { get; set; }
    public int B { get; set; }
    public Class1 Class1 { get; set; }

    [DefaultConstructor]
    public ClassA() {}

    public ClassA(int a) {
         A = a;
    }
}

public class ClassB : IClassB {
     public IClassA ClassA;

     public ClassB(IClassA classA) {
         ClassA = classA;
     }
}

public class ClassM : IClassA {
    public int A { get; set; }

    public ClassM(int a) {
        A = a;
    }

    public ClassM(int a, int b) {
        A = a + b;
    }
}

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

Итак, начнем с самых простых вариантов.

Простые типы


Начнем с класса ClassA, у которого один из конструкторов принимает целочисленный параметр.
public class WithSimpeArguments {
     public IContainer Container;

     public WithSimpeArguments() {
          Container = new Container(x => x.For<IClassA>().Use<ClassA>()
                                                            .Ctor<int>().Is(5)
                                             );
    }
}

Как видно из примера, к уже привычному объявлению добавляется указание на инициализацию конструктора значениями. В данном случае объявление конструктора идет по упрощенной схеме, так как только один параметр типа int. В процессе инициализации мы сразу указали значение. При этом StructureMap подскажет, что значение должно быть целочисленное.

Для проверки работы, можно вызвать уже привычный код
private static string WithSimpleArgumentsExample() {
    var container = new WithSimpeArguments().Container;
    var classA = (ClassA) container.GetInstance<IClassA>();

    return classA.A.ToString();
}

В результате на консоль выведется цифра 5.

Усложняем пример для случая, когда у нас в конструкторе два параметра одного типа. В качестве подопытного класс ClassM.
public class WithMultipleSimpeArguments {
     public IContainer Container;

     public WithMultipleSimpeArguments() {
         Container = new Container(x => x.For<IClassA>().Use<ClassM>()
                                               .Ctor<int>("a").Is(6)
                                               .Ctor<int>("b").Is(5));
    }
}

Теперь конструкция .Ctor<int>(«a»).Is(6) дополнена именем аргумента, для того, чтобы фреймворк смог точно сопоставить значения аргументам. Фреймворк всегда находит самый «жадный» конструктор и хочет, чтоб все аргументы были проинициализированы. Нельзя опустить задание значения для второго аргумента в текущей реализации класса. Но можно указать StructureMap, какой конструктор использовать по умолчанию, для этого надо использовать атрибут DefaultConstructor.

Конструктор по умолчанию


Атрибут DefaultConstructor позволяет явно указать, какой конструктор использовать для создания экземпляра класса. Чтобы можно было в предыдущем примере опустить объявление переменной b и ничего не упало в процессе работы.
public class ClassM : IClassA {
     public int A { get; set; }

    [DefaultConstructor]
    public ClassM(int a) {
        A = a;
    }

    public ClassM(int a, int b) {
         A = a + b;
    }
}

Теперь можно использовать конструктор с одним параметром

Составные типы


Работать с составными типами совсем легко, потому что StructureMap сам обнаруживает и разрешает все зависимости по составным типам. Т.е. если посмотреть в начало, на классы, то видно, что класс ClassB инициализируется классом ClassA. Сейчас увидим что ничего особенного для разрешения такого рода зависимостей не надо.
public class WithObjectArguments {
     public IContainer Container;

     public WithObjectArguments() {
          Container = new Container(x => {
                                      x.For<IClassA>().Use<ClassA>().Ctor<int>().Is(5);
                                      x.For<IClassB>().Use<ClassB>();
                                              });
    }
}

Как видно из примера, никаких дополнительных операторов не применено. Зато можно запросить класс ClassB, который будет инициализирован ClassA.
private static string WithObjectArgumentsExample() {
     var container = new WithObjectArguments().Container;
     var classA = (ClassB) container.GetInstance<IClassB>();

     return classA.ClassA.A.ToString();
}

На экран будет выведена цифра 5.

Приведение типов


Если посмотреть на объявление класса ClassB, то можно заметить что переменная в конструкторе является интерфейсом, а не конкретным типом. Перепишем класс так, чтобы вместо интерфейса, конструктор принимал класса ClassA
public class ClassB : IClassB {
     public ClassB(ClassA classA) {
          ClassA = classA;
     }
}

Теперь связывания типов не произойдет, потому что контейнер регистрирует и возвращает один тип данных. В нашем случае это IClassA.
private static string WithObjectArgumentsForwardingAndWiringExample() {
     var container = new WithObjectArgumentsForwarding().Container;
     var classB = (ClassB) container.GetInstance<IClassB>();

     return classB.ClassA.A.ToString();
}

Данный код вернет 0, так как будет вызван конструктор по умолчанию для класса ClassA, связывания не произошло.

Как вы догадались это не проблема. Можно указать StructureMap что к чему может быть приведено. Делается это с помощью команды Forward которой надо указать исходный тип и желаемый.
public class WithObjectArgumentsForwarding {
    public IContainer Container;

    public WithObjectArgumentsForwarding() {
         Container = new Container(x => {
                                     x.For<IClassA>().Use<ClassA>().Ctor<int>().Is(5);
                                     x.Forward<IClassA, ClassA>();
                                     x.For<IClassB>().Use<ClassB>();
                                             });
   }
}

Теперь можно вызывать метод  WithObjectArgumentsForwardingAndWiringExample и в ответе получить 5.

Задание аргументов


Чаще всего не удается заранее узнать и задать аргументы для класса, они становятся известны только на момент создания требуемого класса. И так как это рабочие будни написания программ, то в StructureMap не могла не появиться поддержка задания аргументов на момент создания класса.

Процесс вызова класса с новыми значениями аргументов похож на общение на языке мастера Йоды, но такова уж специфика DSL. Технические ограничения я имею в виду.

Итак, пусть мы хотим вызвать класс ClassB с новым классом ClassA. Для этого понадобится оператор With.
private static string WithObjectArgumentsOverridingExample() {
    var container = new WithObjectArgumentsForwarding().Container;
    var classB = (ClassB) container
                            .With(new ClassA(8))
                            .GetInstance<IClassB>();

    return classB.ClassA.A.ToString();
}

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

Если же надо указать большее количество аргументов для класса, то необходимо использовать большее количество метода With с указанием имени параметра и его значением.
private static string WithObjectArgumentsOverridingExample() {
     var container = new WithObjectArgumentsForwarding().Container;

     var classB = (ClassB) container
                             .With(new ClassA(8))
                             .With("s").EqualTo(5)
                             .GetInstance<IClassB>();

    return classB.ClassA.A.ToString();
}

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

Свойства


Помимо обязательных параметров конструктора, очень приятно во время создания экземпляра класса задавать значения свойств класса. Т.е. вместо того, чтобы писать
var classA = new ClassA();
classA.A = 8;
classA.B = 20;

можно гораздо короче и красивее задать свойства как:
var classA = new ClassA {
                         A = 8,
                         B = 20
};

Для StuctureMap это вполне по силам, причем все таким же элегантным и понятным способом.

Простое задание свойств


В самом простом случае, как на иллюстрации выше, можно задавать параметры с помощью метода SetProperty.
public class WithArgumentsSetProperty {
     public IContainer Container;

     public WithArgumentsSetProperty() {
         Container = new Container(x => x.For<IClassA>().Use<ClassA>()
                                               .Ctor<int>().Is(5)
                                               .SetProperty(p => {
                                                         p.A = 8;
                                                         p.B = 20;
                                             }));
    }
}

Как видно из примера, свойства строго типизированны и можно пользоваться подсказками intelliSense, т.е. задание свойств протекает легко и непринужденно. Понятное дело, что можно задавать не все свойства, а только те, что хочется проинициализировать на этапе построения экземпляра класса.

Встроенное задание свойств


Для использования встроенного (inline) задания параметров используется метод Setter. С помощью этого метода можно задавать значения для одного параметра за раз. Так как аргументом метода является функция.

Самое простое, это явное задание параметра для инициализации.
public class WithArgumentsSetterExplicit {
    public IContainer Container;

    public WithArgumentsSetterExplicit() {
        Container = new Container(x => {
                                    x.For<IClass1>().Use<Class1>();
                                    x.For<IClassA>().Use<ClassA>()
                                           .Ctor<int>().Is(5)
                                           .Setter(c => c.Class1).Is(new Class1());
                                            });
    }
}

В примере показано, что свойство Class1 будет всегда проинициализированно новым классом Class1. Такая запись должна применяться, если у класса более одного свойства одинакового типа. Если же у вас только одно свойство заданного типа, то фреймворк сможет самостоятельно определить к какому свойству присвоить значение переданного параметра.

Итак, неявная инициализации свойства.
public class WithArgumentsSetterImplicit {
    public IContainer Container;

    public WithArgumentsSetterImplicit() {
        Container = new Container(x => {
                                    x.For<IClass1>().Use<Class1>();
                                    x.For<IClassA>().Use<ClassA>()
                                           .Ctor<int>().Is(5)
                                           .Setter<Class1>().Is(new Class1());
                                           });
    }
}

В этом примере мы не указали имя свойства, но все прошло успешно, так как свойство типа Class1 только одно.

Задание свойств фреймворком


Раз уж StructureMap может автоматически подставлять экземпляры классов в аргументы конструкторов, то он сможет автоматически заполнять свойства классов?

Конечно сможет!

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

Можно модифицировать предыдущий пример, так, чтобы зависимости свойств разрешались средствами StructureMap и контролировались им же.
public class WithArgumentsSetterImplicitDefault {
     public IContainer Container;

     public WithArgumentsSetterImplicitDefault() {
          Container = new Container(x => {
                                      x.For<IClass1>().Use<Class1>();
                                      x.For<IClassA>().Use<ClassA>()
                                             .Ctor<int>().Is(5)
                                             .Setter<Class1>().IsTheDefault();
                                             });
    }
}

В примере появился новый метод IsTheDefault, который говорит фреймворку разрешить зависимость своими средствами. Т.е. в данном случае свойство типа Class1 у класса ClassA будет создано и присвоено исходя из того, как зарегистрирован Class1.

Так же есть пакетная инициализация параметров, когда можно сказать что все свойства определенного типа должны инициализироваться значениями по умолчанию. Для этого используется командаSetAllProperties.
public class WithArgumentsSetterBatchImplicitDefault {
    public IContainer Container;

    public WithArgumentsSetterBatchImplicitDefault() {
         Container = new Container(x => {
                                     x.For<IClass1>().Use<Class1>();
                                     x.For<IClassA>().Use<ClassA>()
                                            .Ctor<int>().Is(5);
                                     x.SetAllProperties(c => c.OfType<Class1>());
                                             });
   }
}

С помощью этого указания StructureMap автоматически проинициализирует все свойства вызываемых классов, у которых типом свойства является Class1.

Допостроение существующих классов


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

Пусть к нам приходит ClassA который создается не в нашей системе. Надо проинициализировать его свойство типа Class1. Сначала настроим StructureMap.
public class WithArgumentsBuildUp {
     public IContainer Container;

     public WithArgumentsBuildUp() {
         Container = new Container(x => {
                                     x.For<IClass1>().Use<Class1>();
                                     x.Forward<IClass1, Class1>();
                                     x.SetAllProperties(c => c.OfType<Class1>());
                                            });
    }
}

Теперь можно вызвать метод BuildUp который достроит объект, по имеющимся конфигурациям.
var container = new WithArgumentsBuildUp().Container;
var classA = new ClassA(14);

container.BuildUp(classA);

На второй строчке свойство Class1 = null, после вызова BuildUp, объект полностью готов.

Время жизни


Немаловажным фактором является возможность управлением времени жизни объекта. Для каких-то классов надо получать один и тот же экземпляр, для других – каждый раз новые. Этим тоже можно управлять в процессе создания правил в контейнере.

Фреймворк оперирует семью политиками управления времени жизни объекта:
  • Per reques (default) – создается каждый раз новый объект.
  • HttpContextLifecycle
  • SingletonLifecycle
  • ThreadlocalStorageLifecycle
  • UniquePerRequestLifecycle – гарантирует что вся цепочка объектов инициализирующих запрошеный объект будет уникальной.
  • HttpSessionLifecycle
  • HybridLifecycle -  это HttpSessionLifecycle или ThreadlocalStorageLifecycle
  • HybridSessionLifecycle – или  HttpContextLifecycle, или HttpSessionLifecycle

Рассмотрим некоторые из них на примере простого класса
public class ClassX : IClassX {
    public int Counter { get; private set; }

    public void Increase() {
        Counter++;
    }
}

public interface IClassX {}

Первым на очереди пусть будет Singleton.
public class LifecycleSingleton {
    public IContainer Container;

    public LifecycleSingleton() {
         Container = new Container(x => x.For<IClassX>().LifecycleIs(new SingletonLifecycle()).Use<ClassX>());
         Container = new Container(x => x.For<IClassX>().Singleton().Use<ClassX>());
    }
}

Для основных политик жизни определены сокращенные методы. Т.е. можно использовать как Singleton(), так и LifecycleIs(new SingletonLifecycle()).

В качестве проверки можно использовать наглядный пример:
private static string LifecycleSingleton() {
    var singleton = new LifecycleSingleton().Container;
    var classX = (ClassX) singleton.GetInstance<IClassX>();

    classX.Increase();
    Console.WriteLine(classX.Counter);

    classX = (ClassX) singleton.GetInstance<IClassX>();

   classX.Increase();
   Console.WriteLine(classX.Counter);

   return "done";
}

В итоге на консоль выведутся данные: «1, 2, Done». При простом объявлении мы бы получили: «1, 1, Done».

Для хранения экземпляра класса в рамках одного потока используется ThreadLocalStorageLifecycle, или сокращенная форма записи HybridHttpOrThreadLocalScoped
public class LifecycleThreadLocal {
    public IContainer Container;

    public LifecycleThreadLocal() {
         Container = new Container(x => x.For<IClassX>()
                                            .LifecycleIs(new ThreadLocalStorageLifecycle())
                                            .Use<ClassX>());

        Container = new Container(x => x.For<IClassX>()
                                           .HybridHttpOrThreadLocalScoped()
                                           .Use<ClassX>());
   }
}

Для HttpContextLifecycle определена сокращенная запись HttpContextScoped
public class LifecycleHttpContext {
    public IContainer Container;

    public LifecycleHttpContext() {
        Container = new Container(x => x.For<IClassX>()
                                           .LifecycleIs(new HttpContextLifecycle())
                                           .Use<ClassX>());

        Container = new Container(x => x.For<IClassX>()
                                           .HttpContextScoped()
                                           .Use<ClassX>());
    }
}

Продолжение в следующей части.
Tags:
Hubs:
+14
Comments2

Articles