Главная страница   /   1.1. Написание поддерживаемого кода (Внедрение зависимостей в .NET

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

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

Марк Симан

1.1. Написание поддерживаемого кода

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

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

Одним из множества способов создания поддерживаемого кода является использование слабого связывания. Еще в 1995 году, когда "Банда четырех" (Эрих Гамма, Ричард Хелм, Ральф Джонсон, Джон Влиссидс) написала книгу "Паттерны проектирования" ("Design Patterns"), существовало универсальное знание:

Программируй, основываясь на интерфейсе, а не на классах.

Этот совет является не заключением, а скорее остроумной предпосылкой книги "Паттерны проектирования": он появляется на странице 18. Слабое связывание делает код расширяемым, а расширяемость делает его поддерживаемым.

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

Какие знания о DI нужно забыть

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

Существует, по крайней мере, четыре общих мифа, касающихся механизма внедрения зависимостей:

  • DI имеет отношение только к "позднему связыванию" (late binding).
  • DI имеет отношение только к модульному тестированию (unit testing).
  • DI – это вид абстрактной фабрики (Abstract Factory) на "стероидах".
  • Для механизма DI необходим DI-контейнер.

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

Позднее связывание

В данном контексте под поздним связыванием понимается возможность заменять части приложения без необходимости перекомпиляции кода. Приложение, которое разрешает добавление-вставку сторонних компонентов (таких, как Visual Studio), является одним из примеров позднего связывания.

Еще один пример – стандартное программное обеспечение, которое поддерживает различные исполняемые среды. У вас может быть приложение, которое может запускаться больше, чем на одном движке базы данных: например, такое, которое поддерживает как Oracle, так и SQL Server. Для поддержки такой возможности остальная часть приложения может обращаться к базе данных посредством интерфейса. База кода может предоставлять различные реализации этого интерфейса с целью обеспечения доступа к Oracle и SQL Server соответственно. Для контроля над тем, какая реализация должна применяться для данной инсталляции, может использоваться опция конфигурации.

Тот факт, что механизм внедрения зависимостей имеет отношение только к такому виду сценариев, является повсеместным заблуждением. Это понятно, поскольку DI разрешает такой сценарий, но ошибочно думать, что зависимость симметрична. То, что DI разрешает позднее связывание, еще не означает, что он имеет отношение только к сценариям позднего связывания. Как это продемонстрировано на рисунке 1-2, позднее связывание является всего лишь одним из аспектов механизма внедрения зависимостей.

Рисунок 1-2: Позднее связывание разрешено механизмом внедрения зависимостей, но полагать, что DI применим только в сценариях позднего связывания значит принимать ограниченное представление более широкой перспективы.

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

Модульное тестирование

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

По правде говоря, мое первоначальное знакомство с механизмом DI произошло во время борьбы с определенными аспектами технологии разработки через тестирование (Test-Driven Development или TDD). Во время этой борьбы я познакомился с DI и узнал, что остальные использовали этот механизм для того, чтобы поддерживать некоторые сценарии, похожие на те, к которым я обращался.

Даже если вы не пишите модульные тесты (если вы этого не делаете, то вам следует начать прямо сейчас), механизм DI все еще уместен, благодаря всем остальным преимуществам, которые он предлагает. Утверждение о том, что механизм DI имеет отношение только к поддержке модульного тестирования, подобно утверждению о том, что DI имеет отношение только к поддержке позднего связывания. Рисунок 1-3 демонстрирует, что, несмотря на то, что это уже другое представление, это представление такое же ограниченное, как и изображенное на рисунке 1-2. В данной книге я сделал все возможное, чтобы продемонстрировать вам общую картину.

Рисунок 1-3: Хотя предположение о том, что модульное тестирование является единственной целью механизма DI, – это уже другое представление, нежели то, которое касалось позднего связывания, оно все равно является ограниченным представлением более широкой перспективы.

Если вы думали, что механизм внедрения зависимостей имеет отношение только к модульному тестированию – забудьте об этом. Механизм DI делает намного больше, чем просто разрешает модульное тестирование.

Абстрактная фабрика "на стероидах"

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

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

Какими были ваши первоначальные соображения по поводу этого предложения? Думали ли вы об инфраструктуре, как о некоторого рода сервисе, к которому вы могли бы обратиться, чтобы получить необходимую зависимость? Если это так, то вы не одиноки в своих мыслях. Многие разработчики и архитекторы думают о механизме внедрения зависимостей как о сервисе, который может использоваться для указания местоположения других сервисов; этот сервис имеет название Service Locator (сервис-локатор), но он является полной противоположностью DI.

Если вы думали о механизме DI, как о сервис-локаторе, – а именно, универсальной фабрике – вам нужно об этом забыть. Механизм внедрения зависимостей является противоположностью Service Locator; это способ структурирования кода таким образом, чтобы нам никогда не нужно было обязательно запрашивать зависимости. В противном случае мы принуждаем пользователей возмещать их.

DI-контейнеры

Тесно связано с предыдущим заблуждением мнение о том, что для механизма DI необходим DI-контейнер. Если бы вы придерживались предыдущего, ошибочного мнения о том, что DI включает в себя Service Locator, то было бы легко прийти к выводу о том, что DI-контейнер может взять на себя ответственность за Service Locator. Это имеет место быть, но это никак не является тем, как мы должны использовать DI-контейнер.

DI-контейнер – это необязательная библиотека, которая может упростить процесс создания компонентов при регистрации приложения, но это не обязательный способ. Когда мы создаем приложения без использования DI-контейнера, мы называем это Poor man's DI; для этого требуется больше работы, но несколько другого рода, при которой нам не нужно идти на компромисс при использовании какого-либо принципа механизма внедрения зависимостей.

Если вы думали, что для механизма DI нужен DI-контейнер, то забудьте об этом. Механизм внедрения зависимостей – это набор принципов и паттернов, а DI-контейнер – это полезный, но необязательный инструмент.

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

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

Давайте предположим, что вы ничего не знаете о механизме внедрения зависимостей и о его целях и начнем с того, что рассмотрим то, что механизм DI делает.

Осознание цели DI

Механизм DI – это не конечная цель, это средство достижения результата. Механизм внедрения зависимостей разрешает слабое связывание, а слабое связывание делает код более поддерживаемым. Это совершенное утверждение, и, несмотря на то, что я мог бы отослать вас за подробностями к таким прочно установившимся авторитетам, как "Банда четырех", я думаю, что справедливо будет объяснить вам, почему это именно так.

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

Проверка в дешевом отеле

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

Рисунок 1-4: В комнате дешевого отеля вы можете найти фен, подсоединенный к стенной розетке напрямую. Это эквивалентно использованию универсальной практики написания сильно связанного кода.

Что происходит, когда фены прекращают работать? Отелю приходится вызывать квалифицированного профессионала, который умеет решать эту проблему. Для ремонта жестко встроенного фена им придется отключить питание во всей комнате, делая ее при этом временно бесполезной. Затем специалист будет использовать специальные инструменты для того, чтобы тщательно отсоединить фен и заменить его на новый. Если вам повезет, то специалист не забудет снова включить питание в комнате и проверить, работает ли новый фен… Если вам повезет. Знакома ли вам вообще эта процедура?

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

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

Обычно мы не монтируем электрические устройства вместе, напрямую присоединяя кабель к стене. Вместо этого, как продемонстрировано на рисунке 1-5, мы используем вилки и розетки. Розетка определяет форму, к которой необходимо присоединить вилку. Если провести аналогию с проектированием программного обеспечения, то розетка – это интерфейс.

Рисунок 1-5: Посредством использования розеток и вилок фен можно слабо связать со стенной розеткой.

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

Во-первых, мы больше не стеснены рамками фенов. Если вы самый обычный читатель, то я бы предположил, что вам больше нужен компьютер, чем фен. Это не проблема: мы выдернем из розетки фен и подключим компьютер к той же розетке, как это показано на рисунке 1-6.

Рисунок 1-6: Используя розетки и вилки, мы можем заменить первоначально используемый фен из рисунка 1-5 на компьютер. Это соответствует принципу замещения Лисков.

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

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

Как это проиллюстрировано на рисунке 1-7, мы можем выдернуть шнур компьютера из розетки, если на данный момент нам не нужно его использовать. Даже если ничего не подключено к розетке, стена не взрывается.

Рисунок 1-7: Отключение компьютера не приводит ни к взрыву стены, ни к взрыву компьютера. Это можно приближенно сравнить с паттерном Null Object.

Если мы отсоединим компьютер от стены, то ни стена, ни компьютер не разрушатся (в действительности, если это портативная ЭВМ, то она может работать и на собственных батарейках в течение некоторого времени). Тем не менее, что касается программного обеспечения, клиент часто ожидает, что сервис будет доступен. Если сервис был удален, то мы получаем NullReferenceException. Для того чтобы справиться с этой ситуацией, мы можем создать реализацию интерфейса, которая ничего не делает. Это паттерн проектирования, известный как Null Object, и он приблизительно соответствует отсоединению компьютера от стены. Благодаря тому, что мы используем слабое связывание, мы можем заменить существующую реализацию чем-то таким, что ничего не выполняет и при этом не приводит к проблемам.

Существует множество других вещей, которые мы можем сделать. Если мы живем по соседству со скачкообразным отключением электричества, то мы можем захотеть, чтобы наш компьютер продолжал работать и после отключения питания, подключив его для этого к системе бесперебойного питания (Uninterrupted Power Supply), как это продемонстрировано на рисунке 1-8: мы подсоединяем систему бесперебойного питания к стенной розетке, а компьютер к этой системе.

Рисунок 1-8: Можно воспользоваться системой бесперебойного питания для того, чтобы компьютер продолжал работать при отключении электричества. Это соответствует паттерну проектирования Decorator.

Компьютер и система бесперебойного питания служат разным целям. Каждый из них обладает самостоятельной ответственностью, на которую не может посягнуть другое устройство. Скорее всего, система бесперебойного питания и компьютер производятся двумя разными производителями, покупаются в разное время и подключаются в разное время. Как демонстрирует рисунок 1-6, мы можем запустить компьютер без системы бесперебойного питания (СБП), но мы также могли бы, возможно, использовать фен во время отключения электричества путем подключения его к СБП.

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

Еще один способ добавления новой функциональности в существующий код – комбинировать существующую реализацию интерфейса с новой реализацией. Когда мы соединяем несколько реализаций в одну, мы используем паттерн проектирования Composite. Рисунок 1-9 иллюстрирует то, как это соответствует подключению различных устройств к удлинителю.

Рисунок 1-9: Удлинитель дает возможность подключать несколько устройств к одной стенной розетке. Это соответствует паттерну проектирования Composite.

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

Ниже приведен окончательный пример. Мы иногда обнаруживаем, что вилка не подходит к определенной розетке. Если вы путешествовали в другую страну, то вы, скорее всего, замечали, что розетки во всем мире отличаются друг от друга. Если вы во время путешествия возите с собой что-то, например фотоаппарат, как показано на рисунке 1-10, то вам нужен адаптер для того, чтобы заряжать его. Соответственно, это паттерн проектирования с таким же именем – Adapter.

Рисунок 1-10: Во время путешествия нам часто нужно использовать адаптер для того, чтобы подключить устройство к иностранной розетке (например, чтобы перезарядить фотоаппарат). Это соответствует паттерну проектирования Adapter.

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

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

Слабое связывание может сделать код более поддерживаемым.

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

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

IMessageWriter writer = new IMessageWriter()

IMessageWriter: Программирование на основании интерфейсов

new IMessageWriter(): Не компилируется

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

С таким представлением цели механизма DI, думаю, вы готовы к примеру.