Внедрение зависимостей. IoC-контейнеры Лекция 03 Предпосылки Вернѐмся к примеру с обращением зависимостей. В результате применения указанного принципа мы получили следующий код: public class DataScripter { private readonly IDataDumper dataDumper; private readonly TextWriter writer; public DataScripter(IDataDumper dataDumper, TextWriter writer) { this.dataDumper = dataDumper; this.writer = writer; } public void GetSqlDump(IDataReader dataReader) { while (dataReader.Read()) { string rowSqlDump = dataDumper.GetRowDump(dataReader); writer.WriteLine(rowSqlDump); } } } Предпосылки Пример использования вышеприведенного кода: public static void Main(string[] args) { IDbConnection connection = CreateConnection(); DataScripter scripter = new DataScripter(new InsertDataDumper(), Console.Out); using (IDbCommand command = connection.CreateCommand()) { command.CommandText = string.Format("SELECT * FROM {0}", args[0]); using (IDataReader reader = command.ExecuteReader()) { scripter.GetSqlDump(reader); } } } Пассивная инверсия зависимостей Заметим, что в рассмотренном выше случае мы применяли принцип инверсии зависимостей, а после этого внедряли зависимые объекты, передавая их параметрами конструктора. Существуют и другие способы передать конкретные реализации классов некоторому объекту, такие как: Property injection Interface injection Method Injection Указанные выше способы составляют группу пассивной инверсии зависимостей — необходимые объекты «впрыскиваются» в host-объект. Пассивная инверсия зависимостей Property injection: public class DataScripter { private readonly TextWriter writer; public IDataDumper DataDumper { get; set; } public DataScripter(TextWriter writer) { this.writer = writer; } } Method Injection: public class DataScripter { private readonly IDataDumper dataDumper; private TextWriter writer; public void ConfigureTextWriter(TextWriter writer) { this.writer = writer; } public DataScripter(IDataDumper dataDumper) { this.dataDumper = dataDumper; } } Пассивная инверсия зависимостей Interface injection подразумевает введение дополнительного интерфейса, который позволяет внедрять зависимости определенного типа: public interface ITextWriterInjector { void InjectTextWriter(TextWriter writer); } public class DataScripter : ITextWriterInjector{ private readonly IDataDumper dataDumper; private TextWriter writer; public void InjectTextWriter(TextWriter writer) { this.writer = writer; } public DataScripter(IDataDumper dataDumper) { this.dataDumper = dataDumper; } } Предпосылки использования IoC-контейнеров Обратим внимание на тот факт, что конкретный экземпляр объекта, который следует передать в конструктор, указывается непосредственно в точке вызова конструктора. Существует несколько способов инкапсуляции выбора конкретной реализации интерфейса: использование фабричных методов и абстрактных фабрик; использование паттерна ServiceLocator и его разновидностей. Паттерн Абстрактная фабрика Абстрактная фабрика может иметь следующий вид: public interface IDataScripterFactory { DataScripter CreateDataScripter(); } public class DataScripterFactory : IDataScripterFactory { protected virtual IDataDumper CreateDataDumper() { return new InsertDataDumper(); } protected virtual TextWriter GetOutputTextWriter() { return Console.Out; } public DataScripter CreateDataScripter() { return new DataScripter(CreateDataDumper(), GetOutputTextWriter()); } } Паттерн Абстрактная фабрика Также она может принимать экземпляры IDataDumper и TextWriter параметрами конструктора: public class DataScripterFactory : IDataScripterFactory { private readonly IDataDumper dataDumper; private readonly TextWriter outputWriter; public DataScripterFactory(IDataDumper dataDumper, TextWriter outputWriter) { this.dataDumper = dataDumper; this.outputWriter = outputWriter; } public DataScripter CreateDataScripter() { return new DataScripter(dataDumper, outputWriter); } } Паттерн Абстрактная фабрика Также возможен следующий вариант параметризации: public class DataScripterFactory<TDataDumper> : IDataScripterFactory where TDataDumper : IDataDumper, new() { public DataScripter CreateDataScripter() { return new DataScripter( new TDataDumper(), Console.Out); } } Паттерн ServiceLocator Суть паттерна ServiceLocator заключена в следующем классе: public static class ServiceLocator { private static readonly Dictionary<Type, Type> services = new Dictionary<Type, Type>(); public static void RegisterService<T>(Type service) { services[typeof (T)] = service; } public static T Resolve<T>() { return (T) Activator.CreateInstance(services[typeof (T)]); } } Он позволяет инкапсулировать связь между интерфейсом и еѐ конкретной реализацией. Паттерн ServiceLocator Метод Resolve. Позволяет получить конкретную реализацию абстрактного класса или интерфейса, переданного параметром шаблона. Метод RegisterService. Регистрирует конкретную реализацию абстрактного класса или интерфейса. Поле Dictionary<Type, Type> services позволяет классу ServiceLocator поддерживать отображение одного типа данных на другой. Паттерн ServiceLocator Теперь мы можем переписать класс DataScripter следующим образом: public class DataScripter { private readonly IDataDumper dataDumper; private readonly TextWriter writer; public DataScripter(TextWriter writer) : this(ServiceLocator.Resolve<IDataDumper>(), writer) { } public DataScripter(IDataDumper dataDumper, TextWriter writer) { this.dataDumper = dataDumper; this.writer = writer; } } Паттерн ServiceLocator Теперь использование класса DataScripter выглядит так: public static void Main(string[] args) { ServiceLocator.RegisterService<IDataDumper>(typeof(InsertDataDumper)); DataScripter scripter = new DataScripter(Console.Out); IDbConnection connection = CreateConnection(); using (IDbCommand command = connection.CreateCommand()) { command.CommandText = string.Format("SELECT * FROM {0}", args[0]); using (IDataReader reader = command.ExecuteReader()) { scripter.GetSqlDump(reader); } } } Активная инверсия зависимостей Заметим, что конструктор объекта DataScripter знает про глобальный объект ServiceLocator и запрашивает у него все необходимые интерфейсы: public class DataScripter { private readonly IDataDumper dataDumper; private readonly TextWriter writer; public DataScripter(TextWriter writer) : this(ServiceLocator.Resolve<IDataDumper>(), writer) { } } Если зависимый класс сам обращается к классу ServiceLocator, то такой подход называется pull approach и входит в группу активной инверсии зависимостей. Активная инверсия зависимостей Другой вариант активной инверсии зависимостей состоит в том, что объект локатора передаѐтся извне. Такой подход называется push approach. Данный способ инверсии зависимостей демонстрирует следующий вариант конструктора объекта DataScripter: public class DataScripter { private readonly IDataDumper dataDumper; private readonly TextWriter writer; public DataScripter(ServiceLocator serviceLocator, TextWriter writer) : this(serviceLocator.Resolve<IDataDumper>(), writer) { } } Паттерн ServiceLocator Обратим внимание, что до обращения зависимостей объект DataScripter знал про конкретные классы и создавал их напрямую. Паттерн ServiceLocator После обращения зависимостей объект DataScripter стал использовать только интерфейсы: Паттерн ServiceLocator После применения паттерна ServiceLocator добавился ещѐ один слой, который инкапсулирует в себе знание о выборе конкретных реализаций интерфейса: IoC-контейнеры Заметим, что класс ServiceLocator требует некоторой доработки. Например, мы можем захотеть контролировать количество создаваемых экземпляров класса. IoC-контейнер или Dependency Injection Framework ― это набор классов, которые реализуют механизм внедрения зависимостей. .NET PHP Symfony Dependency Injection DIContainer C++ Ninject Unity Application Block Autumn Framework QtIOCContainer Java Модуль в составе Spring Framework Модуль в составе Seasar IoC-контейнеры IoC-контейнеры позволяют инкапсулировать в отдельном классе выбор конкретных реализаций интерфейсов или абстрактных классов, а также выполнять автоматическое внедрение этих реализаций в зависимый объект. Механизм работы IoC-контейнеров демонстрирует следующий фрагмент псевдокода: class DataScipterIOCContainer : IOCContainer { protected override Configure() { Use(InsertDataDumper).AsImplementation(IDataDumper); Use(Console.Out).AsImplementation(TextWriter); } } public void Usage() { IOCContainer container = new DataScipterIOCContainer(); DataScripter scripter = container.Create(DataScripter); } Ninject Рассмотрим использование IoC-контейнеров на примере Ninject. Классом, который инкапсулирует в себе информацию о выборе конкретной реализации интерфейса, является наследник NinjectModule. В следующем фрагменте кода объявляется, что в качестве реализаций интерфейсов IDataDumper и TextWriter следует использовать экземпляр класс InsertDataDumper и значение свойства Console.Out соответственно: public class ApplicatoinModule : NinjectModule { public override void Load() { Bind<IDataDumper>().To<InsertDataDumper>(); Bind<TextWriter>().ToMethod(c => Console.Out); } } Ninject Использование рассмотренного выше класса выглядит следующим образом: public static void NinjectUsage() { StandardKernel kernel = new StandardKernel(new ApplicatoinModule()); TextWriter writer = kernel.Get<TextWriter>(); writer.WriteLine("..."); } Где StandardKernel ― это класс библиотеки Ninject, который позволяет внедрять зависимости в соответствии с правилами, описанными в экземпляре объекта, передаваемого в конструктор. Ninject Рассмотрим теперь использование Ninject для класса DataScripter. Для этого объявим класс-наследник NinjectModule следующим образом: public class ScriptingModule : NinjectModule { public override void Load() { Bind<IDataDumper>().To<InsertDataDumper>(); Bind<TextWriter>().ToMethod(c => Console.Out); Bind<DataScripter>().ToSelf(); } } В классе DataScripter необходимо отметить конструктор атрибутом [Inject]: public class DataScripter { [Inject] public DataScripter(IDataDumper dataDumper, TextWriter writer) } Ninject Теперь клиентский код будет выглядеть следующим образом: public static void Main(string[] args) { StandardKernel kernel = new StandardKernel(new ScriptingModule()); DataScripter scripter = kernel.Get<DataScripter>(); IDbConnection connection = CreateConnection(); using (IDbCommand command = connection.CreateCommand()) { command.CommandText = string.Format("SELECT * FROM {0}", args[0]); using (IDataReader reader = command.ExecuteReader()) { scripter.GetSqlDump(reader); } } } Ninject Помимо внедрения зависимостей через конструктор Ninject также поддерживает внедрение зависимостей через свойства и методы. Для внедрения зависимости через свойство его необходимо пометить атрибутом [Inject]: public class DataScripter { private readonly TextWriter writer; [Inject] public IDataDumper DataDumper { get; set; } [Inject] public DataScripter(TextWriter writer) { this.writer = writer; } } Ninject Внедрение зависимости посредством метода происходит аналогичным образом: public class DataScripter { private readonly IDataDumper dataDumper; private TextWriter writer; [Inject] public void ConfigureTextWriter(TextWriter writer) { this.writer = writer; } [Inject] public DataScripter(IDataDumper dataDumper) { this.dataDumper = dataDumper; } } DIContainer Рассмотрим теперь библиотеку внедрения зависимостей DIContainer для PHP5. Контейнер зависимостей в данном случае базируется на конфигурационных XML-файлах. Рассмотрим аналогичный класс DataScripter’а на языке PHP: class DataScripter { private $dataDumper; private $writer; public function __construct(IDataDumper $dataDumper, ITextWriter $writer) { $this->dataDumper = $dataDumper; $this->writer = $writer; } public function GetSqlDump(IDataReader $dataReader) { while ($dataReader->Read()) { $rowSqlDump = $this->dataDumper->GetRowDump(dataReader); $this->writer->WriteLine($rowSqlDump); } } } DIContainer Для указания правил выбора конкретных реализаций используется XML-файл следующего вида: <?xml version="1.0" encoding="shift_jis"?> <!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container//EN" "http://www.seasar.org/dtd/components21.dtd"> <components> <component name="textWriter" class="OutputBufferWriter" /> <component name="dataDumper" class="InsertDataDumper" /> <component autoBinding="auto" name="dataScripter" class="DataScripter" /> </components> Клиентский код выглядит следующим образом: $container = S2ContainerFactory::create('scripter.dicon'); // scripter.dicon – вышеприведенный файл. $dataScripter = $container->getComponent('DataScripter'); // ... $dataScripter->GetSqlDump($reader); DIContainer Данная библиотека поддерживает внедрение зависимостей через метод установки (setter injection). Для этого метод установки должен начинаться с set. class DataScripter { private $dataDumper; private $writer; public function __construct(ITextWriter $writer) { $this->writer = $writer; } public function setDataDumper(IDataDumper $dataDumper) { $this->dataDumper = $dataDumper; } } Конфигурационный файл и клиентский код остаются без изменений. Недостатки IoC-контейнеров IoC-контейнеры зачастую являются лишь механизмом для преодоления трудностей, вызванных «неправильным» дизайном приложений. Затруднено тестирования инварианта, определяемого в модуле привязки интерфейсов к конкретным реализациям.