CodeGym /Cursos /Java Server /Punto de montaje: wiring y grafo de objetos

Punto de montaje: wiring y grafo de objetos

Java Server
Nivel 18, Lección 4
Disponible

1. Dependencias y punto de montaje

Si ayer su servicio podía escribir «a escondidas» dentro de sí mismo new HttpClient() y new ObjectMapper(), hoy tratamos de no hacerlo así. Y esto es correcto: la clase empieza a parecerse a un componente adulto normal, no a un armario autoensamblable de la tienda «Monta tú mismo». Pero todo componente adulto tiene una cara oculta — requiere que alguien lo cree y lo conecte con los demás.

Aquí es donde los principiantes suelen experimentar una pequeña disonancia cognitiva. Por un lado, decimos: «No creen dependencias dentro de las clases, páselas a través del constructor». Por otro lado, igualmente tenemos que escribir new en alguna parte. No hay escapatoria: Java todavía no sabe materializar CatalogService de la nada con el poder de la mente (aunque algunos IDEs se comportan como si lo supieran).

Por lo tanto, surge la idea clave de esta lección: la aplicación debe tener un lugar claro donde se “permita” ensamblar objetos y conectarlos en un sistema funcional. A este lugar se le llama composition root (raíz de composición) o, en lenguaje más coloquial, punto de montaje de la aplicación.

Composition root y new

Composition root no es un patrón mágico del libro «Arquitectura para los que aman sufrir». Es una disciplina muy simple: todo el “cableado” entre objetos vive en un solo lugar, generalmente cerca del punto de entrada (en nuestro caso — en el paquete app). Entonces abre un archivo y ve inmediatamente de qué partes grandes está compuesta la aplicación y quién llama a quién.

Es útil pensarlo así: el código de negocio (servicios, repositorios, clientes) debe ser como electrodomésticos — hacen su trabajo. Y composition root es el cuadro eléctrico donde se conectan los electrodomésticos a la red y entre sí. Los electrodomésticos no deben tender cables por la casa por sí mismos. De lo contrario, será divertido, pero no por mucho tiempo.

Para ser más concretos, fijemos una regla simple en una tabla. No se trata de "belleza", sino de dónde buscarás errores y con qué rapidez podrás cambiar el comportamiento de la aplicación.

Lugar en el proyecto Qué está permitido allí Por qué
com.example.readlater.app Crear objetos mediante new, elegir implementaciones (mock/real), conectar todo en un esquema funcional Es la «zona de montaje»: aquí se ve el panorama completo
catalog.service, readinglist.service Hacer trabajo de aplicación, llamar dependencias, pero no crearlas internamente De lo contrario, el servicio se vuelve no reutilizable y se «pega» a una implementación específica
catalog.client, readinglist.repository Implementación de una dependencia concreta (HTTP, almacenamiento) Aquí está el “hardware”, pero no el “cuadro eléctrico”
dto Solo forma de datos El DTO no debe saber quién y dónde lo crea

Esta regla no le prohíbe hacer new ReadingStatus(...) (aunque el enum de todos modos no se crea). Se trata específicamente de dependencias de infraestructura y de aplicación: HttpClient, ObjectMapper, CatalogClient, CatalogService, controladores/adaptadores de la capa de entrada.

2. Object graph: conexiones de la aplicación

Cuando ha ensamblado los objetos y los ha conectado, no obtiene un «montón de instancias», sino un grafo de objetos (object graph). La palabra suena como algo matemático, pero en realidad es simplemente la respuesta a la pregunta: «¿Qué objetos viven en la memoria y quién se refiere a quién a través de campos?». Si entiende este grafo, entiende la aplicación a nivel de mecánica, no a nivel de esperanza.

¿Por qué es útil ahora mismo, en un proyecto simple? Porque su cerebro aún es capaz de mantener todo el grafo en la cabeza. Es un lujo poco común. En la vida real, el grafo se vuelve tan grande que sin disciplina se convierte en «espaguetis con referencias». Y queremos que, incluso sin Spring, la aplicación sea legible.

Dibujemos (simplificado) object graph para la parte del cliente de ReadLater Starter:

flowchart TD
    A["ReadLaterApplication main"] --> B["CommandRouter"]
    B --> C["CatalogCommandController"]
    C --> D["CatalogService"]
    D --> E["CatalogClient"]
    E --> F["HttpClient"]
    E --> G["ObjectMapper"]

Aquí son importantes dos ideas. Primero, la dirección de las conexiones se lee de arriba abajo: desde la entrada a la lógica de aplicación y luego a la infraestructura. Segundo, HttpClient y ObjectMapper son «ladrillos» de bajo nivel que generalmente es ventajoso crear una vez y reutilizar, en lugar de crear una instancia para cada llamada.

3. Antipatrón: ensamblaje en todas partes

La mayoría de las veces, el código no se degrada porque alguien sea malvado y quiera caos. Se degrada porque «así es más rápido ahora mismo». El escenario más típico: ha extraído un fragmento de código de main() a CatalogService, pero dentro del servicio dejó la creación de dependencias. Formalmente, «dividió en clases». Pero en realidad, simplemente escondió el caos más profundo para que no lo incomode con su apariencia.

import java.net.http.HttpClient;

public final class CatalogService {
    // Mal: el servicio elige por sí mismo la implementación y la forma de crear la dependencia
    // (desde fuera no se ve y es difícil de sustituir en los tests).
    private final CatalogClient client =
            new CatalogHttpClient(HttpClient.newHttpClient(), ObjectMapperFactory.create()); // dependencias ocultas
}

Desde fuera no está claro que el servicio use HTTP. Peor aún: usted no puede reemplazar rápidamente CatalogHttpClient por CatalogMockClient porque el servicio mismo decidió por usted qué cliente le gusta. Es como si el hervidor eligiera su propio enchufe en la pared y, si es necesario, moviera un poco los muebles.

Ya discutimos la opción correcta: al servicio se le «da» la dependencia, no se le «incrusta» dentro.

public final class CatalogService {
    private final CatalogClient client;

    public CatalogService(CatalogClient client) {
        // Bien: la dependencia viene de fuera (desde el composition root)
        this.client = client;
    }
}

Y entonces aparece el siguiente paso lógico: ya que el servicio no crea CatalogClient por sí mismo, alguien debe crear CatalogClient y pasárselo a CatalogService. Ese es el trabajo del composition root.

4. Opción n.º 1: main() como punto de montaje

A veces los estudiantes tienen la sensación de que «ensamblar en main() es una mala práctica». En realidad, no es ni mala ni buena. Simplemente es muy honesta: ves qué se crea, en qué orden, y quién se pasa a quién. Para un proyecto pequeño es incluso una ventaja: menos archivos, menos saltos por el código.

La condición principal: main() no debe ejecutar lógica de negocio, no debe hacer llamadas HTTP ni analizar JSON. Debe crear los objetos necesarios y pasar el control más adelante.

import java.net.http.HttpClient;

public final class ReadLaterApplication {
    public static void main(String[] args) {
        // Infraestructura de bajo nivel — normalmente se crea una vez por ejecución
        HttpClient httpClient = HttpClient.newHttpClient();

        // Mapper JSON común — también parte de la infraestructura, no es necesario replicarlo por todo el código
        var objectMapper = ObjectMapperFactory.create();

        // Wiring: ensamblamos el grafo de arriba abajo (o de abajo arriba — como le sea más claro)
        var catalogClient = new CatalogHttpClient(httpClient, objectMapper);
        var catalogService = new CatalogService(catalogClient);
        var controller = new CatalogCommandController(catalogService);

        // Punto de transferencia de control: a continuación trabaja la capa de entrada de la aplicación
        new CommandRouter(controller).run(args);
    }
}

Observe un detalle importante: aquí no hay «lógica de catálogo», solo wiring. Si se sorprende pensando «y si aquí mismo analizamos args y hacemos search», significa que main() ha empezado a hincharse de nuevo y es hora de sacar el routing a CommandRouter (que es lo que hacemos).

Pero hay un inconveniente honesto: en cuanto se añadan nuevas partes (readinglist, utilidades comunes, otro comando), main() empezará a crecer. Y entonces querrá la segunda opción.

5. Opción n.º 2: clase ApplicationBootstrap

Cuando main() empieza a parecerse a una lista de compras semanal (todo parece importante, pero es pesado de leer), extraemos el ensamblaje a una clase separada que se ocupa solo de eso. A menudo se llama Bootstrap, Assembler, ApplicationFactory — el nombre no es tan importante. Lo importante es que vive en el paquete app y sigue siendo el único punto donde conectamos los componentes grandes.

public final class ReadLaterApplication {
    public static void main(String[] args) {
        // Bootstrap se encarga solo del ensamblaje y la conexión de objetos
        ApplicationBootstrap bootstrap = new ApplicationBootstrap();

        // Router es ya la “capa de entrada” de la aplicación, que recibe entrada externa
        CommandRouter router = bootstrap.createRouter();

        // Pasamos el control más adelante
        router.run(args);
    }
}

Ahora toda la «cocina» está escondida no dentro de los servicios (lo cual es malo), sino en un lugar especialmente designado (lo cual es bueno). Y está escondida no por magia, sino por código Java común — puede abrir ApplicationBootstrap y verlo todo.

import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.http.HttpClient;

public final class ApplicationBootstrap {
    // Dependencias comunes de bajo nivel: se crean una vez por ejecución
    private final HttpClient httpClient = HttpClient.newHttpClient();

    // También es conveniente mantener el mapper como común: misma configuración para toda la aplicación
    private final ObjectMapper objectMapper = ObjectMapperFactory.create();
}

Aquí es importante no caer en el extremo de «hacer todo static». Los campos de bootstrap son normales: es solo un objeto que se crea al inicio y contiene los «detalles comunes de ensamblaje». Aún no son singletons globales, es una posesión explícita normal de dependencias.

6. Grafo: catalog search y catalog details

Ahora armemos las piezas en un esquema más completo, cercano a nuestro proyecto. Seguiremos la lógica de «abajo arriba»: primero creamos las cosas de bajo nivel (HttpClient, ObjectMapper), luego el cliente del catálogo, luego el servicio, luego la capa de entrada (controlador de comandos), luego el router.

import com.fasterxml.jackson.databind.ObjectMapper;

public final class ObjectMapperFactory {
    private ObjectMapperFactory() { }

    public static ObjectMapper create() {
        // Único lugar donde se configura ObjectMapper.
        // Si más adelante se necesitan módulos/ajustes — agréguelos aquí.
        return new ObjectMapper();
    }
}

Ahora — wiring para el catálogo. El paquete superior app lo dejamos para ReadLaterApplication, CommandRouter y ApplicationBootstrap, y el código de entrada de la propia feature lo mantenemos junto a ella — en catalog.command. A continuación nos importa el corte de wiring: no repite toda la lógica de CLI, sino que muestra quién recibe qué a través del constructor.

public final class CatalogCommandController {
    private final CatalogService catalogService;

    public CatalogCommandController(CatalogService catalogService) {
        // Importante: el controlador recibe la dependencia desde fuera, no la crea
        this.catalogService = catalogService;
    }

    public void search(String query) {
        // El controlador es delgado: solo delega en la capa de aplicación
        catalogService.search(query);
    }
}

Observe: el controlador aún no imprime ni formatea nada. Esto es normal si en su proyecto la salida está organizada de otra manera. El sentido del ejemplo es que la capa de entrada es delgada y no crea dependencias.

import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.http.HttpClient;

public final class ApplicationBootstrap {
    // Reutilizamos dependencias de bajo nivel
    private final HttpClient httpClient = HttpClient.newHttpClient();
    private final ObjectMapper objectMapper = ObjectMapperFactory.create();

    public CatalogCommandController createCatalogController() {
        // Aquí el «conmutador» de implementaciones: se puede sustituir el cliente HTTP por un mock
        CatalogClient client = new CatalogHttpClient(httpClient, objectMapper);

        // El servicio recibe el cliente a través del constructor (dependencia explícita)
        CatalogService service = new CatalogService(client);

        // El controlador recibe el servicio a través del constructor
        return new CatalogCommandController(service);
    }
}

Ya se parece a un ensamblaje normal: se ve el orden, se ven las conexiones, se ve dónde está el «conmutador» de implementación (en esta versión siempre CatalogHttpClient, pero bootstrap es el lugar donde se puede elegir CatalogMockClient sin meterse en los servicios).

Y aquí se ve bien por qué plain Java obliga a pagar con trabajo manual: nosotros mismos elegimos la implementación real/mock, nosotros mismos creamos objetos de infraestructura comunes como HttpClient y ObjectMapper, nosotros mismos los pasamos a través de los constructores hasta el lugar necesario. Más tarde, Spring container recopilará este object graph por nosotros, pero los roles y dependencias no desaparecerán. Si el servicio necesita un cliente y el cliente necesita un mapper, igualmente tenemos que pensarlo nosotros — solo sin el boilerplate manual de wiring.

Queda conectar esto con el router de comandos. El router es parte del paquete app porque es el nivel de entrada de toda la aplicación (no es «sobre el catálogo», es «sobre el inicio de la aplicación»).

import java.util.Arrays;

public final class CommandRouter {
    private final CatalogCommandController catalog;

    public CommandRouter(CatalogCommandController catalog) {
        // Router recibe el/los controlador(es) — son sus dependencias de la capa de entrada
        this.catalog = catalog;
    }

    public void run(String[] args) {
        // Validación mínima de entrada: si hay pocos argumentos, no hacemos nada
        if (args.length < 3) return;

        // Routing: elegimos el comando y delegamos en el controlador adecuado
        if ("catalog".equals(args[0]) && "search".equals(args[1])) {
            // Recopilamos query de los argumentos restantes
            String q = String.join(" ", Arrays.copyOfRange(args, 2, args.length));
            catalog.search(q);
        }
    }
}

Aquí hay deliberadamente un mínimo de lógica. El router no hace HTTP, no mapea JSON — solo recibe la entrada externa (args) y la dirige más adelante. Sí, por ahora solo sabe hacer catalog search. Puede ampliarlo de manera similar a catalog details, pero incluso en esta forma se ve bien: el routing vive en un solo lugar, no está disperso por los servicios.

public final class ApplicationBootstrap {
    public CommandRouter createRouter() {
        // Ensamblamos la capa de entrada de la aplicación a partir de piezas ya ensambladas
        return new CommandRouter(createCatalogController());
    }
}

Y todo junto en la mente se convierte en una historia comprensible: ReadLaterApplication crea el bootstrap, el bootstrap crea el router, el router llama al controlador, el controlador llama al servicio, el servicio llama al cliente. Exactamente el object graph que dibujamos en el diagrama.

7. Errores típicos en el wiring manual

Error n.º 1: parte del grafo se crea en bootstrap y otra parte — «tranquilamente» dentro de los servicios.
Es la ruptura de disciplina más insidiosa. Desde fuera parece: «bueno, ya hemos hecho el bootstrap». Pero si dentro de CatalogService todavía vive new CatalogHttpClient(...), en realidad ha creado dos composition root: uno visible y otro oculto. Como resultado, aparece un comportamiento impredecible, duplicación de HttpClient e imposibilidad de reemplazar honestamente la implementación de la dependencia.

Error n.º 2: el ensamblaje se duplica en varios lugares «porque es más rápido así».
Hoy ha ensamblado CatalogService en ReadLaterApplication, mañana en ApplicationBootstrap, pasado mañana en algún CatalogUtils.createService(). Funcionará, pero el proyecto empezará a comportarse como una hidra: cambió el constructor — y de repente hay que corregir tres lugares diferentes, de lo contrario parte de los modos de ejecución se romperán. Un grafo — un punto de ensamblaje.

Error n.º 3: convertir bootstrap en un lugar de lógica de negocio.
Bootstrap no es un «servicio de servicios». No debe decidir cómo buscar libros o cómo manejar errores. Su tarea es ensamblar objetos. Si se sorprende de que en bootstrap hayan aparecido condiciones como «si la consulta está vacía, devolvemos un error», significa que la lógica de negocio se ha ido a donde no debe. El ensamblaje debe ser corto y aburrido. Un bootstrap aburrido es señal de salud del proyecto.

Error n.º 4: campos static globales como «DI rápido».
A veces dan ganas de hacer public static final ObjectMapper MAPPER = ... y relajarse. Sí, es rápido. Pero luego querrá tener diferentes configuraciones del mapper o reemplazarlo en un escenario de prueba, y de repente resultará que todo está clavado en un solo lugar. En el proyecto de aprendizaje entrenamos el hábito de dependencias explícitas: si un objeto es necesario, que llegue a través del constructor y se cree en el composition root.

Error n.º 5: crear «por si acaso» todo, incluso lo que no se necesita en esta ejecución.
Lo bueno del ensamblaje manual es que puede ver el coste de cada new. Si crea un montón de objetos que no se usan en el comando actual, o bien desperdicia recursos o (peor) activa efectos secundarios desapercibidos. Un buen ensamblaje no tiene por qué ser perezoso o astuto, pero debe ser consciente: usted entiende para qué se creó cada objeto.

1
Tarea
Java Server, nivel 18, lección 4
Bloqueada
Un composition root para el comando de búsqueda
Un composition root para el comando de búsqueda
1
Tarea
Java Server, nivel 18, lección 4
Bloqueada
Selección de implementación en `ApplicationBootstrap`
Selección de implementación en `ApplicationBootstrap`
1
Cuestionario/control
Arquitectura de la aplicación, nivel 18, lección 4
No disponible
Arquitectura de la aplicación
Capas, dependencias y composición
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION