Pull to refresh

Внедрение зависимостей для начинающих

Reading time7 min
Views6.4K
Original author: Steve Hobbs
Привет, Хабр!

У нас готовится к выходу второе издание легендарной книги Марка Симана "Внедрение зависимостей на платформе .NET".



Даже в такой объемной книге вряд ли возможно целиком охватить подобную тему. Но мы предлагаем вам сокращенный перевод очень доступной статьи, излагающей суть внедрения зависимостей простым языком — с примерами на C#.

Цель этой статьи – объяснить концепцию внедрения зависимостей и показать, как она программируется в отдельно взятом проекте. Из Википедии:

Внедрение зависимости – это паттерн проектирования, отделяющий поведение от разрешения зависимости. Таким образом удается открепить компоненты, сильно зависящие друг от друга.

Внедрение зависимости (или DI) позволяет предоставлять реализации и сервисы другим классам для потребления; код при этом остается очень слабосвязанным. Основной момент в данном случае таков: на место реализаций можно легко подставлять другие реализации, и при этом придется менять минимум кода, так как реализация и потребитель связаны, скорее всего, только контрактом.

В C# это означает, что реализации ваших сервисов должны отвечать требованиям интерфейса, и, создавая потребители для ваших сервисов, вы должны ориентироваться на интерфейс, а не на реализацию, и требовать, чтобы реализация вам предоставлялась или внедрялась, так, чтобы вам не приходилось создавать экземпляры самостоятельно. При таком подходе можно будет не беспокоиться на уровне классов о том, как создаются зависимости и откуда они берутся; в данном случае важен только контракт.

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


Рассмотрим пример, в котором нам может пригодиться DI. Сначала давайте создадим интерфейс (контракт), который позволит нам выполнять некоторую задачу, например, логировать сообщение:

public interface ILogger {  
  void LogMessage(string message); 
}

Обратите внимание: в этом интерфейсе нигде не описано, как логируется сообщение и куда оно логируется; здесь просто передается намерение записать строку в некоторый репозиторий. Далее давайте создадим сущность, которая использует этот интерфейс. Допустим, мы создадим класс, который отслеживает определенный каталог на диске и, как только в каталог будет внесено изменение, логирует соответствующее сообщение:

public class DirectoryWatcher {  
 private ILogger _logger;
 private FileSystemWatcher _watcher;

 public DirectoryWatcher(ILogger logger) {
  _logger = logger;
  _watcher = new FileSystemWatcher(@ "C:Temp");
  _watcher.Changed += new FileSystemEventHandler(Directory_Changed);
 }

 void Directory_Changed(object sender, FileSystemEventArgs e) {
  _logger.LogMessage(e.FullPath + " was changed");
 }
}

В данном случае наиболее важно отметить, что нам предоставляется нужный нам конструктор, который реализует ILogger. Но, опять же, отметим: нас не волнует, куда идет лог, или как он создается. Мы можем просто программировать с учетом интерфейса и ни о чем больше не задумываться.

Таким образом, чтобы создать экземпляр нашего DirectoryWatcher, нам также нужна готовая реализация ILogger. Давайте продолжим и создадим экземпляр, логирующий сообщения в текстовый файл:

public class TextFileLogger: ILogger {  
 public void LogMessage(string message) {
  using(FileStream stream = new FileStream("log.txt", FileMode.Append)) {
   StreamWriter writer = new StreamWriter(stream);
   writer.WriteLine(message);
   writer.Flush();
  }
 }
}

Давайте создадим еще один, который записывает сообщения в лог событий Windows:

public class EventFileLogger: ILogger {  
 private string _sourceName;

 public EventFileLogger(string sourceName) {
  _sourceName = sourceName;
 }

 public void LogMessage(string message) {
  if (!EventLog.SourceExists(_sourceName)) {
   EventLog.CreateEventSource(_sourceName, "Application");
  }
  EventLog.WriteEntry(_sourceName, message);
 }
}

Теперь у нас две отдельные реализации, логирующие сообщения очень разными способами, но обе они реализуют ILogger, и это означает, что любая из них может использоваться там, где требуется экземпляр ILogger. Далее можно создать экземпляр DirectoryWatcher и указать ему использовать один из наших логеров:

ILogger logger = new TextFileLogger();  
DirectoryWatcher watcher = new DirectoryWatcher(logger);

Или же, просто изменив правую часть первой строки, мы можем воспользоваться другой реализацией:

ILogger logger = new EventFileLogger();  
DirectoryWatcher watcher = new DirectoryWatcher(logger);

Все это происходит без каких-либо изменений в реализации DirectoryWatcher, и это самое важное. Мы внедряем нашу реализацию логгера в потребитель, так, что потребителю не приходится создавать экземпляр самостоятельно. Показанный пример тривиален, но представьте, каково использовать такие приемы в крупномасштабном проекте, где у вас несколько зависимостей, и потребителей, которые их используют, во много раз больше. А затем вдруг поступает требование изменить метод, логирующий сообщения (скажем, теперь сообщения должны логироваться на SQL-сервер для целей аудита). Если у вас в той или иной форме не используется внедрение зависимостей, то вам придется внимательно просмотреть код и внести изменения везде, где фактически создается, а затем используется экземпляр логгера. В крупном проекте такая работа может быть обременительна и чревата ошибками. Применяя DI, вы просто измените зависимость в одном месте, а остальная часть приложения фактически поглотит изменения и сразу же начнет использовать новый метод логирования.

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

Контейнеры для внедрения зависимостей


Многие фреймворки для внедрения DI, которые можно просто скачать и использовать, идут на шаг дальше и задействуют контейнер для внедрения зависимостей. В сущности, это класс, который хранит отображения типов и возвращает зарегистрированную реализацию для данного типа. В нашем простом примере мы сможем запрашивать у контейнера экземпляр ILogger, и он вернет нам экземпляр TextFileLogger, или любой другой экземпляр, вместе с которым мы инициализировали контейнер.

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

Контейнер обычно создается таким образом, что мы можем получить доступ к 'разрешителю' (такой сущности, которая позволяет нам запрашивать экземпляры) из любой точки проекта.
Наконец, в профессиональных фреймворках обычно поддерживается феномен подзависимостей – в таком случае зависимость сама имеет одну или несколько зависимостей от других типов, также известных контейнеру. В данном случае разрешитель может выполнить и эти зависимости, выдав вам в ответ полную цепочку правильно созданных зависимостей, которые соответствуют отображениям ваших типов.

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

public class SimpleDIContainer {  
 Dictionary < Type, object > _map;
 public SimpleDIContainer() {
   _map = new Dictionary < Type, object > ();
  } 

/// <summary> 
/// отображает тип интерфейса на реализацию этого интерфейса, с опционально присутствующими аргументами. 
/// </summary> 
/// <typeparam name="TIn">The interface type</typeparam> 
/// <typeparam name="TOut">The implementation type</typeparam> 
/// <param name="args">Optional arguments for the creation of the implementation type.</param> 
 public void Map <TIn, TOut> (params object[] args) {
   if (!_map.ContainsKey(typeof(TIn))) {
    object instance = Activator.CreateInstance(typeof(TOut), args);
    _map[typeof(TIn)] = instance;
   }
  } 

/// <summary> 
/// получает сервис, реализующий T 
/// </summary> 
/// <typeparam name="T">The interface type</typeparam>
 public T GetService<T> () where T: class {
  if (_map.ContainsKey(typeof(T))) return _map[typeof(T)] as T;
  else throw new ApplicationException("The type " + typeof(T).FullName + " is not registered in the container");
 }
}

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

public class SimpleDIContainer {  
 Dictionary <Type, object> _map;
 public SimpleDIContainer() {
   _map = new Dictionary < Type, object > ();
  } 

 /// <summary> 
 /// отображает тип интерфейса на реализацию этого интерфейса, с опционально присутствующими аргументами. 
/// </summary> 
/// <typeparam name="TIn">The interface type</typeparam> 
/// <typeparam name="TOut">The implementation type</typeparam> 
/// <param name="args">Optional arguments for the creation of the implementation type.</param> 
public void Map <TIn, TOut> (params object[] args) {  
   if (!_map.ContainsKey(typeof(TIn))) {
    object instance = Activator.CreateInstance(typeof(TOut), args);
    _map[typeof(TIn)] = instance;
   }
  } 

/// <summary> 
/// получает сервис, реализующий T 
/// </summary> 
/// <typeparam name="T">The interface type</typeparam>
 public T GetService <T> () where T: class {
  if (_map.ContainsKey(typeof(T))) return _map[typeof(T)] as T;
  else throw new ApplicationException("The type " + typeof(T).FullName + " is not registered in the container");
 }
}

Рекомендую придерживаться этого паттерна, когда будете добавлять новые зависимости в свой проект. По мере того, как ваш проект будет увеличиваться в размерах, вы сами убедитесь, насколько просто управлять слабосвязанными компонентами. Приобретается значительная гибкость, а сам проект, в конечном итоге, гораздо проще поддерживать, видоизменять и адаптировать к новым условиям.
Tags:
Hubs:
Total votes 6: ↑6 and ↓0+6
Comments16

Articles

Information

Website
piter.com
Registered
Founded
Employees
201–500 employees
Location
Россия