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

advertisement
Внедрение зависимостей.
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-контейнеры зачастую являются лишь
механизмом для преодоления трудностей,
вызванных «неправильным» дизайном
приложений.
Затруднено тестирования инварианта,
определяемого в модуле привязки
интерфейсов к конкретным реализациям.
Download