CodeGym /Cursos /Spring Core /Modos dev, demo, test

Modos dev, demo, test

Spring Core
Nivel 14, Lección 0
Disponible

1. Una base - modos diferentes

Prácticamente cualquier aplicación no vive en un único «mundo ideal», sino en varios. La ejecutamos localmente para ver rápidamente qué sucede, ejecutamos una versión demo para mostrar el comportamiento a compañeros o al cliente, y también ejecutamos un modo de prueba donde deseamos determinismo y ausencia de efectos secundarios. Esto no es un lujo, sino una forma de no volverse loco incluso en un proyecto pequeño.

Tomemos nuestro ContextFlow. En la vida real, deseamos que el inicio local (dev) sea lo más simple posible: todo visible en la consola, sin necesidad de ajustes de archivos, nada se «pierde». Para el modo demo (demo) se desea otro comportamiento: por ejemplo, escribir la auditoría en un archivo para que queden artefactos, y dejar las notificaciones en la consola para no depender de sistemas externos. Para el modo de prueba (test) se desea reglas aún más estrictas: la generación de identificadores debe ser predecible, las notificaciones deben estar desactivadas o reemplazadas por no-op, y las operaciones de archivo deben ser las más seguras y no extenderse por el disco.

Para que esto se sienta concreto, es útil tener primero un esbozo aproximado de los modos. No es una configuración definitiva, sino un mapa preliminar: ahora es importante ver qué cambia de un modo a otro.

Modo Lo importante en este despliegue Qué comportamiento esperamos
dev ver rápidamente qué hace la aplicación máxima observabilidad, mínima molestia
demo mostrar el proceso y dejar artefactos comportamiento visible más resultados «tangibles» como un archivo de auditoría
test repetibilidad y ausencia de ruido valores deterministas y mínimos efectos secundarios

El punto clave aquí es el siguiente: los modos no deben cambiar el significado del proceso de negocio. Un pedido se crea y se cancela de la misma manera. Solo cambia cómo «observamos» el proceso y qué efectos externos permitimos en ese modo. Es decir, no se trata de una lógica de dominio diferente, sino de diferentes condiciones infraestructurales del mismo escenario.

2. Mala opción: «modo» en el servicio de negocio

Cuando un desarrollador se enfrenta por primera vez a «necesito tres modos», el camino más natural es agregar una variable mode en el código de negocio y tener una fiesta de if-else. Parece rápido e incluso funciona... exactamente hasta que el proyecto crece más allá de una clase. Después de eso, if-else comienza a multiplicarse y, como cualquier organismo vivo, intenta sobrevivir en cada servicio.

Imaginemos que hemos creado un servicio de notificaciones «inteligente» y lo hemos entrenado para entender el modo de funcionamiento. A primera vista es conveniente: en test simplemente no enviamos nada. A segunda vista, el servicio pasó a depender no solo de NotificationSender, sino también de en qué modo se ensambló el contenedor. Es decir, tenemos conocimiento sobre el entorno dentro de la capa de negocio.

import com.example.contextflow.domain.ports.NotificationSender;

public class NotificationDispatchService {
    private final NotificationSender sender;
    // Mal: el servicio comienza a conocer el entorno (dev/demo/test), y no solo la tarea de dominio
    private final String mode;

    public NotificationDispatchService(NotificationSender sender, String mode) {
        // Importante: sender es una dependencia correcta (puerto)
        this.sender = sender;
        // Mal: mode es una dependencia de configuración "oculta" de la capa de negocio
        this.mode = mode;
    }

    public void notifyOrderCreated(String message) {
        // El "modo silencioso" en pruebas parece conveniente,
        // pero mezcla la lógica de negocio con los modos de compilación de la aplicación
        if ("test".equals(mode)) return;
        sender.send(message);
    }
}

El problema aquí no es el if en sí mismo. El problema es que el servicio de negocio repentinamente se volvió «dependiente del perfil». Ya no puedes leer el constructor y entender qué es esta clase: ¿se trata de enviar notificaciones o de ensamblado condicional de entornos? Y lo peor es que empiezas a repetir este patrón en otros lugares. Y muy pronto tienes if ("demo") en la auditoría, if ("test") en la generación de informes, if ("dev") en el registro... y vuelves al mismo dolor con el que comenzaste el curso: dependencias ocultas y bootstrap que se expande, solo que ahora está escondido dentro de servicios «normales».

Hay un dolor de ingeniería más sutil. En algún momento los modos no son tres, sino cuatro o cinco, y cada nueva rama es una nueva oportunidad de romper el comportamiento accidentalmente. El código comienza a parecer un «bosque de condiciones», y en un bosque, como es sabido, es fácil perderse. Especialmente si eres junior y aún no tienes la súper habilidad de mantener un gran contexto en la mente (lo cual es normal: el cerebro también tiene scope y lifecycle).

3. Mala opción: if-else en el código de wiring

El siguiente paso natural después de darse cuenta de «no en los servicios de negocio» es mover if-else a la configuración. Esto es realmente mejor: al menos el código de negocio vuelve a estar «limpio», y la selección de implementación ocurre en un solo lugar. Pero hay un matiz: si pones una condición dentro de un método @Bean, todavía estás codificando los modos como lógica manual, no como un modelo declarativo de ensamblado del contenedor.

Un ejemplo a menudo se ve así: leemos una propiedad y, dependiendo de ella, devolvemos una implementación diferente.

import com.example.contextflow.domain.ports.AuditWriter;
import com.example.contextflow.infrastructure.audit.ConsoleAuditWriter;
import com.example.contextflow.infrastructure.audit.FileAuditWriter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

@Configuration
class AuditConfig {
    @Bean
    AuditWriter auditWriter(Environment env) {
        // Leemos la configuración de Environment (normalmente de application.properties / application.yml)
        String mode = env.getProperty("contextflow.audit.mode", "console");

        // Importante: la elección de la implementación se realiza manualmente mediante una condición,
        // por lo que la configuración se convierte en un "mini programa"
        return "file".equalsIgnoreCase(mode) ? new FileAuditWriter() : new ConsoleAuditWriter();
    }
}

¿Por qué esto sigue sin ser «lo correcto», aunque formalmente funciona? Porque estamos convirtiendo la configuración en un mini programa con ramificación y una matriz oculta de modos. Hoy hay una condición, mañana hay tres, pasado mañana empiezas a depender de combinaciones de valores, y tu capa de configuración se convierte en un pequeño «intérprete de condiciones». En esta etapa, los principiantes a menudo tienen la idea: «¿Quizás hacer un mega-config y poner 20 if-else?». Se puede. Incluso se ejecutará. Pero leerlo será más doloroso que la documentación de BeanFactoryPostProcessor (y ni siquiera hemos llegado allí todavía).

Y otro matiz importante: cuando decides mediante if-else dentro de @Bean, el contenedor sigue viendo una definición de bean auditWriter. No ve las variantes alternativas como entidades separadas que se pueden activar/desactivar. Debido a esto, el diagnóstico y la comprensión de «qué configuración está activa actualmente» se vuelven menos transparentes.

Es decir, «condición en la configuración» es un paso de transición normal, especialmente si tienes un proyecto muy pequeño, pero queremos un modelo más escalable, basado en contenedor. Y aquí es donde entran en escena los perfiles.

4. Ensamblado con perfiles

Un perfil en Spring es una forma de decirle al contenedor: «En este modo, registra unos beans, y en otro, otros». La palabra importante aquí es registra. Los perfiles no funcionan como un «interruptor de comportamiento de un objeto ya creado», sino como un filtro en la etapa de ensamblado de ApplicationContext. El contenedor esencialmente obtiene la capacidad de ensamblar diferentes «configuraciones» de la misma aplicación.

Es útil tener en mente una imagen simple: primero Spring lee los metadatos sobre los beans (BeanDefinition), luego decide cuáles participan en el ensamblado, y solo entonces crea objetos y los conecta. Los perfiles se intercalan precisamente en el momento de «qué definiciones de beans tomamos en consideración».

Aquí hay un esquema simplificado. Para esto solo necesitamos un hecho: el filtrado ocurre antes de la creación de objetos.

flowchart TD
    %% Los perfiles funcionan en la etapa "qué definiciones de bean participan en el ensamblado"
    A["Inicio ApplicationContext"] --> B["Lectura y registro de bean definitions"]
    B --> C{"¿Perfil coincide?"}
    C -->|sí| D["Conservamos bean definition"]
    C -->|no| E["Saltamos bean definition"]
    D --> F["Creamos objetos y hacemos wiring"]
    F --> G["Aplicación lista para funcionar"]

Los perfiles son la respuesta a la pregunta: cómo hacer que la misma aplicación pueda tener diferentes implementaciones infraestructurales, sin obligar al código de negocio a conocer los modos. Si has visto el esbozo de modos anterior, los perfiles son precisamente el mecanismo que permite al contenedor en test no registrar el notificador «real», y en demo conectar la auditoría de archivos.

Por ahora no entremos en la sintaxis de @Profile; aquí es más importante fijar la idea: el perfil es una característica del ensamblado del contenedor, no un estado del modelo de dominio. El perfil no debe «vivir» en Order.status y no debe transmitirse a través de la cadena de métodos. Vive en Environment del contenedor y afecta a qué beans aparecen en el contexto.

5. Perfiles y propiedades

Una confusión muy común entre los principiantes: «ya que podemos leer configuraciones de properties, ¿para qué necesitamos perfiles?». La respuesta es porque son niveles diferentes de configuración. Las properties establecen valores, los perfiles cambian la composición de la aplicación. Es como la diferencia entre «de qué color es el coche» y «si el coche tiene motor o es una bicicleta». Ambos son transporte, pero el nivel de decisión es muy diferente.

Veamos un ejemplo simple de configuración por propiedad: ReportOutputManager tiene una ruta donde escribir los informes. Ese valor es realmente conveniente tenerlo en .properties y cambiarlo sin recompilar.

import org.springframework.beans.factory.annotation.Value;

public class ReportOutputManager {
    private final String outputDir;

    public ReportOutputManager(@Value("${contextflow.report.output-dir}") String outputDir) {
        // Las propiedades configuran los parámetros de la implementación ya seleccionada
        this.outputDir = outputDir;
    }
}

Aquí el contenedor crea exactamente el mismo bean, solo con un valor diferente de outputDir en diferentes entornos. Y esto está bien.

Ahora imagina otra tarea: en test no queremos escribir archivos de auditoría en absoluto. No «escribir en otro directorio», sino «no escribir archivo como concepto». Queremos reemplazar toda la implementación de AuditWriter. Aquí las properties se convierten en una herramienta torpe: intentarás hacer contextflow.audit.mode=none, y luego escribir if-else en AuditWriter o en la configuración. Esto nos devuelve a los puntos 2-3.

Para no mezclar estos dos mundos, es útil tener una tabla comparativa:

Pregunta Properties Profiles
¿Qué configuramos? valores dentro de los beans conjunto de beans y sus implementaciones
¿Cuándo afecta? al crear el bean (inyección de valores) antes de crear los beans (filtrado de registro)
¿Dónde almacenar la lógica? en la configuración de valores en la configuración de ensamblado
Ejemplo típico ruta output-dir, nombre de la aplicación ConsoleAuditWriter vs FileAuditWriter, UuidIdGenerator vs Deterministic

Y aquí hay una conclusión importante. Una buena configuración se parece a una cocina bien organizada. Las especias (properties) están en el estante y se pueden cambiar cada día. Pero la estufa, el horno y el microondas (profiles) son el «equipamiento de la cocina»: no quieres montar el horno a partir de especias cada vez. Además, con especias no puedes reemplazar el horno, por mucho que lo intentes.

6. Modos de ContextFlow: dev, demo, test

Ahora aterricemos todo en nuestro proyecto sin entrar en anotaciones específicas. Ahora es más importante ver qué problema de ingeniería resuelven los perfiles en ContextFlow y por qué lo hacen a nivel de contenedor.

En ContextFlow ya tenemos las premisas correctas: los servicios de aplicación dependen de puertos (interfaces), y esto significa que podemos cambiar las implementaciones sin reescribir los servicios. Por ejemplo, NotificationDispatchService no tiene por qué saber que en el perfil de prueba las notificaciones están desactivadas. Él llama honestamente a sender.send(...), y el contenedor en test inserta NoOpNotificationSender.

Así es como debería verse dicho servicio en un universo saludable:

import com.example.contextflow.domain.ports.NotificationSender;
import org.springframework.stereotype.Service;

@Service
public class NotificationDispatchService {
    // Importante: el servicio depende del puerto (interfaz), no de una implementación concreta
    private final NotificationSender sender;

    public NotificationDispatchService(NotificationSender sender) {
        // El contenedor insertará el bean sender adecuado según el perfil de ensamblado
        this.sender = sender;
    }

    public void notifyOrderCreated(String message) {
        // El método de negocio se mantiene "limpio": sin mode/env/if-else sobre el entorno
        sender.send(message);
    }
}

Observa lo que no hay: no hay mode, no hay Environment, no hay «si test, devuélvete». El servicio hace exactamente su trabajo, y el contenedor decide qué NotificationSender se considera «actual» en este modo de ensamblado.

Lo mismo con la auditoría. AuditService o cualquier otra capa que escriba auditoría, no debe tener lógica interna como «en dev imprime, en demo escribe archivo». Debe depender de AuditWriter, y el perfil determina qué AuditWriter está registrado. En este sentido, los perfiles son una forma de fijar una disciplina arquitectónica: la capa de negocio no conoce el entorno, el entorno gestiona el ensamblado.

Para ContextFlow es suficiente tener en mente una conexión simple: dev y demo son necesarios para la observabilidad, test para la previsibilidad. A nivel de puertos, esto significa lo siguiente: en dev el contenedor selecciona las implementaciones más visibles, como auditoría de consola y notificaciones de consola; en demo mantiene la misma simplicidad, pero puede conservar artefactos como un archivo de auditoría en build/; en test inserta un OrderIdGenerator determinista, notificaciones no-op y los efectos secundarios más seguros.

Observa cuán orgánicamente se integran los perfiles en nuestro dominio. No estamos inventando una «razón didáctica», estamos describiendo necesidades reales de desarrollo: comodidad del inicio local, comodidad de la demostración y comodidad de las comprobaciones predecibles. Solo queda formalizar esto como un ensamblado normal a nivel de contenedor: sin if-else en los servicios y sin convertir la configuración en una novela condicional de 300 páginas.

7. Errores típicos al trabajar con perfiles

Error nº1: percibir el perfil como un estado de negocio.
Cuando profile comienza a pasarse a través de parámetros de métodos o se guarda en objetos de dominio (order.setProfile("demo")), es una señal de que el límite se ha roto. El perfil no es parte de la lógica de negocio, sino un mecanismo de configuración del contenedor. Debe determinar qué beans se crean, no cómo se comporta un pedido concreto.

Error nº2: resolver los modos mediante if-else dentro de los servicios.
A primera vista es simple: agregaste if (mode.equals("demo")) y todo funciona. Pero con eso estás mezclando la lógica de negocio con el entorno. La clase deja de ser transparente: su comportamiento depende no solo de los datos de entrada, sino también del estado externo. DI precisamente permite sacar esa condicionalidad al ensamblado del contenedor y mantener los servicios limpios.

Error nº3: crear un perfil para cada ajuste pequeño.
Cuando aparecen profile=notifications-on, profile=notifications-off, profile=audit-file, profile=audit-console, el sistema rápidamente se vuelve inmanejable. El perfil se trata del modo de funcionamiento (conjunto de beans), no de banderas individuales. Las diferencias pequeñas deben vivir en properties, no inflar la cantidad de perfiles.

Error nº4: mezclar profiles, properties y qualifiers sin un modelo explícito.
Si parte de las decisiones se toman mediante @Profile, parte mediante @Value, parte mediante @Qualifier, en algún momento se vuelve incomprensible quién es responsable de qué. La configuración se convierte en un «lugar mágico» donde todo afecta a todo. Una buena regla: el perfil selecciona la implementación, las properties configuran su comportamiento.

Error nº5: esperar que el perfil se pueda «cambiar sobre la marcha».
Los perfiles se aplican en la etapa de inicialización de ApplicationContext. Si esperas que se pueda cambiar el perfil durante la ejecución de la aplicación y que todo se reestructure, ese es un modelo incorrecto. Para cambios en tiempo de ejecución existen otros mecanismos (feature flags, properties), pero no profiles.

1
Tarea
Spring Core, nivel 14, lección 0
Bloqueada
Ensamblaje del modo fuera del servicio de negocio
Ensamblaje del modo fuera del servicio de negocio
1
Tarea
Spring Core, nivel 14, lección 0
Bloqueada
La propiedad cambia el valor, no la implementación
La propiedad cambia el valor, no la implementación
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION