CodeGym /Cursos /Java Server /Dependencias a través del constructor

Dependencias a través del constructor

Java Server
Nivel 18, Lección 3
Disponible

1. De «lo hace él mismo» a la colaboración

Si está acostumbrado a programas pequeños de aprendizaje, el modelo de pensamiento suele ser: «Escribí un método y él lo hace todo solo». Esto es normal al principio, pero en un proyecto backend muy pronto descubre que una clase rara vez trabaja sola. Casi siempre depende de otros objetos: alguien se conecta a la red, alguien analiza JSON, alguien imprime el resultado al usuario, alguien almacena datos. Y es importante no ocultar esas relaciones.

Imagine ReadLater Starter como un pequeño equipo. CatalogCommandController es la persona que recibe la orden «búsqueda en catálogo». CatalogService es quien entiende qué hay que hacer: formar la consulta, obtener datos, devolver el resultado. CatalogClient es el «mensajero» que realmente va al catálogo externo (o a datos mock). Cada uno tiene su rol, pero solo pueden trabajar juntos. Y la pregunta es: ¿cómo deben «pasarse el trabajo» para que el proyecto siga siendo claro y flexible?

El camino más común de un principiante: «bueno, en el servicio simplemente crearé el cliente». Y el cliente, a su vez, creará un HttpClient. Y HttpClient dentro de sí… bueno, en principio puede crear otro HttpClient, ¿por qué no? Así el proyecto se vuelve una muñeca rusa, donde cada capa oculta a la siguiente. ¿Funciona? A veces sí. ¿Es comprensible? Muy pronto deja de serlo. ¿Flexible? Casi nunca.

2. Dependencias: cómo verlas

La palabra «dependencia» suena un poco dramática, como si la clase hubiera caído en una dependencia emocional de ObjectMapper. En la práctica es más sencillo: una dependencia es otro objeto sin el cual el objeto actual no puede realizar su trabajo. Si el servicio no puede hacer una petición sin el cliente, entonces el cliente es su dependencia. Si el cliente no puede leer JSON sin ObjectMapper, entonces ObjectMapper es su dependencia.

La idea clave que reforzamos hoy: las dependencias deben ser visibles. No en el sentido de «estar en el escritorio del ordenador», sino en el sentido de «ser comprensibles a partir del código». Y la mejor manera de hacer visible una dependencia es pasarla a través del constructor. Así, al abrir la clase, se ve de inmediato: «Ajá, este servicio necesita tal cliente». Como la lista de ingredientes en un envase, solo que sin letra pequeña.

En Java esto es especialmente natural porque el constructor es el momento en que el objeto se vuelve «lista para vivir». Si un objeto no puede vivir sin un cliente, que el cliente sea una parte obligatoria de la creación del objeto. Así evitamos objetos semi-funcionales del tipo «creamos el servicio, y el cliente lo asignamos luego de alguna manera». En el código backend, «luego de alguna manera» suele significar «el viernes por la noche en producción».

Para fijar la diferencia, conviene decir: el parámetro de un método y la dependencia no son lo mismo. Si el método search(String query) recibe la cadena query, eso son datos de una llamada concreta. Pero si el servicio necesita un CatalogClient para funcionar, eso es parte de la construcción del servicio. Los datos van y vienen; la dependencia vive al lado durante mucho tiempo y determina el comportamiento del objeto.

3. Dependencias ocultas y el dolor del new

Mientras el proyecto es pequeño, «crear todo dentro de la clase» parece conveniente. Pero esa conveniencia es como un microcrédito: agradable en el momento, dolorosa después. Veamos un ejemplo típico de dependencias ocultas en el ya conocido dominio del catálogo.

Supongamos que ha creado el servicio así:

package com.example.readlater.catalog.service;

import com.example.readlater.catalog.client.CatalogHttpClient;
import com.example.readlater.catalog.client.CatalogClient;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.net.http.HttpClient;

public final class CatalogService {
    // Mal: el servicio crea sus propias dependencias (se vuelven "ocultas").
    // Desde fuera no se ve que aquí realmente se necesitan HttpClient, ObjectMapper y una implementación concreta del cliente.
    private final HttpClient httpClient = HttpClient.newHttpClient(); // objeto pesado, normalmente se reutiliza
    private final ObjectMapper objectMapper = new ObjectMapper();     // objeto pesado, normalmente se reutiliza
    private final CatalogClient client = new CatalogHttpClient(httpClient, objectMapper); // dependencia fija a una implementación
}

El código es corto, y un principiante suele pensar: «¡Qué bonito! Todo dentro, nada fuera hace falta». Y entonces comienza la realidad del backend.

Primero, pierde la capacidad de cambiar entre real y mock de manera normal. El proyecto ya tenía un modo mock, y tiene sentido: protege contra una API externa inestable. Pero si CatalogService crea él mismo CatalogHttpClient, es como si dijera: «¿Mock? No he oído hablar». Se puede poner un if dentro del servicio y a veces crear CatalogMockClient, pero entonces el servicio se convierte a la vez en lógica de negocio y en fábrica de objetos. Y justo queremos separar responsabilidades, no juntarlas de nuevo.

Segundo, empieza a duplicar la creación de objetos «pesados». HttpClient y ObjectMapper normalmente se crean una vez y se reutilizan. Si cada servicio y cada cliente los crean por separado, obtendrá objetos sobrantes o configuraciones diferentes en distintos lugares. Al principio es imperceptible, pero luego descubre que un ObjectMapper sabe hacer una cosa, otro otra, y usted está desconcertado de por qué el JSON se analiza «a veces».

Tercero, las dependencias ocultas hacen que el código sea ilegible a nivel arquitectónico. Abre CatalogService y ve no solo su sentido, sino también un montón de montaje técnico: creación de HttpClient, creación de ObjectMapper, elección de la implementación del cliente. El servicio deja de ser un servicio y se convierte en una «combinada»: ara, siembra y además se fabrica sus propias piezas.

Cuarto, aparece un «acoplamiento fuerte» a clases concretas. Si el servicio dentro escribe new CatalogHttpClient(...), entonces depende no de una abstracción, sino de una implementación concreta. Esto casi siempre le quita flexibilidad. Incluso si no piensa en pruebas (no nos metemos en el tema de testing), la flexibilidad es necesaria también en el desarrollo normal: modo mock, proveedor alternativo, sustituto temporal, cliente simplificado para demo.

Para fijar el problema, conviene comparar los dos enfoques en una tabla. No es «academicismo», solo una lista de efectos que notará en la práctica.

Enfoque Cómo se ve el código Qué pagamos después
new dentro de la clase (dependencias ocultas) «Todo en un solo lugar» No se puede cambiar la implementación fácilmente, crece la duplicación, el servicio se hincha, la arquitectura es difícil de entender
Dependencias a través del constructor (dependencias explícitas) «La clase dice honestamente lo que necesita» Hay que ensamblar los objetos en algún lugar (esto será la próxima lección), pero la estructura se vuelve transparente

4. Dependencias a través del constructor

Ahora realicemos la operación principal de la lección de hoy: convertir las dependencias ocultas en explícitas. La idea es simple: si CatalogService no puede trabajar sin CatalogClient, que CatalogClient llegue a través del constructor. Así el servicio se vuelve honesto: «Soy un servicio, mi deber es coordinar el comportamiento; el cliente me lo dan desde fuera».

La forma básica se ve así:

package com.example.readlater.catalog.service;

import com.example.readlater.catalog.client.CatalogClient;

public final class CatalogService {
    // Dependencia explícita: el servicio declara que no puede funcionar sin el cliente.
    private final CatalogClient client;

    public CatalogService(CatalogClient client) {
        // Constructor lo más "aburrido" posible: simplemente guardamos la dependencia.
        this.client = client;
    }
}

Desde el punto de vista sintáctico, casi no tiene importancia. Desde el punto de vista estructural, es un gran paso. Ahora CatalogService no elige cómo será el cliente. Simplemente usa lo que le han dado. Esto es «collaboration»: los objetos colaboran, no se crían unos a otros dentro de sí mismos.

Note el final. Es una pequeña pero importante disciplina. Si la dependencia se pasa en el constructor, normalmente no querrá cambiarla durante la vida del objeto. El servicio no debería decidir a mitad del trabajo: «Bueno, ahora trabajaré con otro cliente». Eso parece un giro argumental en una mala serie: inesperado y sin previo aviso.

Exactamente igual actuamos con otras clases. Por ejemplo, la capa de entrada (el controlador de comandos) tampoco debe crear el servicio por sí misma, sino recibirlo a través del constructor:

package com.example.readlater.catalog.command;

import com.example.readlater.catalog.service.CatalogService;

public final class CatalogCommandController {
    // El controlador depende del servicio: es una dependencia normal "hacia arriba en el nivel".
    private final CatalogService catalogService;

    public CatalogCommandController(CatalogService catalogService) {
        // El controlador no "ensambla" el servicio — se lo dan desde fuera.
        this.catalogService = catalogService;
    }
}

Observe lo más fácil que es leer estas clases. Parecen «cables»: quién está conectado con quién se ve de inmediato. Y cuando lee código backend, la mayor parte del tiempo hace exactamente eso: entender quién llama a quién y dónde reside la responsabilidad.

Y un punto importante más. El constructor debe ser lo más aburrido posible. Es un cumplido, no un insulto. Un buen constructor hace dos cosas: recibe dependencias y las guarda. En cuanto el constructor comienza a hacer llamadas de red, leer archivos, analizar JSON o calcular algo complejo, convierte la creación del objeto en una lotería: «se creará o no se creará». En el mundo del backend, es una fuente extra de problemas.

5. Dependencia de la interfaz: el contrato del cliente

Cuando decimos «pase la dependencia a través del constructor», casi de inmediato surge la siguiente pregunta: ¿qué exactamente hay que pasar? ¿La clase concreta CatalogHttpClient? ¿O algo más abstracto? Para nuestro proyecto la respuesta es metodológicamente importante: queremos poder intercambiar la implementación del cliente (real/mock) sin cambiar el servicio.

Por eso el servicio debe depender no de CatalogHttpClient, sino de CatalogClient, la interfaz. La interfaz aquí actúa como un contrato: «sé buscar/obtener detalles». Y cómo exactamente —a través de la red o desde un archivo— no es tan importante para el servicio.

La interfaz más simple podría verse así:

package com.example.readlater.catalog.client;

import com.example.readlater.catalog.dto.normalized.CatalogBookDetails;

public interface CatalogClient {
    // Contrato: al servicio solo le importa "obtener los detalles por un identificador externo".
    // La implementación (HTTP, mock, archivo, caché) no es problema del servicio.
    CatalogBookDetails fetchDetails(String externalId);
}

Y puede haber dos implementaciones. Por ejemplo, la implementación real:

package com.example.readlater.catalog.client;

import com.example.readlater.catalog.dto.normalized.CatalogBookDetails;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.net.http.HttpClient;

public final class CatalogHttpClient implements CatalogClient {
    // Dependencias técnicas del cliente HTTP:
    private final HttpClient httpClient;         // responsable de las peticiones HTTP
    private final ObjectMapper objectMapper;     // responsable de JSON (serialización/deserialización)

    public CatalogHttpClient(HttpClient httpClient, ObjectMapper objectMapper) {
        // Importante: no creamos HttpClient/ObjectMapper internamente — se pasan desde fuera (reutilización y configuración uniforme).
        this.httpClient = httpClient;
        this.objectMapper = objectMapper;
    }

    @Override
    public CatalogBookDetails fetchDetails(String externalId) {
        // Aquí debería ir la implementación de la petición HTTP real y el análisis JSON.
        return null; // la implementación ya se realizó en días anteriores
    }
}

Y la implementación mock:

package com.example.readlater.catalog.mock;

import com.example.readlater.catalog.client.CatalogClient;
import com.example.readlater.catalog.dto.normalized.CatalogBookDetails;

public final class CatalogMockClient implements CatalogClient {
    @Override
    public CatalogBookDetails fetchDetails(String externalId) {
        // Implementación mock: devolvemos datos predecibles sin red.
        return new CatalogBookDetails(externalId, "Mock title", "Mock author");
    }
}

Aquí mantenemos los ejemplos cortos a propósito y no profundizamos en los detalles de HTTP y Jackson — ya se vieron antes. Ahora lo importante es otra cosa: CatalogService no sabe qué cliente concreto le han dado. Trabaja con la interfaz y obtiene el resultado.

Y esto, por cierto, es un hábito muy «adulto». No se trata de testing ni de «patrones por los patrones». Se trata de que en el backend real casi siempre quiere capacidad de sustitución: hoy un proveedor externo, mañana otro; hoy real, mañana mock; hoy un modo simplificado, mañana el completo. La interfaz permite mantener el servicio estable.

6. Cadena de dependencias y dirección de las flechas

Cuando comienza a pasar dependencias a través del constructor, en la cabeza (y en el proyecto) aparece una cadena: un objeto depende de otro, este de un tercero. Y aquí es importante que esta cadena sea direccionada y lógica. Si las dependencias empiezan a ir «en direcciones opuestas», aparecen ciclos, y los ciclos son la versión arquitectónica de los auriculares enredados en el bolsillo: se pueden sacar, pero siempre duele.

Dibujemos una cadena típica para nuestra fase de cliente como esquema. No es «UML oficial», solo un dibujo para que el cerebro lo procese más fácilmente.

flowchart TD
  A["CatalogCommandController"] --> B["CatalogService"]
  B --> C["CatalogClient"]
  C --> D["HttpClient"]
  C --> E["ObjectMapper"]

El sentido es simple: cuanto más cerca del punto de entrada, mayor es el nivel de abstracción. El controlador conoce el servicio, el servicio conoce el cliente, el cliente conoce HttpClient y ObjectMapper. Pero no debe haber una flecha inversa. ObjectMapper no debe conocer a CatalogClient. CatalogClient no debe conocer a CatalogService. Y desde luego no es necesario que el servicio imprima algo directamente en la consola (eso ya es «capa de presentación», vive más cerca de la entrada).

Esta dirección de las flechas hace que el código sea predecible. Abre CatalogService y entiende: «coordina la lógica de aplicación, pero no se ocupa de HTTP». Abre CatalogHttpClient y entiende: «se encarga de HTTP/JSON, pero no decide qué comando se ejecuta». Así tiene una navegación arquitectónica: saber por qué clases buscar un problema.

Es importante notar otro efecto. El enfoque basado en constructor automáticamente «expulsa» la creación de objetos hacia algún lugar externo. Si CatalogService ya no crea al cliente, entonces alguien más debe hacerlo. Pero ese es tema de la próxima lección: composition root y ensamblaje manual del object graph. Ahora simplemente fijamos el patrón: una vez que las dependencias se vuelven explícitas, surge la necesidad de un punto explícito de ensamblaje.

7. Constructor sin magia: final y verificaciones

En la práctica, los desarrolladores principiantes tienen dos extremos. Primero: «el constructor no hace falta, lo crearé todo dentro del método». Segundo: «ya que el constructor es importante, metamos dentro la mitad de la aplicación». Necesitamos un tercer camino: el constructor como lugar donde fijamos las dependencias, hacemos verificaciones mínimas y no ejecutamos lógica de negocio.

Normalmente, un buen hábito es verificar que las dependencias no sean null. No es «paranoia», es protección contra errores muy extraños en los que el objeto aparentemente está creado, pero dentro tiene null y explota en el momento más inesperado. En Java existe Objects.requireNonNull.

Ejemplo:

package com.example.readlater.catalog.service;

import com.example.readlater.catalog.client.CatalogClient;

import java.util.Objects;

public final class CatalogService {
    private final CatalogClient client;

    public CatalogService(CatalogClient client) {
        // Protección mínima: es mejor fallar de inmediato con un mensaje claro
        // que obtener un NPE en medio de un método de negocio.
        this.client = Objects.requireNonNull(client, "client must not be null");
    }
}

Otra disciplina: no pase «todo lo que se le ocurra». A veces uno quiere hacer esto: CatalogService(String baseUrl, int timeoutMs, boolean mockMode, ObjectMapper mapper, HttpClient client, ...). Y esto convierte rápidamente el constructor en una «guía telefónica». Normalmente es una señal de que o ha mezclado responsabilidades, o aún no ha extraído un objeto que agrupe la configuración relacionada (la configuración la trataremos por separado en el Nivel 20, no nos adelantemos).

Y por último: no haga del constructor el lugar donde se inicia el trabajo. A veces se encuentra código como «en el constructor del cliente hacemos inmediatamente una petición para comprobar la disponibilidad del servicio». Suena útil, pero convierte la creación del objeto en una operación con efectos secundarios y posibles caídas de red. Es mejor que el objeto se cree tranquilamente, y las acciones reales se ejecuten en métodos donde esté preparado para manejar errores y devolver un resultado.

8. Errores típicos en la inyección

Error n.º 1: la dependencia supuestamente se pasa a través del constructor, pero dentro igual se crea con new.
Esto sucede a menudo «por costumbre»: ha añadido CatalogClient al constructor, pero luego igualmente ha escrito this.client = new CatalogHttpClient(...) porque «es más fácil». Al final obtiene un doble sentido y pierde los beneficios. La cura es simple: si la dependencia se pasa, use esa misma, no cree una nueva.

Error n.º 2: el constructor empieza a ejecutar lógica de negocio o llamadas de red.
Parece lógico «comprobar inmediatamente que todo funciona», pero en el mundo del backend esto convierte rápidamente la creación del objeto en una fuente de caídas. Si en el constructor ocurre un error de red, el objeto no se creará y obtendrá un caos en tiempo de ejecución inesperado. El constructor debe ser aburrido: recibir, guardar, verificar null.

Error n.º 3: constructor demasiado largo y una decena de parámetros.
Si ve un constructor con 12 argumentos, normalmente no es «sin querer». Es una señal de que la clase tiene demasiadas responsabilidades o ha mezclado datos de llamada (como query) con dependencias (como CatalogClient). En ese caso, es mejor detenerse y preguntar: «¿Esta clase está realmente en su lugar?».

Error n.º 4: la dependencia se pasa, pero no se guarda en un campo final y puede cambiar.
Técnicamente es posible, pero lógicamente casi siempre es perjudicial. Un servicio que de repente cambia de cliente se vuelve impredecible: una llamada hizo la petición en real, la segunda en mock, la tercera a algún otro sitio. Si realmente necesita cambiar de modo, no lo haga «reasignando el campo», sino decidiendo a nivel de elección de implementación al crear el objeto.

Error n.º 5: el servicio depende de una clase concreta en lugar de una interfaz, y real/mock vuelve a ser un dolor.
Si CatalogService recibe un CatalogHttpClient, parece que ha hecho explícitas las dependencias, pero no ha ganado flexibilidad. Es mejor depender de CatalogClient como contrato. Así el servicio no sabe de dónde vienen los datos, y no debe saberlo — no es su trabajo.

1
Tarea
Java Server, nivel 18, lección 3
Bloqueada
Transmisión explícita del cliente a través del constructor
Transmisión explícita del cliente a través del constructor
1
Tarea
Java Server, nivel 18, lección 3
Bloqueada
Paso explícito de `HttpClient` y `ObjectMapper`
Paso explícito de `HttpClient` y `ObjectMapper`
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION