Версия на испанском языке:
Внедрение зависимостей в 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
List getAll() {
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 {
List getAll();
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
List getAll() {
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 List getAll() {
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”