Перевод статьи подготовлен в преддверии старта курса «Разработчик C++».




Давным-давно, когда я только начинал вести блог на LosTechies, я написал несколько статей о инверсии зависимостей (Dependency Inversion — DI), внедрении (или инъекции) зависимостей (Dependency Injection — также DI) и, конечно, о том, как я наконец начал понимать, в чем суть внедрения зависимостей. В то время я полагал, что оба DI были просто вариациями названия одной техники — называли ли вы это «инъекция» или «инверсия».

Инъекция != Инверсия


Где-то через год после этих двух публикаций я написал статью для Code Magazine о принципах разработки программного обеспечения SOLID. В процессе написания я попросил моего тогдашнего сотрудника Дерека Грира побыть рецензентом этой статьи. Это оказался самый лучшим поступок, который я когда-либо совершал для моего понимания SOLID, потому что Дерек был достаточно любезен (и терпелив по отношению к моему упрямству), чтобы показать мне, где мое понимание инверсии зависимостей отличалось от правильного. Он нашел время, чтобы провести меня через оригинальные статьи Дяди Боба, объяснить, где я смешивал внедрение зависимостей с инверсией, и наконец разъяснил для меня эту тему. В результате у меня получилась отличная статья, которая до сих пор достаточно популярна — и я очень благодарен Дереку за помощь в исправлении моего понимания.

Впрочем, я и по сей день продолжаю видеть, как другие люди совершают ту же ошибку — даже в ruby. Поэтому, чтобы помочь другим понять, чем является инверсия зависимостей, и чем она не является, я повторно публикую здесь раздел про нее из моей статьи про SOLID. Я понимаю, что часть текста не будет иметь смысла вне контекста остальных частей статьи. Однако общий принцип должен быть очевиден, а вы имеете возможность в любой момент перейти по ссылке на статью, чтобы прояснить интересующие вас моменты.

(Юридическая формальность: следующий текст первоначально появился в выпуске журнала CODE Magazine за январь/февраль 2010 года и воспроизводится здесь с их разрешения)

Принцип инверсии зависимостей


Принцип инверсии зависимостей (Dependency Inversion Principle — DIP) состоит из двух частей:

  • Модули верхних уровней не должны зависеть от модулей низших уровней. И те и другие должны зависеть от абстракций.
  • Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Освежите в памяти, как вы в последний раз хотели включить лампу, чтобы осветить часть комнаты. Нужно ли было вырезать дыру в стене, копаться в поисках электропроводки, оголять ее и впаивать лампу прямо в проводку дома? Конечно же нет (по крайней мере, я надеюсь, что нет!). Электрическая розетка обеспечивает стандартный интерфейс для такого случая. Никто в большинстве промышленно развитых стран не ожидает от обычной лампы необходимости впаивать ее непосредственно в электропроводку здания. Кроме того, никто не ожидает, что сможет вставлять в розетку одну только лампу. Мы привыкли подключать лампы, компьютеры, телевизоры, пылесосы и другие устройства. Стандартная электрическая розетка на 120 вольт, 60 герц, стала повсеместной частью общества в Соединенных Штатах.

Тот же принцип применим и в разработке программного обеспечения. Вместо того чтобы работать с набором классов, которые жестко связаны между собой, вы бы хотели работать со стандартным интерфейсом. Более того, вы бы хотели гарантировать, что сможете заменить реализацию, не нарушая ожидания этого интерфейса, согласно LSP (Liskov Substitution Principle — Принцип подстановки Лисков). Таким образом, если вы работаете с интерфейсом и хотите иметь возможность заменять его, вам нужно следить за тем, чтобы вы работали только с интерфейсом, а не с конкретной реализацией. То есть код, который полагается на интерфейс, должен знать подробности только об этом интерфейсе. Он не должен знать ни о каких конкретных классах, которые реализуют этот интерфейс.

Взаимоотношения политик, деталей и абстракций


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

В качестве примера того, почему зависимость детали от политики является инверсией зависимости, рассмотрим код, который мы написали в FormatReaderService. Служба чтения форматированных файлов — это политика. Она определяет, что интерфейс IFileFormatReader должен делать — ожидаемое поведение этих методов. Это позволяет вам сосредоточиться на самой политике, определяя, как работает служба чтения, без учета реализации деталей — читателей отдельных форматов. Таким образом, читатели зависят от абстракции, предоставляемой службой чтения. В конце концов, и служба и отдельные читатели зависят от абстракции интерфейса читателя формата.

Уменьшение связанности путем инвертирования зависимостей


Вы знаете, что не иметь зависимостей (иметь нулевую связанность) для класса нецелесообразно. У вас не было бы пригодного для использования набора классов, если бы у вас была нулевая связанность. Тем не менее, вы также хотели бы по возможности избавляться от прямой связанности. Вы хотели бы разделить вашу систему настолько, чтобы это позволило менять ее отдельные части, не изменяя ничего, кроме этих самых частей. Принцип инверсии зависимости является ключом к этому замыслу. Опираясь только на абстракцию, такую ??как интерфейс или базовый класс, вы можете уменьшать связанность между различными частями вашей системы. Это позволяет перекомпоновывать систему различными реализациями.

Рассмотрим множество классов, которые необходимо инстанцировать в четкой иерархии, чтобы вы могли получить некоторую, нужную вам функциональность. Проще всего с помощью класса самого высокого уровня (того, который вы хотите вызывать) создавать экземпляр класса на один уровень ниже, с его же помощью инстанцировать класс еще ниже по уровню, и так далее. На рисунке 14 представлен граф объектов, в котором объект более высокого уровня (политика) зависит и напрямую связан с объектом более низкого уровня (деталью).


Рисунок 14: Политика связанная с деталью.

Это реализует необходимую иерархию, но напрямую связывает классы. Вы не сможете использовать Foo, не таская за собой Bar. Если вы хотите разделить эти классы, вы можете легко ввести интерфейс, который будет реализовывать Bar, и от которого будет зависеть Foo. На рисунке 15 показан простой интерфейс IBar, который вы можете создать на основе public API класса Bar.


Рисунок 15: Разделение с помощью абстракции.

В этом сценарии вы можете отделить реализацию Bar от использования ее в Foo, введя интерфейс. Тем не менее, вы лишь отделили реализацию, выделив из нее интерфейс. Вы еще не инвертировали структуру зависимостей и не исправили все проблемы связанности в этой схеме.

Что же произойдет, когда в данном сценарии вы захотите изменить Bar? В зависимости от того, какие изменения вы хотите внести, вы можете спровоцировать цепную реакцию, которая заставит вас изменить интерфейс IBar. Foo зависит от интерфейса IBar, поэтому вы также должны изменить реализацию Foo. Возможно, вы отделили реализацию Bar, но оставили Foo зависимым от изменений в Bar. То есть Политика по-прежнему зависит от Детали.

Если вы хотите инвертировать структуру зависимостей, чтобы Деталь стала зависимой от Политики, то в первую очередь вы должны поменять свою перспективу. Разработчик, работающий с этой системой, должен понимать, что вы не только абстрагировали реализацию от интерфейса. Да, это разделение необходимо, но этого недостаточно. Вы должны понимать, кому принадлежит абстракция — Политике или Детали.

Принцип инверсии зависимостей гласит, что детали должны зависеть от политик. Это означает, что у вас должна быть политика, определяющая и владеющая абстракцией, которую реализует деталь. В сценарии Foo->IBar->Bar вам нужно рассматривать IBar как часть Foo, а не просто как обертку для Bar. Хоть структурно ничего и не поменялось, но перспектива владения изменилась, как показано на рисунке 16.


Рисунок 16: Политика владеет абстракцией. Деталь зависит от политики.

Если Foo владеет абстракцией IBar, вы можете поместить эти две конструкции в пакет, независимый от Bar. Вы можете поместить их в свое собственное пространство имен, в свою собственную сборку и т. д. Это может значительно увеличить наглядность того, какой класс или модуль зависит от какого. Если вы видите, что AssemblyA содержит Foo и IBar, а AssemblyB обеспечивает реализацию IBar, то вам легче заметить, что детализация Bar зависит от политики, определенной Foo.

Если вы правильно инвертировали структуру зависимостей, цепная реакция на изменения политики и/или деталей теперь также будет корректной. Когда вы меняете реализацию Bar, вы больше не наблюдаете восходящей цепочки изменений. Это связано с тем, что Bar требуется соответствие абстракции, предоставленной Foo — детали больше не диктуют изменения в политике. Затем, когда вы изменяете потребности Foo, вызывая изменения в интерфейсе IBar, ваши изменения распространятся по структуре. Bar (деталь) будет вынужден меняться в зависимости от изменения политики.

Разделение и инверсия зависимостей системы отправки электронной почты


Просматривая нашу кодовую базу, вы можете заметить, что IFileFormatReader уже является примером инверсии зависимости. Класс FormatReaderService владеет определением интерфейса читателя формата. Если потребности службы чтения форматированных файлов изменятся, вы, скорее всего, увидите цепочку изменений нисходящую к читателям форматов. Однако, если изменится читатель конкретного формата файлов, вы вряд ли увидите, что эти изменения отразятся на службе чтения форматированных файлов. Это заставляет задуматься, где еще вы можете инвертировать системные зависимости.

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

И с этим всем на уме, вы решаете создать объект с именем ProcessingService. После нескольких минут тасования кода туда-сюда, чтобы попытаться консолидировать процесс, вы понимаете, что не хотите, чтобы служба обработки была связана напрямую со службами чтения базы данных или чтения файлов. После небольшого размышления вы замечаете между ними паттерн: метод «GetMessageBody». Используя этот метод в качестве основы, вы создаете новый интерфейс IMessageInfoRetriever и реализует его как службой чтения базы данных, так и службой чтения файлов.

public interface IMessageInfoRetriever
{ 
  string GetMessageBody(); 
}

public class FileReaderService: IMessageInfoRetriever
{
  public string GetMessageBody() {/* ... */}
}

public class DatabaseReaderService: IMessageInfoRetriever
{
  public string GetMessageBody() {/* ... */}
}

Этот интерфейс позволяет вам предоставить любую реализацию для служб обработки, которая вам понадобится. Затем вы обращаете внимание на службу электронной почты, которая в настоящее время напрямую связана со службой обработки. Простой интерфейс IEmailService решает эту проблему. На рисунке 17 показана полученная структура.


Рисунок 17: Инвертирование зависимостей службы обработки и службы чтения файлов.

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



Узнать о курсе подробнее.