Hablemos sobre las ventajas de la inversión de control (IoC por sus siglas en inglés) y el desacoplamiento en el desarrollo de software.
desacoplamiento
Frecuentemente escuchamos que el bajo acoplamiento es una de las cualidades del buen software, pero no es muy claro al comienzo de un proyecto por qué.
Tomemos en cuenta el siguiente requerimiento:
Dentro de un conjunto de datos con tareas (por ejemplo: ir a comprar la comida del perro), es necesario exponer un endpoint que retorne únicamente aquellas tareas que se encuentran terminadas.
Ahora bien, imaginemos que, más tarde, durante la investigación de los detalles de esta tarea, nos encontramos con que ese conjunto de datos en realidad es un servicio externo, esto quiere decir que tendríamos que:
- lanzar una petición a dicho servicio para obtener la información
- filtrar las tareas que se encuentren completadas
- retornar la información filtrada
En general, esto no representa un obstáculo a nivel técnico, incluso podríamos plasmar la lógica de la siguiente forma:
final var client = HttpClient.newBuilder()
.build();
final var request = HttpRequest.newBuilder()
.GET()
.uri(URI.create(ENDPOINT))
.build();
try {
final var response = client.send(request, HttpResponse.BodyHandlers.ofString());
final var rawJson = response.body();
final var gson = new Gson();
final var results =
Arrays.asList(gson.fromJson(rawJson, TaskDto[].class));
// retrieve the filtered results directly
return results.stream()
.filter(t -> Boolean.TRUE.equals(t.getCompleted()))
.collect(Collectors.toList());
} catch (final IOException | InterruptedException ex) {
LOG.warning("Error while calling the task's endpoint");
return Collections.emptyList();
}
El inconveniente de hacerlo todo directamente en la capa superior (en este caso, el API), es que más tarde no podremos reutilizar esos segmentos de código en capas inferiores.
Entonces, una segunda iteración para nuestra solución podría considerar lo siguiente:

La idea es que tengamos una capa inicial de lógica (donde se integra toda la solución). Para nuestro caso, estamos utilizando el patrón de facade; conforme avancemos hacia las capas más internas, las operaciones que realizaremos serán más atómicas.
Por ejemplo, la capa de service en este caso se encarga únicamente de la llamada al servicio tercero. Esto permitirá que, de ser necesario trabajar con la información del servicio remoto (filtrar por tareas completadas, mostrar toda la información, filtrar por usuario, etc.), aún podamos reutilizar el servicio y efectuar las operaciones en la capa superior.
Haciendo los ajustes necesarios:
Facade:

Service:

inversión de control
Lo que tenemos hasta ahora puede funcionar eficazmente. El problema es que, dadas las instancias de nuestras dependencias (dependencia del servicio en fachada), volvemos a tener un alto acoplamiento, ya que, si bien, el código del servicio es reutilizable, será necesario cambiar la clase concreta explícitamente en todos los lugares necesarios en caso de que más de un componente (clase) dependa del mismo servicio.
Para contrarrestar esto existe el patrón de inversión de control, el cual, como su nombre lo sugiere, delega la responsabilidad a la capa superior sobre la instancia que se va a utilizar internamente:

Prácticamente, se trata de recibir la instancia como parámetro, y que la responsabilidad (el control) la tenga la capa superior (la cual utiliza nuestro componente actual), esto remueve la responsabilidad de crear el componente interno. De aquí el nombre de inversion de control.


SOLID y otros cuentos
El problema con la implementación anterior es que aún seguimos comprometiendo la clase concreta que estamos utilizando, y claramente recuerdo que es de gente bien usar interfaces.

Pero, en serio, el utilizar interfaces nos beneficiará en mostrar únicamente los métodos que queremos exponer y reducir riesgos, por lo que es necesario utilizarlos en este punto:
Facade:

Service:


conclusiones
En este punto es cuando hablamos sobre los beneficios de utilizar estas prácticas/patrones en el desarrollo para aumentar la reusabilidad de código, reducir el acoplamiento, facilitar las pruebas unitarias, mejorar la calidad del código, etc., pero creo que eso está más que explicado en otras publicaciones.
La moraleja de este post, es que, considerando un código que cualquiera de nosotros pudimos haber escrito en 15-20 minutos, tuvimos que refactorizar una y otra vez para llegar a un estado de calidad alta, pero habríamos ahorrado mucho tiempo si hubiéramos confiado y seguido desde el comienzo las prácticas y principios como SOLID, DRY, Objects calisthenics, entre otros.
Yo sé que los siguientes puntos son una traducción de algunos de los principios de SOLID, pero para que queden un poco más claros, los traduciré al español que todos entendemos:
- para toda clase, hay un 99.99 % de probabilidad que requiera una interface
- una clase debería enfocarse a un solo tipo de cosas (consumir un servicio, filtrar resultados, construir una respuesta, etc.)
- las instancias de las dependencias se delegan a las capas superiores
Puedes encontrar el código de ejemplo en este repositorio