Learning Java/JavaEE

Blog about my Java/JavaEE experiences


馃拤 Inyecci贸n de Dependencias en Java 鈽曪笍

Collaborators

Read the English Version here

Java es un lenguaje orientado a objetos con algunos aspectos funcionales incluidos en su n煤cleo. Al igual que cualquier otro lenguaje orientado a objetos, las clases y los objetos son la base de cualquier funcionalidad que podamos escribir y usar. Las relaciones entre las clases / objetos permiten ampliar y reutilizar la funcionalidad. Sin embargo, la forma en que elegimos construir esas relaciones determina cu谩n modular, desacoplada y reutilizable es nuestra base de c贸digo, no solo en t茅rminos de nuestro c贸digo de producci贸n sino tambi茅n en nuestras suites de prueba.

En este art铆culo, vamos a describir el concepto de Inyecci贸n de dependencias en Java y c贸mo nos ayuda a tener una base de c贸digo m谩s modular y desacoplada, lo que nos facilita la vida, incluso para las pruebas, sin la necesidad de ning煤n contenedor o marco sofisticado.

驴Qu茅 es una Dependencia?

Cuando una clase ClassA usa cualquier m茅todo de otra clase ClassB, podemos decir que ClassB es una dependencia de ClassA.

1

class ClassA {

  ClassB classB = new ClassB();

  int tenPercent() {
    return classB.calculate() * 0.1d;
  }
}

En este ejemplo, ClassA est谩 calculando el 10% del valor, y calculando ese valor, est谩 reutilizando la funcionalidad expuesta por ClassB.

2

Y se puede usar as铆:

class Main {
  public static void main(String... args) {
    ClassA classA = new ClassA();

    System.out.println("Ten Percent: " + classA.tenPercent());
  }
}

Ahora, hay un gran problema con este enfoque:

ClassA est谩 estrechamente acoplada con ClassB

Si necesitamos cambiar / reemplazar ClassB con ClassC porque ClassC tiene una versi贸n optimizada del m茅todo Calculate () , necesitamos recompilar ClassA porque no tenemos una manera de cambiar esa dependencia , est谩 codificado dentro de ClassA.

Collision

El Principio de Inyecci贸n de Dependencias

El Principio de inyecci贸n de dependencia no es m谩s que poder pasar (inyectar) las dependencias cuando sea necesario en lugar de inicializar las dependencias dentro de la clase receptora.

Desacoplar la construcci贸n de sus clases de la construcci贸n de las dependencias de sus clases.

Tipos de Inyecci贸n de Dependencias en Java

Inyecci贸n con Setters (No recomendado)

class ClassA {

  ClassB classB;

  /* Setter Injection */
  void setClassB(ClassB injected) {
    classB = injected;
  }

  int tenPercent() {
    return classB.calculate() * 0.1d;
  }
}

Con este enfoque, eliminamos la palabra clave new de nuestra ClassA. Por lo tanto, alejamos la responsabilidad de la creaci贸n de ClassB de ClassA.

ClassA todav铆a tiene una fuerte dependencia de ClassB pero ahora se puede inyectar desde el exterior:

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());
  }
}

El ejemplo anterior es mejor que el enfoque inicial porque ahora podemos inyectar en ClassA una instancia de ClassB o incluso mejor, una subclase de 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());
  }
}

Pero hay un problema significativo con el enfoque de Inyecci贸n con Setters:

Estamos ocultando la dependencia ClassB en ClassA porque al leer la firma del constructor, no podemos identificar sus dependencias de inmediato. El siguiente c贸digo provoca una NullPointerException en tiempo de ejecuci贸n:

class Main {
  public static void main(String... args) {
    ClassA classA = new ClassA();

    System.out.println("Ten Percent: " + classA.tenPercent()); // NullPointerException here
  }
}

npe

En un lenguaje de tipo est谩tico como Java, siempre es bueno dejar que el compilador nos ayude. Ver Inyecci贸n con Constructor

Inyecci贸n con Constructor (Altamente recomendado)

class ClassA {

  ClassB classB;

  /* Constructor Injection */
  ClassA(ClassB injected) {
    classB = injected;
  }

  int tenPercent() {
    return classB.calculate() * 0.1d;
  }
}

ClassA todav铆a tiene una fuerte dependencia de ClassB pero ahora se puede inyectar desde afuera usando el constructor:

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());
  }
}

VENTAJAS:

Happy

Inyecci贸n con Fields (Ni帽os no intenten esto en casa)

Hay una tercera forma de inyectar dependencias en Java, y se llama Inyecci贸n con Fields. La 煤nica manera de que funcione la inyecci贸n de campo es:

Este enfoque tiene los mismos problemas expuestos por el enfoque de Inyecci贸n con Setters y adem谩s agrega complejidad debido a la mutaci贸n / reflexi贸n requerida. Desafortunadamente, este es un patr贸n bastante com煤n cuando las personas usan un Framework de inyecci贸n de dependencias.

NOTA:

Cuando una clase ClassA usa cualquier m茅todo de otra clase ClassB podemos decir que ClassB es una dependencia de ClassA.

Si ClassA tiene una dependencia de ClassB, el constructor ClassA deber铆a requerir ClassB.

Feedback

Ejemplo Realista

Cada ejemplo de Hello World para cualquier idea, concepto o patr贸n es muy simple de entender y simplemente funciona bien. Pero cuando necesitamos implementarlo en un proyecto real, las cosas se vuelven m谩s complicadas y, a menudo, como ingenieros, tendemos a tratar de resolver el problema introduciendo nuevas capas al problema en lugar de comprender cu谩l es el problema real.

Ahora que conocemos las ventajas del Principio de Inyecci贸n de Dependencia usando el enfoque de Inyecci贸n de constructor, creemos un ejemplo m谩s realista para ver algunos inconvenientes y c贸mo podemos resolverlo sin introducir una nueva capa en la mezcla.

La Aplicaci贸n de Tareas (ToDo鈥檚 App)

Todos

Dise帽emos una aplicaci贸n de Todo para realizar operaciones CRUD (Crear, Leer, Actualizar, Eliminar) para administrar nuestra lista de tareas, y una arquitectura original puede ser as铆:

3

4

Escribamos las clases de Java para nuestro dise帽o usando el enfoque de Inyecci贸n del constructor que acabamos de aprender:

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/")));
  }
}

Ahora enfoquemos nuestra atenci贸n en la relaci贸n entre las clases TodoView y TodoHttpClient y agreguemos m谩s detalles:

Magic

class TodoHttpClient extends MyMagicalHttpAbstraction {

  TodoView(final String baseUrl) {
    super(baseUrl);
  }

  @GET
  List<Todo> 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();
  }
}

Testing our design

Scientist

Creemos una prueba unitaria para la clase TodoView donde probamos la clase de forma aislada sin instanciar ninguna de sus dependencias. En este caso, la dependencia es TodoHttpClient:

@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();
  }
}

Ahora que tenemos nuestro caso de prueba aprobado, analicemos c贸mo nuestro dise帽o impacta el enfoque de prueba:

Mejoremos nuestro dise帽o

VideoLearning

Una cosa que podemos hacer para desacoplar nuestras clases es introducir una interfaz, ya que el lenguaje Java siempre es bueno confiar en abstracciones en lugar de confiar en implementaciones reales.

Pongamos una interfaz entre TodoView y TodoHttpClient:

5

TodoProvider

interface TodoProvider {
  List<Todo> getAll();
  Todo get(long id);
  long save(Todo todo);
  Todo update(Todo todo);
  void delete(long id);
}

Hagamos el TodoHttpClient para implementar esa interfaz:

class TodoHttpClient extends MyMagicalHttpAbstraction implements TodoProvider {

  TodoView(final String baseUrl) {
    super(baseUrl);
  }

  @GET
  List<Todo> 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);
  }
}

Ahora la clase TodoView se ve as铆:

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();
  }
}

驴Qu茅 ganamos con estos cambios?

Podemos cambiar el TodoHttpClient con algo como TodoDBProvider en el TodoApp y el comportamiento de la aplicaci贸n seguir谩 siendo el mismo:

new TodoApp(new TodoView(new TodoDbProvider("dbName", "dbUser", "dbPassword")));

Veamos c贸mo eso ayuda en las pruebas unitarias

Library

@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();
  }
}

La prueba sigue pasando (verde), lo cual es genial, pero espera 鈥 nada cambi贸 en realidad :(

Los 煤nicos cambios estaban relacionados con los nombres:

驴Podemos eliminar Mockito?

Si ahora tenemos una interfaz, 驴por qu茅 estamos acoplados a Mockito para crear un objeto falso que podemos crear manualmente usando una clase an贸nima? Cambiemos eso:

@ExtendWith(MockitoExtension.class)
class TodoViewTest {

  // Given
  TodoProvider provider = new TodoProvider() {
    @Override
    public List<TodoItem> 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();
}

Genial, ahora nuestro dise帽o es m谩s flexible, ya que podemos inyectar una implementaci贸n diferente de TodoProvider, y podemos hacer lo mismo en nuestras pruebas sin usar Mockito. Pero estamos pagando el precio: Verbosidad, Mockito elimina la necesidad de implementar todos los m茅todos de las interfaces.

Solo el comienzo

En el siguiente art铆culo, eliminemos la verbosidad de las pruebas y escribamos un dise帽o a煤n mejor.

Est茅n atentos para m谩s publicaciones como esta.