Что такое зависимости в java
Краткое введение во внедрение зависимостей: что это и когда это необходимо использовать
Nov 4, 2018 · 4 min read
Введение
В разработке программного обеспечения, внедрение зависимостей это такая техника, где посредством одного объекта (или статического метода) предоставляются зависимости другого объекта. Зависимость — это объект, который может быть использован (как сервис).
Это определение из Википедии (в версии на английском языке — прим. переводчика), и все же, его не так и просто понять. Давайте разберемся с этим получше.
Перед тем как понять, что же это означает в программировании, давайте сначала разберемся, что это означает в общем смысле. Таким образом это поможет нам лучше разобраться с этим понятием.
Зависимость или зависимое — означает полагаться на что-то. Это все равно что, если сказать, что мы слишком много полагаемся на мобильные телефоны — это означает, что мы зависим от них.
Поэтому, прежде чем перейти к понятию внедрение зависимостей, сначала давайте разберемся, что означает зависимость в программировании.
Когда класс A использует некоторую функциональность из класса B, тогда говорят, что класс A зависим от класса B.
В Java, прежде чем мы сможем использовать методы других классов, нам необходимо для начала создать экземпляры этого класса (то есть класс А должен создать экземпляр класса В).
Таким образом, передавая задачу создания объекта чему-то другому и прямое использование этой зависимости называется внедрением зависимостей.
Так почему следует использовать внедрение зависимостей?
Представьте, что у нас есть класс для описания автомобилей, также содержащий другие различные объекты, например, колеса, двигатели и прочее.
Перед вами класс Car, отвечающий за создание всех объектов зависимостей. Теперь, что если мы решим избавиться колес компании MRFWheels и хотим использовать колеса от Yokohama в будущем?
Нам нужно будет воссоздать объект класса Car с новой зависимостью от Yokohama. Но при использовании внедрении зависимостей мы можем изменить колеса во время выполнения программы (потому что зависимости можно внедрять во время выполнения, а не во время компиляции).
Вы можете думать о внедрении зависимостей как о посреднике в нашем коде, который выполняет всю работу по созданию предпочтительного объекта колеса и предоставлению его классу Автомобиль.
Это делает наш класс автомобилей независимым от создания объектов таких как колеса, аккумулятор и т.д.
Существует три основных типа внедрения зависимостей:
Внедрение зависимостей ответственно за:
Если есть какие-либо изменения в объектах, то DI смотрит на него, и он не должен относиться к классу с использованием этих объектов.
Таким образом, если объекты будут меняться в будущем, тогда ответственность DI заключается в предоставлении соответствующих объектов классу.
Инверсия управления — концепция, лежащая в основе внедрения зависимости
Это означает, что класс не должен конфигурировать свои зависимости статистически, а должен быть сконфигурирован другим классом извне.
Это пятый принцип S.O.L.I.D из пяти основных принципов объектно-ориентированного программирования и разработки от дяди Боба, в котором говорится, что класс должен зависеть от абстракции, а не от чего-то конкретного (простыми словами, жестко закодированного).
Согласно принципам, класс должен полностью сосредоточиться на выполнении своих обязанностей, а не на создании объектов, необходимых для выполнения этих обязанностей. И именно здесь начинается внедрение зависимостей: она предоставляет классу требуемые объекты.
Преимущества использования внедрения зависимостей
Недостатки использования внедрения зависимостей
Тем не менее вы вполне можете реализовать внедрение зависимостей самостоятельно без использования сторонних библиотек и фреймворков или используя их.
Библиотеки и фреймворки, реализующие внедрение зависимостей
Для того чтобы узнать больше о внедрении зависимостей, вы можете ознакомиться со списком дополнительных источников ниже:
Основы внедрения зависимостей
В этой статье я расскажу об основах внедрения зависимостей (англ. Dependency Injection, DI) простым языком, а также расскажу о причинах использования этого подхода. Эта статья предназначена для тех, кто не знает, что такое внедрение зависимостей, или сомневается в необходимости использования этого приёма. Итак, начнём.
Что такое зависимость?
Прежде чем продолжить, я хочу уточнить, что такая взаимосвязь — это хорошо, ведь нам не нужно, чтобы один класс выполнял всю работу в приложении. Нам необходимо разделять логику на разные классы, каждый из которых будет отвечать за определенную функцию. И в таком случае классы смогут эффективно взаимодействовать.
Как работать с зависимостями?
Давайте рассмотрим три способа, которые используются для выполнения задач по внедрению зависимостей:
Первый способ: создавать зависимости в зависимом классе
Проще говоря, мы можем создавать объекты всякий раз, когда они нам нужны. Посмотрите на следующий пример:
Это очень просто! Мы создаем класс, когда нам это необходимо.
Преимущества
Недостатки
Каждый класс должен выполнять лишь свою работу.
Поэтому мы не хотим, чтобы классы отвечали за что-либо, кроме своих собственных задач. Внедрение зависимостей при этом является дополнительной задачей, которую мы ставим перед ними.
Второй способ: внедрять зависимости через пользовательский класс
Итак, понимая, что внедрение зависимостей внутри зависимого класса — не самая лучшая идея, давайте изучим альтернативный способ. Здесь зависимый класс определяет все необходимые ему зависимости внутри конструктора и позволяет пользовательскому классу предоставлять их. Является ли такой способ решением нашей проблемы? Узнаем немного позже.
Посмотрите на пример кода ниже:
Преимущества
Недостатки
Второй способ очевидно работает лучше первого, но у него всё ещё есть свои недостатки. Возможно ли найти более подходящее решение? Прежде чем рассмотреть третий способ, давайте сначала поговорим о самом понятии внедрения зависимостей.
Что такое внедрение зависимостей?
Внедрение зависимостей — это способ обработки зависимостей вне зависимого класса, когда зависимому классу не нужно ничего делать.
Исходя из этого определения, наше первое решение явно не использует идею внедрения зависимостей, а второй способ заключается в том, что зависимый класс ничего не делает для предоставления зависимостей. Но мы все ещё считаем второе решение плохим. ПОЧЕМУ?!
Поскольку определение внедрения зависимости ничего не говорит о том, где должна происходить работа с зависимостями (кроме как вне зависимого класса), разработчик должен выбрать подходящее место для внедрения зависимостей. Как видно из второго примера, пользовательский класс является не совсем правильным местом.
Как же сделать лучше? Давайте рассмотрим третий способ обработки зависимостей.
Третий способ: пусть кто-нибудь ещё обрабатывает зависимости вместо нас
Согласно первому подходу зависимые классы отвечают за получение своих собственных зависимостей, а во втором подходе мы переместили обработку зависимостей из зависимого класса в пользовательский класс. Давайте представим, что существует кто-то другой, кто мог бы обрабатывать зависимости, вследствие чего ни зависимый, ни пользовательский классы не выполняли бы эту работу. Этот способ позволяет работать с зависимостями в приложении напрямую.
«Чистая» реализация внедрения зависимостей (по моему личному мнению)
Ответственность за обработку зависимостей возлагается на третью сторону, поэтому ни одна часть приложения не будет с ними взаимодействовать.
Внедрение зависимостей — это не технология, фреймворк, библиотека или что-то подобное. Это просто идея. Идея работать с зависимостями вне зависимого класса (желательно в специально выделенной части). Вы можете применять данную идею, не используя какие-либо библиотеки или фреймворки. Тем не менее, мы обычно обращаемся к фреймворкам для внедрения зависимостей, потому что это упрощает работу и позволяет избежать написания шаблонного кода.
Любой фреймворк внедрения зависимостей имеет две неотъемлемые характеристики. Вам могут быть доступны и другие дополнительные функции, но эти две функции будут присутствовать всегда:
Во-вторых, фреймворки позволяют определить, как нужно предоставить каждую зависимость, и это происходит в отдельном файле (файлах). Приблизительно это выглядит так (учитывайте, что это лишь пример, и он может отличаться от фреймворка к фреймворку):
Преимущества
Обратите внимание, никакой код внутри приложения не меняется, только метод провайдера. Кажется, что ничего не может быть ещё проще и гибче.
Недостатки
Заключение
В этой статье я попытался объяснить основы работы с понятием внедрения зависимостей, а также перечислил причины необходимости использования этой идеи. Существует ещё множество ресурсов, которые вы можете изучить, чтобы больше узнать о применении DI в ваших собственных приложениях. Например, этой теме посвящён отдельный раздел в продвинутой части нашего курса Android-профессии.
Dependency injection
От переводчика
Представляемый вашему вниманию перевод открывает серию статей от Jakob Jenkov, посвященных внедрению зависимостей, или DI. Примечательна серия тем, что в ней автор, анализируя понятия и практическое применение таких понятий как «зависимость», «внедрение зависимостей», «контейнер для внедрения зависимостей», сравнивая паттерны создания объектов, анализируя недостатки конкретных реализаций DI-контейнеров (например, Spring), рассказывает, как пришел к написанию собственного DI-контейнера. Таким образом, читателю предлагается познакомиться с довольно цельным взглядом на вопрос управления зависимостями в приложениях.
В данной статье сравнивается подход к настройке объектов изнутри и извне (DI). По смыслу настоящая статья продолжает статью Jakob Jenkov Understanding Dependencies, в которой дается определение самому понятию «зависимости» и их типам.
Серия включает в себя следующие статьи
Внедрение зависимостей
«Внедрение зависимостей» — это выражение, впервые использованное в статье Мартина Фаулера Inversion of Control Containers and the Dependency Injection Pattern. Это хорошая статья, но она упускает из виду некоторые преимущества контейнеров внедрения зависимостей. Также я не согласен с выводами статьи, но об этом — в следующих текстах.
Объяснение внедрения зависимостей
Внедрение зависимостей — это стиль настройки объекта, при котором поля объекта задаются внешней сущностью. Другими словами, объекты настраиваются внешними объектами. DI — это альтернатива самонастройке объектов. Это может выглядеть несколько абстрактно, так что посмотрим пример:
UPD: после обсуждения представленных автором фрагментов кода с flatscode и fogone, я принял решение скорректировать спорные моменты в коде. Изначальный замысел был в том, чтобы не трогать код и давать его таким, каков он написан автором. Оригинальный авторский код в спорных местах закомментирован с указанием «в оригинале», ниже дается его исправленная версия. Также оригинальный код можно найти по ссылке в начале статьи.
Этот DAO (Data Access Object), MyDao нуждается в экземпляре javax.sql.DataSource для того, чтобы получить подключения к базе данных. Подключения к БД используются для чтения и записи в БД, например, объектов Person.
Заметьте, что класс MyDao создает экземпляр DataSourceImpl, так как нуждается в источнике данных. Тот факт, что MyDao нуждается в реализации DataSource, означает, что он зависит от него. Он не может выполнить свою работу без реализации DataSource. Следовательно, MyDao имеет «зависимость» от интерфейса DataSource и от какой-то его реализации.
Класс MyDao создает экземпляр DataSourceImpl как реализацию DataSource. Следовательно, класс MyDao сам «разрешает свои зависимости». Когда класс разрешает собственные зависимости, он автоматически также зависит от классов, для которых он разрешает зависимости. В данном случае MyDao завсист также от DataSourceImpl и от четырех жестко заданных строковых значений, передаваемых в конструктор DataSourceImpl. Вы не можете ни использовать другие значения для этих четырех строк, ни использовать другую реализацию интерфейса DataSource без изменения кода.
Как вы можете видеть, в том случае, когда класс разрешает собственные зависимости, он становится негибким в отношении к этим зависимостям. Это плохо. Это значит, что если вам нужно поменять зависимости, вам нужно поменять код. В данном примере это означает, что если вам нужно использовать другую базу данных, вам потребуется поменять класс MyDao. Если у вас много DAO-классов, реализованных таким образом, вам придется изменять их все. В добавок, вы не можете провести юнит-тестирование MyDao, замокав реализацию DataSource. Вы можете использовать только DataSourceImpl. Не требуется много ума, чтобы понять, что это плохая идея.
Давайте немного поменяем дизайн:
Заметьте, что создание экземпляра DataSourceImpl перемещено в конструктор. Конструктор принимает четыре параметра, это — четыре значения, необходимые для DataSourceImpl. Хотя класс MyDao все еще зависит от этих четырех значений, он больше не разрешает зависимости сам. Они предоставляются классом, создающим экземпляр MyDao. Зависимости «внедряются» в конструктор MyDao. Отсюда и термин «внедрение (прим. перев.: или иначе — инъекция) зависимостей». Теперь возможно сменить драйвер БД, URL, имя пользователя или пароль, используемый классом MyDao без его изменения.
Внедрение зависимостей не ограничено конструкторами. Можно внедрять зависимости также используя методы-сеттеры, либо прямо через публичные поля (прим. перев.: по поводу полей переводчик не согласен, это нарушает защиту данных класса).
Класс MyDao может быть более независимым. Сейчас он все еще зависит и от интерфейса DataSource, и от класса DataSourceImpl. Нет необходимости зависеть от чего-то, кроме интерфейса DataSource. Это может быть достигнуто инъекцией DataSource в конструктор вместо четырех параметров строкового типа. Вот как это выглядит:
Теперь класс MyDao больше не зависит от класса DataSourceImpl или от четырех строк, необходимых конструктору DataSourceImpl. Теперь можно использовать любую реализацию DataSource в конструкторе MyDao.
Цепное внедрение зависимостей
Пример из предыдущего раздела немного упрощен. Вы можете возразить, что зависимость теперь перемещена из класса MyDao к каждому клиенту, который использует класс MyDao. Клиентам теперь приходится знать о реализации DataSource, чтобы быть в состоянии поместить его в конструктор MyDao. Вот пример:
Как вы можете видеть, теперь MyBizComponent зависит от класса DataSourceImpl и четырех строк, необходимых его конструктору. Это еще хуже, чем зависимость MyDao от них, потому что MyBizComponent теперь зависит от классов и от информации, которую он сам даже не использует. Более того, реализация DataSourceImpl и параметры конструктора принадлежат к разным слоям абстракции. Слой ниже MyBizComponent — это слой DAO.
Решение — продолжить внедрение зависимости по всем слоям. MyBizComponent должен зависеть только от экземпляра MyDao. Вот как это выглядит:
Снова зависимость, MyDao, предоставляется через конструктор. Теперь MyBizComponent зависит только от класса MyDao. Если бы MyDao был интерфейсом, можно было бы менять реализацию без ведома MyBizComponent.
Такой паттерн внедрения зависимости должен продолжается через все слои приложения, с самого нижнего слоя (слоя доступа к данным) до пользовательского интерфейса (если он есть).
Внедрение зависимостей DI в Java Spring
Что такое внедрение зависимостей
Необходимость внедрения зависимостей
Spring IOC разрешает такие зависимости с помощью внедрения зависимостей, что упрощает тестирование и повторное использование кода. Слабая связь между классами может быть возможна путем определения интерфейсов для общих функций, и инжектор создаст экземпляры объектов требуемой реализации. Задача создания экземпляров объектов выполняется контейнером в соответствии с конфигурациями, указанными разработчиком.
Типы внедрения зависимостей Spring
Существует два типа внедрения зависимостей Spring.
в файле bean-config.
Пример: Допустим, есть класс GFG, который использует SDI и задает свойства гиков. Код для него приведен ниже.
Пример: возьмем тот же пример, что и SDI.
Внедрение зависимостей установщика (SDI) против внедрения зависимостей конструктора (CDI)
Пример Spring DI
Мы использовали три класса и интерфейс в качестве компонентов, чтобы проиллюстрировать концепции CDI и SDI. Это классы Vehicle, ToyotaEngine, Tires и интерфейс IEngine соответственно.
В нашем примере мы видим, что класс Vehicle зависит от реализации Engine, который является интерфейсом. (Таким образом, в основном производителю транспортных средств требуется стандартный двигатель, соответствующий индийским нормам выбросов.) Класс ToyotaEngine реализует интерфейс, и его ссылка предоставляется в файле конфигурации bean-компонента, сопоставленном с одним из свойств класса транспортного средства.
Компонент InjectwithConstructor использует аргумент-конструктор элемента с именем атрибута и ссылкой. Атрибут Name коррелирует с именем аргумента конструктора, указанным в определении класса Vehicle. Атрибут ref указывает на ссылку bean-компонента, которую можно использовать для инъекции.
InjectwithSetter использует элемент свойства, чтобы указать «имя» свойства и «значение» свойства. Вместо значения атрибут ref может использоваться для обозначения ссылки на компонент.
pom.xml
Enigne.java
ToyotaEngine.java
Tyres.java
Vehicle.java
springContext.xml
Процесс создания экземпляра bean-компонента и внедрения зависимостей показан на рисунке ниже:
Резюме
Как обсуждалось выше, избегайте использования инъекции полей, так как это только обеспечивает лучшую читаемость, несмотря на многие недостатки. Инъекции сеттеров и конструкторов имеют свои плюсы и минусы, как обсуждалось выше. Поэтому мы должны использовать комбинацию того и другого, что также предлагается самим сообществом Spring.
Understanding Dependencies
От переводчика
Мы — внедрители. Мы должны внедрять, а не фантазировать!
(Рина Зеленая, к/ф «Девушка без адреса»)
К переводу этой статьи меня побудили две причины: 1) желание лучше разобраться с фреймворком Spring, 2) небольшое количество источников по теме на русском языке.
Краеугольный камень ООП — «внедрение зависимостей». Если описание процесса «внедрения» в целом, удовлетворительно, то объяснение понятия «зависимость» обычно оставляют за скобками. На мой взгляд, это существенное упущение.
Чтобы не фантазировать, а внедрять, нужно сначала разобраться с тем, что мы внедряем. И в этом нам может помочь лаконичная статья Jakob Jenkov «Understanding Dependencies». Она будет полезна не только тем, кто пишет на Java, но и тем, кто пишет на других языках и следит за качеством проектирования приложений.
UPD: Я перевел еще одну статью Jakob Jenkov о зависимостях. Читайте на Хабре перевод статьи Dependency Injection, которая открывает одноименную серию статей и по смыслу продолжает данную статью. В статьях серии рассматриваются такие понятия как Dependency, Dependency Injection (DI), DI-контейнеры.
Понимая зависимости
Что такое зависимость?
Когда класс А использует класс или интерфейс B, тогда А зависит от B. А не может выполнить свою работу без B, и А не может быть переиспользован без переиспользования B. В таком случае класс А называют «зависимым», а класс или интерфейс B называют «зависимостью».
Два класса, которые используют друг друга, называют связанными. Связанность между классами может быть или слабой, или сильной, или чем-то средним. Степень связности не бинарна и не дискретна, она находится в континууме. Сильная связанность ведет к сильным зависимостям, и слабая связность ведет к слабым зависимостям или даже к отсутствию зависимостей в некоторых ситуациях.
Зависимости, или связи имеют направленность. То, что A зависит от B не значит, что B зависит от A.
Почему зависимости это плохо?
Зависимости плохи тем, что снижают переиспользование. Снижение переиспользования плохо по многим причинам. Обычно переиспользование оказывает позитивное влияние на скорость разработки, качество кода, читаемость кода и т.д.
Как зависимости могут навредить, наиболее хорошо показывает пример: представьте, что у Вас есть класс CalendarReader, который может читать события календаря из XML-файла. Реализация CalendarReader приведена ниже:
Метод readCalendarEvents получает объект типа File в качестве параметра. Поэтому, этот метод зависит от класса File. Зависимость от класса File означает, что CalendarReader способен на чтение событий календаря только из локальных файлов в файловой системе. Он не может читать события календаря из сетевого соединения, базы данных, или из ресурсов по classpath. Можно сказать, что CalendarReader тесно связан c классом File и локальной файловой системой.
Менее связанной реализацией будет замена параметра типа File параметром типа InputStream, как в коде ниже:
Как Вы можете знать, InputStream может быть получен из объекта типа File, из сетевого Socket, класса URLConnection, объекта Class (Class.getResourceAsStream(String name)), колонки из БД через JDBC и т.п. Теперь CalendarReader больше не завязан на локальную файловую систему. Он может читать файлы событий календаря из многих источников.
С версией метода readCalendarEvents(), использующей InputStream, класс CalendarReader повысил возможности переиспользования. Тесная привязка к локальной файловой системе была удалена. Вместо этого, она была заменена на зависимость от класса InputStream. Зависимость от InputStream более гибка, чем зависимость от класса File, но не означает, что CalendarReader на 100% может быть переиспользован. Он все еще не может читать данные из канала NIO, например.
Типы зависимостей
Зависимости — это не просто «зависимости». Есть несколько типов зависимостей. Каждый из них ведет к большей или меньшей гибкости в коде. Типы зависимостей:
Зависимости интерфейсов — это зависимости от интерфейсов. Например, метод в кодовой вставке ниже получает CharSequence в качестве параметра. CharSequence — стандартный интерфейс Java (в пакете java.lang). Классы CharBuffer, String, StringBuffer и StringBuilder реализуют интерфейс CharSequence, поэтому экземпляры только этих классов могут быть использованы в качестве параметров этого метода.
Зависимости методов или полей — это зависимости от конкретных методов или полей объекта. Не важно, каков класс объекта или какой интерфейс он реализует, пока он имеет метод или поле требуемого типа. Следующий пример иллюстрирует зависимость методов. Метод readFileContents зависит от метода, названного «getFileName» в классе объекта, переданного как параметр (fileNameContainer). Обратите внимание, что зависимость не видна из декларации метода!
Зависимости методов или переменных характерны для API, которые используют рефлексию. Например, Butterfly Persistence использует рефлексию для того, чтобы обнаружуить геттеры и сеттеры класса. Без геттеров и сеттеров Butterfly Persistence не может читать и записывать объекты класса из/в базу данных. Таким образом Butterfly Persistence зависит от геттеров и сеттеров. Hibernate (схожий ORM API) может как использовать геттеры и сеттеры, так и поля напрямую, так и через рефлексию. Таким образом, Hibernate также имеет зависимость либо от методов, либо от полей.
Зависимость методов или («функций») также может быть замечена в языках, поддерживающих указатели на функции или указатели на методы, которые должны быть переданы в качестве аргументов. Например, делегаты в C#.
Дополнительные характеристики зависимостей
Зависимости имеют и другие важные характеристики помимо типа. Зависимости могут быть зависимостями времени компиляции, времени исполнения, видимые, скрытые, прямые, непрямые, контекстуальные и т.п. Эти дополнительные характеристики будут раскрыты в следующих разделах.
Зависимости реализации интерфейса
Если класс A зависит от интерфейса I, тогда A не зависит от конкретной реализации I. Но A зависит от какой-то реализации I. A не может выполнять свою работу без некоторой реализации I. Таким образом, когда класс зависит от интерфейса, этот класс также зависит от реализации интерфейса.
Чем больше методов есть в интерфейсе, тем меньше шансов, что разработчики будут предоставлять собственные реализации, если у них этого не просят. Следовательно, чем больше методов есть в интерфейсе, тем больше возможность того, что разработчики «застрянут» на стандартной реализации этого интерфейса. Другими словами, чем более сложным и громоздким становится интерфейс, тем более тесно он связывается со своей дефолтной имплементацией.
Из-за зависимостей реализации интерфейса, Вы не должны добавлять функциональность в интерфейс слепо. Если функциональность может быть инкапсулирована в свой компонент, в свой отдельный интерфейс, нужно делать так.
Ниже — пример того, что это значит. Код примера показывает узел дерева для иерархической древовидной структуры.
Представьте, что Вы хотите иметь возможность подсчитать количество потомков конкретного узла. Сначала Вы можете поддаться искушению, и добавить метод countDescendents() в интерфейс ITreeNode. Тем не менее, если Вы так сделаете, каждый, кто захочет реализовать интерфейс ITreeNode, вынужден будет реализовывать и метод countDescendents().
Вместо этого Вы можете реализовать класс DescendentCounter, который может просматривать экземпляр ITreeNode и считать всех потомков этого экземпляра. DescendentCounter может быть переиспользован с другими реализациями интерфейса ITreeNode. Вы только что уберегли своих пользователей от проблемы реализации метода countDescendents(), даже если им нужнореализовать интерфейс ITreeNode!
Зависимости времени компиляции и времени исполнения
Зависимость, которая может быть разрешена во время компиляции, называется зависимостью времени компиляции. Зависимость, которая не может быть разрешена до начала исполнения — зависимость времени исполнения. Зависимости времени компиляции могут быть проще замечены, чем зависимости времени выполнения, однако, зависимости времени исполнения могут быть более гибкими. Например, Butterfly Persistence, находит геттеры и сеттеры класса во время исполнения и автоматически мапит их с таблицами БД. Это очень простой способ сопоставлять классы с таблицами БД. Тем не менее, чтобы делать это, Butterfly Persistence зависит от правильно названных геттеров и сеттеров.
Видимые и скрытые зависимости
Видимые зависимости — это зависимости, которые разработчики могут видеть из интерфейса класса. Если зависимости не могут быть обнаружены в интерфейсе класса, это — скрытые зависимости.
В примере, приведенном ранее, зависимости String и CharSequence метода readFileContents() — видимые зависимости. Они видимы в декларации метода, который является частью интерфейса класса. Зависимости метода readFileContents(), который получает Object в качестве параметра, невидимы. Вы не можете видеть из интерфейса, что метод readFileContents() вызывает fileNameContainer.toString(), чтобы получить имя файла, или как на самом деле происходит, вызывает метод getFileName().
Другой пример скрытой зависимости — зависимость от статического синглтона или статических методов внутри метода. Вы не можете видеть из интерфейса, что класс зависит от статического метода или статического синглтона.
Как вы можете представить, скрытые зависимости могут быть злом. Их трудно обнаружить разработчику. Их можно выявить только изучая код.
Это не то же самое, что говорить что не стоить никогда использовать скрытые зависимости. Скрытые зависимости часто являются результатом предоставления разумных значений по умолчанию (providing sensible defaults). В этом примере это может не быть проблемой.
MyComponent имеет скрытую зависимость от MyDefaultImpl как можно видеть в конструкторе. Но MyDefaultImpl не имеет опасных сайд-эффектов, поэтому в данном случае скрытая зависимость не опасна.
Прямые и непрямые зависимости
Зависимость может быть либо прямой, либо непрямой. Если класс A использует класс B, тогда класс A имеет прямую зависимость от класса B. Если A зависит от B, B зависит от C, тогда A имеет непрямую зависимость от C. Если вы не можете использовать A без B, и не можете использовать B без С, то вы не можете также использовать A без C.
Непрямые зависимости также называют сцепленными (цепными), или транзитивными (в «Better, Faster, Lighter Java» by Bruce A. Tate and Justin Gehtland).
Неоправданно обширные зависимости
Иногда компоненты зависят от большей информации, чем им нужно для работы. Например, представьте компонент логина в веб-приложении. Этому компоненту нужны только логин и пароль, и он вернет объект пользователя, если найдет такового. Интерфейс может выглядеть так:
Вызов компонента мог бы выглядеть так:
Выглядит просто, не так ли? И даже если методу логина потребуется больше параметров, не нужно будет изменять вызывающий код.
Но сейчас метод логина имеет то, что я называю «неоправданно обширные зависимости» от интерфейса HttpServletRequest. Метод зависит от большего, чем ему требуется для работы. LoginManager требует только имя пользователя и пароль, чтобы найти пользователя, но получает HttpServletRequest как параметр в методе логина. HttpServletRequest содержит гораздо больше информации, чем нужно LoginManager.
Зависимость от интерфейса HttpServletRequest вызывает две проблемы:
Но посмотрите, что случится с вызывающим кодом теперь:
Он стал более сложным. Вот причина, по которой разработчики создают неоправданно широкие зависимости. Чтобы упростить вызывающий код.
Зависимости локальные и контекстные
При разработке приложений нормально разбивать приложения на компоненты. Некоторые из этих компонентов — компоненты общего назначения, которые могут быть использованы также в других приложениях. Другие компоненты специфичны для приложения и не будут использоваться за пределами приложения.
Для компонентов общего назначения, любые классы, принадлежащие к компоненту (или API), являются «локальными». Остальная часть приложения — это «контекст». Если компонент общего назначения зависит от специфичных для приложения классов, это называется «контекстная зависимость». Контекстные зависимости плохи тем, что делают невозможным использование компонента общего назначения вне приложения. Заманчиво думать, что только плохой ОО разработчик будет создавать контекстные зависимости, но это не так. Контекстные зависимости обычно возникают, когда разработчики стараются упростить создание своего приложения. Хороший пример здесь — приложения, обрабатывающие запросы, такие как приложения, соединенные с очередями сообщений или веб-приложения.
Представьте, что приложение, которое получает запрос в виде XML, обрабатывает запросы и получает в ответ XML. В обработке XML-запроса участвуют несколько отдельных компонентов. Каждому из этих компонентов нужна разная информация, некоторая информаця уже была обработана предыдущими компонентами. Очень соблазнительно собрать XML-файл и всю связанную обработку внутри объекта запроса некоторого вида, который отправляется всем компонентам, в последовательности обработки. Обрабатывающий компонент может считать информацию из этого объекта запроса и добавить информацию от себя для компонентов, стоящих далее в последовательности обработки. Принимая этот объект запроса как параметр, каждый из компонентов, обрабатывающих запрос, зависит от этого запроса. Объект запроса специфичен для приложения, это вызывает зависимость от контекста каждого компонента обработки запроса.
Стандартные vs кастомные зависимости класса/интерфейса
Во многих ситуациях для компонента лучше зависеть от класса или интерфейса из стандартных Java (или C#) пакетов. Эти классы или интерфейсы всегда доступны каждому, что упрощает удовлетворение этих зависимостей. Также эти классы с меньшей вероятностью могут измениться и вызвать падение компиляции вашего приложения.
Однако, в некоторых ситуациях зависеть от стандартных библиотек — не лучшая вещь. Например, методу нужно 4 строки для его конфигурации. Поэтому ваш метод принимает 4 строки как параметры. Например, это имя драйвера, url базы данных, имя пользователя и пароль для подключения к базе данных. Если все эти строки всегда используются вместе, для пользователя этого метода может быть понятнее, если вы сгруппируете эти 4 строки в класс и будете передавать его экземпляр, вместо 4 строк.
Резюме
Мы рассмотрели несколько разных типов и характеристик зависимостей. В общем, зависимости интерфейса предпочтительнее зависимостей классов. В некоторых ситуациях, вы можете обнаружить, что зависимость классов предпочтительнее зависимости интерфейсов. Зависимости методов и полей могут быть очень полезны, но помните, что они обычно — скрытые зависимости, и скрытые зависимости затрудняют пользователям ваших компонентов их нахождение, и удовлетворение их требований.
Зависимости реализации интерфейса встречаются чаще, чем вы думаете. Я видел их во многих приложениях и API. Постарайтесь ограничить их максимально, сохраняя интерфейсы небольшими. По крайней мере, те интерфейсы, которые реализует пользователь компонента. Переместите дополнительные функции (например, подсчет и т. д.) во внешние компоненты, которые принимают экземпляр рассматриваемого интерфейса в качестве параметра.
Лично я предпочитаю зависимости времени компиляции зависимостям времени исполнения, но в некоторых случаях зависимости времени исполнения более элегантны. Например, Mr. Persister использует зависимости времени выполнения от геттеров и сеттеров, что освобождает ваши pojo от реализации персистентного интерфейса. Зависимости времени исполнения таким образом, могут быть менее инвазивными, чем
зависимости времени компиляции.
Скрытые зависимости могут быть опасны, но поскольку зависимости времени выполнения иногда также скрытые зависимости, у вас может не всегда быть выбор.
Помните, что даже если компонент не имеет прямых зависимостей от другого компонента, он может все же иметь непрямую зависимость то него. Менее ограничивающие, но тем не менее, опосредованные зависимости — также зависимости.
Постарайтесь избегать неоправданно широких зависимостей. Держите в уме, что неоправданно широкие зависимости возникают тогда, когда вы группируете множество параметров в класс. Это общий рефакторинг, который проводится, чтобы сделать код более простым, но как вы можете видеть, может вести к неоправданно широким зависимостям.
Компонент, который предполагается использовать в различных контекстах, не должен иметь никаких контекстных зависимостей. То есть компонент не должен зависеть от других компонентов в контексте, в котором он изначально разработан и в том, в который он интегрирован.
Этот текст только описал зависимости. Он не говорит вам, что делать с ними. Другие тексты на этом тренинговом сайте погрузят вас в эту тему (прим. перев.: имеется ввиду личный сайт автора).