Главная » Spring » Внедрение  зависимостей Spring

0

Для кого-то фраза «внедрение зависимостей» может звучать устрашающе, вызывая в воображении сложные приемы программи- рования или шаблоны проектирования. Однако на самом деле DI не настолько сложно, как кажется. На самом деле применение DI в проектах позволяет существенно упростить программный код, об- легчит его понимание и тестирование.

Любое нетривиальное приложение (более сложное, чем простое приложение Hello World) состоит из двух или более классов, ко- торые взаимодействуют друг с другом, реализуя некоторую логику. Обычно каждый объект ответствен за получение собственных ссы- лок на объекты, с которыми он взаимодействует (его зависимости). Это может привести к сильной связности и сложностям при тести- ровании.

Например, взгляните на класс рыцаря в листинге 1.3 ниже.

Листинг 1.3. Класс DamselRescuingKnight может принимать только экземпляр класса RescueDamselQuests

package  com.springinaction.knights;

public  class  DamselRescuingKnight  implements  Knight  { private  RescueDamselQuest  quest;

public  DamselRescuingKnight()  {

quest = new RescueDamselQuest();   // Тесная связь с классом

}                                                               // RescueDamselQuest

public void embarkOnQuest() throws QuestException { quest.embark();

Как показано в листинге 1.3, экземпляр DamselRescuingKnight созда- ет собственный экземпляр RescueDamselQuest в конструкторе. Это обу- словливает тесную связь между DamselRescuingKnight и RescueDamselQuest и существенно ограничивает возможности рыцаря. Если потребу- ется спасти даму, он готов будет совершить этот подвиг. Но если потребуется убить дракона или, например, стать рыцарем Круглого стола, он не сможет сделать этого1.

Но, что хуже всего, для класса DamselRescuingKnight очень сложно будет написать модульный тест. В процессе тестирования желатель- но было бы убедиться, что при вызове метода embarkOnQuest() класса DamselRescuingKnight вызывается метод embark() класса RescueDamselQuest. Но в данном случае нет очевидного способа реализовать такую про- верку. К сожалению, класс DamselRescuingKnight останется непротести- рованным.

Связь классов – это «двуглавый зверь». С одной стороны, сильно связанный код сложен в тестировании, его трудно использовать по- вторно, он сложен в понимании, и, как правило, такой код оказыва- ется очень хрупким при исправлении ошибок (исправление одной ошибки может привести к нескольким новым). С другой стороны, совершенно не связанный код ничего не делает. Чтобы делать что-то полезное, классы должны знать друг о друге. Связанность необхо- дима, но должна тщательно контролироваться.

С другой стороны, благодаря DI объекты получают свои зависи- мости во время создания от некоторой третьей стороны, координи- рующей работу каждого объекта в системе. Объекты не создают и не получают свои зависимости самостоятельно – зависимости вне- дряются в объекты.

Для иллюстрации рассмотрим класс BraveKnight, представленный в листинге 1.4, реализующий рыцаря, который не только храбр, но и способен совершать любые подвиги.

Листинг 1.4. Класс BraveKnight, достаточно гибкий, чтобы совершить любой подвиг

package  com.springinaction.knights;

public  class  BraveKnight  implements  Knight  {

1   Здесь необходимо уточнить, что класс DamselRescuingKnight  реализует

«рыцаря, спасающего даму», а класс RescueDamselQuest реализует «сцена- рий спасения дамы». – Прим. перев.

private  Quest  quest;

public BraveKnight(Quest quest) {

this.quest  =  quest;                                    // Внедрение  сценария  подвига

}

public void embarkOnQuest() throws QuestException { quest.embark();

}

}

Как видно из листинга, в отличие от класса DamselRescuingKnight, класс BraveKnight не создает собственного сценария подвига, а полу- чает его извне, в виде аргумента конструктора. Такой способ внедре- ния зависимостей называется внедрением через конструктор.

Более того, сценарий подвига имеет тип интерфейса Quest, который реализуют все такие сценарии. Поэтому BraveKnight (храбрый рыцарь) сможет совершать любые подвиги, такие как RescueDamselQuest (спас- ти даму), SlayDragonQuest (убить дракона), MakeRoundTableRounderQuest (стать рыцарем Круглого стола) или любой другой, реализующий интерфейс  Quest.

Фактически класс BraveKnight никак не связан с конкретной реа- лизацией Quest. Для него не важно, какой подвиг будет поручен, при условии что он реализует интерфейс Quest. В этом состоит основное преимущество DI – слабая связанность. Если объект взаимодейству- ет со своими зависимостями только через их интерфейсы (ничего не зная о конкретных реализациях или особенностях их создания), за- висимости можно будет замещать любыми другими реализациями, без необходимости учитывать эти различия в самом объекте.

Прием замены зависимостей очень часто используется при тести- ровании, когда выполняется подстановка фиктивной реализации. Класс DamselRescuingKnight невозможно было протестировать в пол- ной мере из-за тесной связи, но класс BraveKnight  легко поддается тестированию за счет подстановки фиктивной реализации интер- фейса Quest, как показано в листинге 1.5.

Листинг 1.5. Протестировать класс BraveKnight можно с помощью фиктивной реализации интерфейса Quest

package  com.springinaction.knights; import static org.mockito.Mockito.*;

import org.junit.Test;

public class BraveKnightTest {

@Test

public void knightShouldEmbarkOnQuest() throws QuestException { Quest mockQuest = mock(Quest.class);   // Создание фиктивного

// объекта  Quest BraveKnight knight = new BraveKnight(mockQuest); // Внедрение knight.embarkOnQuest();

verify(mockQuest,  times(1)).embark();

}

}

В данном примере фиктивная реализация интерфейса Quest соз- дана с помощью фреймворка Mockito. После получения фиктивно- го объекта создается новый экземпляр BraveKnight, в который через конструктор внедряется фиктивный объект Quest. После вызова ме- тода embarkOnQuest() выполняется обращение к фреймворку Mockito с целью убедиться, что метод embark() интерфейса Quest фиктивного объекта был вызван только один раз.

Рис. 1.1. Механизм внедрения зависимостей основан на предоставлении объекту

его зависимостей извне, а не на приобретении этих зависимостей самим объектом

Передача сценария подвига рыцарю

Теперь, когда класс BraveKnight может принимать любые задания, как определить, какой именно объект Quest был ему передан? Про- цесс создания связей между прикладными компонентами называет- ся связыванием (wiring). Фреймворк Spring поддерживает множест-

во способов связывания компонентов, но наиболее общим из них является способ на основе XML. В листинге 1.6 показано содержи- мое простого конфигурационного файла Spring, knights.xml, который передает объекту BraveKnight задание SlayDragonQuest.

Листинг 1.6. Внедрение сценария SlayDragonQuest в объект BraveKnight средствами Spring

<?xml  version="1.0"  encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

<bean id="knight" class="com.springinaction.knights.BraveKnight">

<constructor-arg ref="quest" />     <!– Внедрение компонента quest –>

</bean>

<!– Создание SlayDragonQuest –>

<bean id="quest"

class="com.springinaction.knights.SlayDragonQuest"  />

</beans>

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

Теперь, объявив отношения между BraveKnight и Quest, необходимо загрузить XML-файл и запустить приложение.

Рассмотрим этот механизм в действии

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

Поскольку компоненты  приложения объявлены в XML-файле

knights.xml, в качестве контекста приложения может использовать- ся класс ClassPathXmlApplicationContext. Реализация контекста в Spring загружает контекст из одного или более XML-файлов, находящихся в библиотеке классов (classpath). Метод main() в листинге 1.7 ис- пользует ClassPathXmlApplicationContext, чтобы загрузить knight.xml и получить ссылку на объект Knight.

Листинг 1.7. KnightMain.java загружает контекст Spring, содержащий объект Knight

package  com.springinaction.knights;

import  org.springframework.context.ApplicationContext;

import      org.springframework.context.support.ClassPathXmlApplicationContext;

public class KnightMain {

public  static  void  main(String[]  args)  {

// Загрузка контекста Spring ApplicationContext context =

new       ClassPathXmlApplicationContext("knights.xml");

// Получение компонента knight

Knight   knight   =   (Knight)   context.getBean("knight");

// Использование компонента knight knight.embarkOnQuest();

}

}

Здесь метод main() создает контекст приложения Spring на основе файла knights.xml. Затем использует контекст как фабрику для из- влечения компонента с идентификатором «knight». Получив ссылку на объект Knight, он вызывает метод embarkOnQuest(), чтобы отправить рыцаря выполнять задание. Обратите внимание, что этот класс ниче- го не знает о задании Quest, переданном рыцарю. Собственно говоря, он даже не знает, что имеет дело с классом BraveKnight. Только файл knights.xml имеет полную информацию о реализациях, участвующих в работе.

На этом мы закончим краткое знакомство с приемом внедрения зависимостей. Дополнительные примеры  применения DI будут встречаться на протяжении всей книги. Желающим поближе позна- комиться с этим приемом я рекомендую прочитать книгу Дханжи Прасанна (Dhanji R. Prasanna) «Dependency Injection».

А теперь познакомимся с другими стратегиями упрощения про- граммирования на языке Java – декларативным программированием посредством аспектов.

Источник:   Уоллс К., Spring в действии. – М.: ДМК Пресс, 2013. – 752 с.: ил.

По теме:

  • Комментарии