Рубрики
Без рубрики

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

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

Версия на испанском языке:

Внедрение зависимостей в 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”