Версия на испанском языке:
Внедрение зависимостей в Java
Карлос Чачин ☕ 👽 – 8 февраля – 9 минут чтения
Статья была первоначально опубликована по адресу cchacin.github.io
ОБНОВЛЕНИЕ: Редакционные изменения для улучшения удобочитаемости, благодаря Шефали Агарвал
.
ОБНОВЛЕНИЕ 2019-12-19: Редакционные изменения для улучшения удобочитаемости.
Java – это объектно-ориентированный язык с некоторыми функциональными аспектами, включенными в его ядро. Как и любой другой объектно-ориентированный язык, классы и объекты являются основой любой функциональности, которую мы можем написать и использовать. Связи между классами/объектами позволяют расширять и повторно использовать функциональность. Однако способ, который мы выбираем для построения этих отношений, определяет, насколько модульной, несвязанной и многоразовой является наша кодовая база, не только с точки зрения нашего производственного кода, но и в наших наборах тестов.
В этой статье мы опишем концепцию внедрения зависимостей в Java и то, как это помогает нам иметь более модульную и несвязанную кодовую базу, что облегчает нашу жизнь, даже для тестирования, без необходимости в каком-либо сложном контейнере или фреймворке.
Что такое Зависимость?
Когда класс Класс
использует любой метод другого класса ClassB
, мы можем сказать , что Класс B
является зависимостью от Класс
.
class ClassA { ClassB classB = new ClassB(); int tenPercent() { return classB.calculate() * 0.1d; } }
В этом примере/| Класс вычисляет 10% от значения и, вычисляя это значение, повторно использует функциональность, предоставляемую
классом B .
И его можно использовать следующим образом:
class Main { public static void main(String... args) { ClassA classA = new ClassA(); System.out.println("Ten Percent: " + classA.tenPercent()); } }
Теперь с этим подходом возникает большая проблема:
Класс
тесно связан с ClassB
Если нам нужно было изменить/заменить Class
на Classic
, потому что Classic
имеет оптимизированную версию метода calculate()
, нам нужно перекомпилировать Класс
поскольку у нас нет способа изменить эту зависимость, она жестко закодирована внутри Класс
.
Принцип Внедрения Зависимостей
Принцип Внедрения зависимостей – это не что иное, как возможность передавать ( вводить
) зависимости при необходимости вместо инициализации зависимостей внутри класса получателя.
Отделите построение ваших классов от построения зависимостей ваших классов
Формы внедрения зависимостей в Java
Инъекция сеттера (не рекомендуется)
class ClassA { ClassB classB; /* Setter Injection */ void setClassB(ClassB injected) { classB = injected; } int tenPercent() { return classB.calculate() * 0.1d; } }
При таком подходе мы удаляем new
ключевое слово из нашего Класс А
. Таким образом, мы переносим ответственность за создание класса B
с Класс
.
Класс
все еще имеет жесткую зависимость от ClassB
но теперь это может быть введено
извне:
class Main { public static void main(String... args) { ClassA classA = new ClassA(); ClassB classB = new ClassB(); classA.setClassB(classB); System.out.println("Ten Percent: " + classA.tenPercent()); } }
Приведенный выше пример лучше, чем первоначальный подход, потому что теперь мы можем вводить
в Класс
экземпляр ClassB
или, что еще лучше, подкласс ClassB
:
class ImprovedClassB extends ClassB { // content omitted }
class Main { public static void main(String... args) { ClassA classA = new ClassA(); ImprovedClassB improvedClassB = new ImprovedClassB(); classA.setClassB(improvedClassB); System.out.println("Ten Percent: " + classA.tenPercent()); } }
Но существует серьезная проблема с подходом Setter Injection
:
Мы скрываем зависимость Class
в Класс
потому что, читая сигнатуру конструктора, мы не можем сразу определить его зависимости. Приведенный ниже код вызывает исключение NullPointerException
во время выполнения:
class Main { public static void main(String... args) { ClassA classA = new ClassA(); System.out.println("Ten Percent: " + classA.tenPercent()); // NullPointerException here } }
В статически типизированном языке, таком как Java, всегда полезно позволить компилятору помочь нам. См. Инъекция Конструктора
Инъекция конструктора (настоятельно рекомендуется)
class ClassA { ClassB classB; /* Constructor Injection */ ClassA(ClassB injected) { classB = injected; } int tenPercent() { return classB.calculate() * 0.1d; } }
Класс
все еще имеет жесткую зависимость от ClassB
но теперь он может быть введен
извне с помощью конструктора:
class Main { public static void main(String... args) { /* Notice that we are creating ClassB fisrt */ ClassB classB = new ImprovedClassB(); /* Constructor Injection */ ClassA classA = new ClassA(classB); System.out.println("Ten Percent: " + classA.tenPercent()); } }
преимущества:
- Функциональность остается неизменной по сравнению с
Инъекцией сеттера
подход - Мы удалили
новую
инициализацию изКласс
. - Мы все еще можем ввести специализированный подкласс
ClassB
вКласс А
. - Теперь компилятор собирается запросить у нас зависимости, которые нам нужны во время компиляции.
Полевая инъекция (дети не пробуют это дома)
Существует 3-й способ внедрения зависимостей в Java, и он называется Field Injection
. Единственный способ для работы полевой инъекции – это:
- Изменение поля, потому что это непубличное и не окончательное поле
- Изменение конечного/закрытого поля с использованием отражения
Этот подход имеет те же проблемы, что и подход Setter Injection
, и дополнительно добавляет сложности из-за требуемой мутации/отражения. К сожалению, это довольно распространенный шаблон, когда люди используют Dependency Injection Framework
.
записка:
Когда класс Класс
использует любой метод другого класса ClassB
мы можем сказать, что Класс B
является зависимостью от Класс
.
Если Класс
имеет зависимость от ClassB
, Класс
конструктор должен требовать ClassB
.
Реалистичный Пример
Каждый Привет, мир
пример для любой идеи, концепции или шаблона очень прост для понимания, и он просто отлично работает. Но когда нам нужно реализовать это в реальном проекте, все становится сложнее, и часто, как инженеры, мы склонны пытаться решить проблему, вводя новые уровни в проблему вместо того, чтобы понимать, в чем заключается реальная проблема.
Теперь, когда мы знаем преимущества Dependency Injection Principle
с использованием подхода Constructor Injection
, давайте создадим более реалистичный пример, чтобы увидеть некоторые неудобства и как мы можем их решить, не вводя новый слой в микс.
Приложение Todos
Давайте разработаем приложение Todos для выполнения операций CRUD (Создание, Чтение, Обновление, Удаление) для управления нашим списком задач, и оригинальная архитектура может быть такой:
TodoApp
– это основной класс, который будет инициализировать наше приложение; это может быть приложение для Android, веб-страница или настольное приложение, использующее любой фреймворк.Чтобы сделать View
классом, который будет отображать представление для взаимодействия, этот класс собирается делегировать аспекты, связанные с данными,TodoHttpClient
. Единственная ответственность заключается в том, чтобы рисовать/рисовать/отображать информацию и получать входные данные для выполнения действий с данными, используя зависимостьTodoHttpClient
.To do Http Client
– это класс, содержащий набор HTTP-методов для выполненияTodo
объекты, использующие REST API.To do
– это объект value, представляющий элемент todo в нашем хранилище данных.
Давайте напишем классы Java для нашего дизайна, используя подход Constructor Injection
, который мы только что изучили:
class Todo { /* Value Object class */ // content omitted }
class TodoApp { private final TodoView todoView; TodoApp(final TodoView todoView) { this.todoView = todoView; } // content omitted }
class TodoView { private final TodoHttpClient todoHttpClient; TodoView(final TodoHttpClient todoHttpClient) { this.todoHttpClient = todoHttpClient; } // content omitted }
class Main { public static void main(String... args) { new TodoApp(new TodoView(new TodoHttpClient("https://api.todos.io/"))); } }
Теперь давайте сосредоточим наше внимание на взаимосвязи между TodoView
и
классы TodoHttpClient
class TodoHttpClient extends MyMagicalHttpAbstraction { TodoView(final String baseUrl) { super(baseUrl); } @GET ListgetAll() { return super.get(Todo.class); } @GET Todo get(long id) { return super.get(Todo.class, id); } @POST long save(Todo todo) { return super.post(todo); } @PUT Todo update(Todo todo) { return super.put(todo, todo.getId()); } @DELETE void delete(long id) { super.delete(Todo.class, id); } }
class TodoView extends MyFrameworkView { private final TodoHttpClient httpClient; // View initialized by the view library/framework // or injected as a dependency as well private ListView listView; private DetailView detailView; TodoView(final TodoHttpClient httpClient) { this.httpClient = httpClient; } void showTodos() { listView.add(httpClient.getAll()); } void showTodo(Todo selected) { detailView.print(httpClient.get(selected.getId())); } void save(Todo todo) { httpClient.save(todo); listView.add(todo) } void update(Todo todo) { httpClient.update(todo); detailView.refresh(todo); } void delete(long id) { httpClient.delete(id); listView.refresh(); } }
Тестирование нашего дизайна
Давайте создадим модульный тест для класса TodoView
, где мы протестируем класс изолированно, не создавая никаких его зависимостей. В этом случае зависимость – это Для выполнения Http-клиента
:
@ExtendWith(MockitoExtension.class) class TodoViewTest { @Test void shouldBeEmptyWhenEmptyList(@Mock TodoHttpClient httpClient) { // Given Mockito.when(httpClient.getAll()).thenReturn(List.of()); // When TodoView todoView = new TodoView(httpClient); todoView.showTodos(); // Then Assertions.assertThat(todoView.getListView()).isEmpty(); } }
Теперь, когда мы прошли наш тестовый пример, давайте проанализируем, как наш дизайн влияет на подход к тестированию:
- Мы ввели фреймворк Mockito , чтобы иметь возможность создавать поддельный экземпляр
Для выполнения Http-клиента
, и это значительно усложняет задачу. - Мы должны подготовить наш экземпляр
Для выполнения Http Client
, чтобы подделать возврат пустого списка при вызове методаGetAll()
, теперь наш модульный тест также содержит сведения о реализацииTodoHttpClient
. - Кроме того, поскольку
To do Http Client
является конкретным классом, мы не можем изменить реализацию для вызова DB вместо этого без необходимости также изменять классTodoView
, и нам потребуется переписать модульные тесты, даже если они должны изолировать эту деталь реализации.
Давайте улучшим наш дизайн
Одна вещь, которую мы можем сделать, чтобы отделить наши классы, – это ввести интерфейс, поскольку язык Java всегда полезен для того, чтобы полагаться на абстракции вместо того, чтобы полагаться на реальные реализации.
Давайте создадим интерфейс между TodoView
и Чтобы сделать Http-клиент
:
Что делать Провайдеру
interface TodoProvider { ListgetAll(); Todo get(long id); long save(Todo todo); Todo update(Todo todo); void delete(long id); }
Давайте создадим Для выполнения Http-клиента
для реализации этого интерфейса:
class TodoHttpClient extends MyMagicalHttpAbstraction implements TodoProvider { TodoView(final String baseUrl) { super(baseUrl); } @GET ListgetAll() { return super.get(Todo.class); } @GET Todo get(long id) { return super.get(Todo.class, id); } @POST long save(Todo todo) { return super.post(todo); } @PUT Todo update(Todo todo) { return super.put(todo, todo.getId()); } @DELETE void delete(long id) { super.delete(Todo.class, id); } }
Теперь класс TodoView
выглядит следующим образом:
class TodoView extends MyFrameworkView { private final TodoProvider provider; // View initialized by the view library/framework // or injected as a dependency as well private ListView listView; private DetailView detailView; TodoView(final TodoProvider httpClient) { this.provider = provider; } void showTodos() { listView.add(provider.getAll()); } void showTodo(Todo selected) { detailView.print(provider.get(selected.getId())); } void save(Todo todo) { provider.save(todo); listView.add(todo) } void update(Todo todo) { provider.update(todo); detailView.refresh(todo); } void delete(long id) { provider.delete(id); listView.refresh(); } }
Что мы получаем от этих изменений?
Мы можем изменить Для выполнения Http-клиента
с помощью чего-то вроде Выполнить DB Provider
в TodoApp
, и поведение приложения останется прежним:
new TodoApp(new TodoView(new TodoDbProvider("dbName", "dbUser", "dbPassword")));
Давайте посмотрим, как это помогает в модульных тестах
@ExtendWith(MockitoExtension.class) class TodoViewTest { @Test void shouldBeEmptyWhenEmptyList(@Mock TodoProvider provider) { // Given Mockito.when(provider.getAll()).thenReturn(List.of()); // When TodoView todoView = new TodoView(httpClient); todoView.showTodos(); // Then Assertions.assertThat(todoView.getListView()).isEmpty(); } }
Тест по-прежнему зеленый, и это здорово, но подождите … на самом деле ничего не изменилось:(
Единственные изменения были связаны с наименованием:
Чтобы сделать Http-клиент
->Что делать Провайдеру
нет значения для теста.HttpClient
->провайдер
здесь нет никакой ценности для теста.- Мы все еще полагаемся на издевательский фреймворк.
- Мы все еще привязываем тест к имени интерфейса:
Для выполнения Provider
. - Мы все еще привязываем тест к имени метода:
GetAll()
Можем ли мы удалить издевательский фреймворк?
Если у нас теперь есть интерфейс, почему мы подключены к mocking framework для создания поддельного объекта, который мы можем создать вручную с помощью анонимного класса? Давайте изменим это:
@ExtendWith(MockitoExtension.class) class TodoViewTest { // Given TodoProvider provider = new TodoProvider() { @Override public ListgetAll() { return List.of(); } @Override public TodoItem get(long id) { return null; } @Override public long save(TodoItem todo) { return 0; } @Override public TodoItem update(TodoItem todo) { return null; } @Override public void delete(long id) { } }; // When var todoView = new TodoView(provider); todoView.displayListView(); // Then assertThat(todoView.getTodoItemList()).isEmpty(); }
Мило, теперь наш дизайн стал более гибким, так как мы можем ввести другой Чтобы выполнить реализацию Provider
, и мы можем сделать то же самое в наших тестах, не используя имитирующий фреймворк. Но мы платим цену: Многословие , фреймворк mocking устраняет необходимость в реализации каждого отдельного метода из интерфейсов.
Только начало
В следующей статье давайте уберем многословие из тестов и напишем еще лучший дизайн.
Следите за обновлениями для получения новых сообщений, подобных этому.
Оригинал: “https://dev.to/cchacin/dependency-injection-in-java-part-1-2o84”