Introducción: La necesidad de un motor de presentación de informes flexible

Las aplicaciones empresariales requieren con frecuencia un motor de reportaje que puede adaptarse a los requisitos de negocio siempre cambiantes. Un generador de informes estático y codificado rápidamente se convierte en una carga de mantenimiento cuando los interesados exigen nuevas fuentes de datos, filtros, formatos de salida o diseños visuales. El patrón de constructor, un patrón de diseño creacional de la Gang of Four, ofrece una manera limpia de construir objetos complejos, independiente al mantener el proceso de construcción

En este artículo diseñaremos un motor de reportaje desde el suelo, comenzando con una clase núcleo y una interfaz flexible . Luego implementaremos constructores concretos, los integraremos como frijoles de Spring Boot, añadiremos un Director] para plantillas de informes predefinidas, y discutiremos consideraciones reales como la producción de subching, la seguridad de rosular, y el finalización de tu negocio.

Comprender el patrón del constructor en profundidad

El patrón de constructores se confunde con los patrones de la fábrica abstracta o de la fábrica, pero su propósito es distinto: guía la construcción de un producto paso a paso, permitiendo al cliente elegir qué pasos a invocar y en qué orden. Un libro clásico "Patrones de diseño" ] ejemplo está creando un documento – usted podría querer una versión PDF, una versión HTML, o una versión de texto plano.

Los participantes clave en el patrón:

  • Producto] – El objeto complejo que se construye (nuestro ).
  • Builder] – Interfaz abstracta que define los pasos de construcción.
  • ConcreteBuilder] – Implementa la interfaz de Builder, monta el producto y proporciona un método para recuperar el resultado.
  • Director (opcional) – Orquesta los pasos de construcción utilizando la interfaz de Builder, a menudo encapsula una secuencia de construcción predeterminada o a menudo usada.

Esta separación de preocupaciones significa que el mismo proceso de construcción puede producir diferentes representaciones simplemente intercambiando el ConcreteBuilder. Para un motor de presentación de informes, esto se traduce en la posibilidad de generar un “informe de resumen” o un “informe detallado” utilizando la misma interfaz pero diferentes implementaciones.

Diseño del motor de presentación de informes

Nuestro motor de reporte se construirá alrededor del producto y una interfaz fluida . interfaces fluidas (encadenamiento de metod) son un ajuste natural para el patrón de Builder y conducen a un código cliente legible.

Definir el producto del informe

La clase contiene los datos básicos necesarios para generar cualquier informe. En un sistema real puede agregar campos para encabezados, pies, definiciones de gráficos, subreportaciones, etc. Por nuestro ejemplo, lo mantenemos enfocado:

public class Report {
 private String title;
 private String dataSource; // e.g., "jdbc/myDb" or "file:/data.csv"
 private String query; // SQL or a query identifier
 private List<String> columns; // columns to display
 private Filter filter; // complex filter object
 private String outputFormat; // PDF, CSV, XLSX, HTML
 private boolean showTotals;

 // private constructor – only builders create instances
 private Report() {}

 // Builder inner class or external – we'll use an external builder
 // Getters (no setters after construction) – omitted for brevity
 public String getTitle() { return title; }
 public String getDataSource() { return dataSource; }
 // etc.
}

Observe al constructor privado. Esto impone que un sólo puede ser creado a través de un constructor, asegurando que cada instancia esté correctamente configurado.

Creación de la interfaz de ReportBuilder

La interfaz de constructor declara métodos para cada paso de configuración opcional. Para apoyar la encadenamiento de métodos, cada elemento vuelve en sí mismo. Un método final devuelve el método construido .

public interface ReportBuilder {
 ReportBuilder setTitle(String title);
 ReportBuilder setDataSource(String dataSource);
 ReportBuilder setQuery(String query);
 ReportBuilder setColumns(List<String> columns);
 ReportBuilder setFilter(Filter filter);
 ReportBuilder setOutputFormat(String outputFormat);
 ReportBuilder showTotals(boolean showTotals);
 Report build();
}

Esta interfaz es intencionalmente amplia. Los constructores de hormigón pueden optar por ignorar ciertos métodos (por ejemplo, un simple editor de informes resumidos podría ignorar ) o validar la configuración antes de construir.

Implementación de Concretos

Implementemos dos constructores para demostrar flexibilidad: un y un . Ambos implementan la misma interfaz pero producen diferentes tipos de informes.

Información detalladaAsociador

public class DetailedReportBuilder implements ReportBuilder {
 private Report report = new Report();

 @Override
 public ReportBuilder setTitle(String title) {
 report.setTitle(title);
 return this;
 }

 @Override
 public ReportBuilder setDataSource(String dataSource) {
 report.setDataSource(dataSource);
 return this;
 }

 @Override
 public ReportBuilder setQuery(String query) {
 report.setQuery(query);
 return this;
 }

 @Override
 public ReportBuilder setColumns(List<String> columns) {
 report.setColumns(columns);
 return this;
 }

 @Override
 public ReportBuilder setFilter(Filter filter) {
 report.setFilter(filter);
 return this;
 }

 @Override
 public ReportBuilder setOutputFormat(String outputFormat) {
 report.setOutputFormat(outputFormat);
 return this;
 }

 @Override
 public ReportBuilder showTotals(boolean showTotals) {
 report.setShowTotals(showTotals);
 return this;
 }

 @Override
 public Report build() {
 // Validate critical fields
 if (report.getDataSource() == null) {
 throw new IllegalStateException("DataSource must be set");
 }
 // Additional validation logic...
 return report;
 }
}

SummaryReportBuilder

Un informe resumido podría ignorar columnas, filtros y totales, y en lugar de agregar todo en un solo número o una tabla simple.

public class SummaryReportBuilder implements ReportBuilder {
 private String title;
 private String dataSource;
 private String query;
 // other fields are ignored or given defaults

 @Override
 public ReportBuilder setTitle(String title) {
 this.title = title;
 return this;
 }

 @Override
 public ReportBuilder setDataSource(String dataSource) {
 this.dataSource = dataSource;
 return this;
 }

 @Override
 public ReportBuilder setQuery(String query) {
 this.query = query;
 return this;
 }

 // All other setter methods either do nothing or throw UnsupportedOperationException
 @Override
 public ReportBuilder setColumns(List<String> columns) {
 return this; // summary report ignores columns
 }

 // ... similar for filter, outputFormat, showTotals

 @Override
 public Report build() {
 Report report = new Report();
 report.setTitle(title);
 report.setDataSource(dataSource);
 report.setQuery(query);
 report.setOutputFormat("CSV"); // default format
 return report;
 }
}

Con este enfoque, un cliente puede elegir el constructor que coincide con la complejidad de salida requerida sin cambiar la secuencia de construcción. Esta es la esencia del patrón de constructor.

Añadiendo un Director para Plantillas Predefinidas

A menudo quieres encapsular secuencias de construcción comunes. Una clase de director puede hacer esto:

public class ReportDirector {
 private final ReportBuilder builder;

 public ReportDirector(ReportBuilder builder) {
 this.builder = builder;
 }

 public Report constructMonthlySalesReport(String region) {
 return builder
 .setTitle("Monthly Sales – " + region)
 .setDataSource("jdbc/sales_db")
 .setQuery("SELECT * FROM sales WHERE region = :region")
 .setColumns(List.of("Product", "Units Sold", "Revenue"))
 .setFilter(new DateFilter(LocalDate.now().minusMonths(1), LocalDate.now()))
 .setOutputFormat("PDF")
 .showTotals(true)
 .build();
 }

 public Report constructQuickSummary() {
 return builder
 .setTitle("Quick Summary")
 .setDataSource("jdbc/sales_db")
 .setQuery("SELECT count(*) as cnt, sum(revenue) as total FROM sales")
 .setOutputFormat("CSV")
 .build();
 }
}

El Director puede ser inyectado con cualquier implementación. Esto descifra la plantilla de los detalles de construcción concretos.

Patrón de constructor en botín de primavera: cableado y uso

La inyección de dependencia de Spring Boot hace que sea fácil administrar los constructores como frijoles y cambiarlos en el tiempo de ejecución.

Paso 1: Define a los constructores como los frijoles de primavera

Podemos anotar a nuestros constructores de hormigón con o declararlos en una clase :

@Configuration
public class ReportConfig {

 @Bean
 @Scope("prototype") // because each builder session uses a fresh instance
 public DetailedReportBuilder detailedReportBuilder() {
 return new DetailedReportBuilder();
 }

 @Bean
 @Scope("prototype")
 public SummaryReportBuilder summaryReportBuilder() {
 return new SummaryReportBuilder();
 }

 @Bean
 @Scope("prototype")
 public ReportDirector reportDirector(ReportBuilder builder) {
 // This bean will not resolve without specifying the builder – we'll discuss later
 return new ReportDirector(builder);
 }
}

Utilizar es importante: cada llamada a debe crear una nueva instancia de constructor con un estado interno fresco. Si usamos el alcance de un solotón, el constructor conservaría el estado de llamadas anteriores, causando errores.

Para manejar el hecho de que requiere un constructor específico, podemos utilizar o un patrón de fábrica. Un enfoque práctico es definir varios frijoles de director, uno por tipo de constructor:

@Bean
public ReportDirector detailedReportDirector(@Qualifier("detailedReportBuilder") ReportBuilder builder) {
 return new ReportDirector(builder);
}

@Bean
public ReportDirector summaryReportDirector(@Qualifier("summaryReportBuilder") ReportBuilder builder) {
 return new ReportDirector(builder);
}

Paso 2: Inyecte a los constructores/directores en los controladores o servicios

Un controlador típico puede aceptar un parámetro tipo informe y utilizar el componente adecuado:

@RestController
@RequestMapping("/reports")
public class ReportController {

 @Autowired
 private ReportDirector detailedReportDirector;

 @Autowired
 private ReportDirector summaryReportDirector;

 @GetMapping("/monthly/{region}")
 public ResponseEntity<Report> getMonthlySales(@PathVariable String region) {
 Report report = detailedReportDirector.constructMonthlySalesReport(region);
 // Execute report generation logic...
 return ResponseEntity.ok(report);
 }

 @GetMapping("/summary")
 public ResponseEntity<Report> getSummary() {
 Report report = summaryReportDirector.constructQuickSummary();
 return ResponseEntity.ok(report);
 }
}

Alternativamente, podría inyectar a los constructores directamente y dejar que la capa de servicio elija. El punto clave: el código cliente nunca sabe sobre los internos del constructor – simplemente llama o un método de director.

Personalización avanzada: Constructores dinámicos con Proveedor de Objetos de Primavera

A veces la selección del constructor debe ocurrir en tiempo de ejecución basado en las propiedades de configuración o los roles del usuario. La primavera puede ayudar a inyectar un prototipo de frijol de la pereza:

@Service
public class ReportService {

 private final ObjectProvider<DetailedReportBuilder> detailedBuilderProvider;
 private final ObjectProvider<SummaryReportBuilder> summaryBuilderProvider;

 public ReportService(ObjectProvider<DetailedReportBuilder> detailedBuilderProvider,
 ObjectProvider<SummaryReportBuilder> summaryBuilderProvider) {
 this.detailedBuilderProvider = detailedBuilderProvider;
 this.summaryBuilderProvider = summaryBuilderProvider;
 }

 public Report generateReport(String type, Map<String, String> params) {
 ReportBuilder builder;
 if ("detailed".equalsIgnoreCase(type)) {
 builder = detailedBuilderProvider.getObject();
 } else {
 builder = summaryBuilderProvider.getObject();
 }
 // Apply common params (e.g., title, dataSource)
 String title = params.getOrDefault("title", "Report");
 builder.setTitle(title)
 .setDataSource(params.get("dataSource"));
 // Build
 return builder.build();
 }
}

Este patrón evita tener que pre-wire cada posible constructor directamente, mientras que mantiene el código limpio y testable.

Asegurar la inmutabilidad y la seguridad del pan

El producto debe ser inmutable después de la construcción. Debido a que los constructores se utilizan típicamente en un solo hilo y no son compartidos, no necesitamos sincronizar el propio constructor. Sin embargo, si usted planea reutilizar un constructor a través de los hilos (no recomendado), asegúrese de que el constructor no tiene estado compartido mutable.

Para hacer cumplir la inmutabilidad, hacer la clase verdaderamente inmutable:

  • Marca todos los campos como .
  • Pase todos los valores a través del constructor (el constructor llama a un constructor privado que lo establece todo).
  • Proveer sólo los comedores, sin niñeras.
  • Para colecciones (por ejemplo, columnas), haga copias defensivas en el constructor o utilice .
public class Report {
 private final String title;
 private final String dataSource;
 private final String query;
 private final List<String> columns;
 private final Filter filter;
 private final String outputFormat;
 private final boolean showTotals;

 Report(String title, String dataSource, String query,
 List<String> columns, Filter filter,
 String outputFormat, boolean showTotals) {
 this.title = title;
 this.dataSource = dataSource;
 this.query = query;
 this.columns = columns == null ? List.of() : List.copyOf(columns);
 this.filter = filter;
 this.outputFormat = outputFormat;
 this.showTotals = showTotals;
 }
 // getters...
}

Entonces el crea el a través de este constructor completo, pasando todos los valores reunidos. Esto garantiza que una vez construido, el informe no puede ser alterado.

Pruebas del motor de reportaje

El patrón de constructor hace pruebas directas porque puede inyectar constructores de mock o constructores específicos para pruebas. Para pruebas de unidad del producto , puede instantáneamente utilizar directamente un constructor. Para pruebas de integración, puede verificar que el constructor correcto se llama y que el informe final cumple con las expectativas.

Unidad de prueba de un concreto

@Test
void testDetailedReportBuilder() {
 DetailedReportBuilder builder = new DetailedReportBuilder();
 Report report = builder
 .setTitle("Test")
 .setDataSource("jdbc/test")
 .setOutputFormat("PDF")
 .build();

 assertThat(report.getTitle()).isEqualTo("Test");
 assertThat(report.getDataSource()).isEqualTo("jdbc/test");
 assertThat(report.getOutputFormat()).isEqualTo("PDF");
 assertThat(report.isShowTotals()).isFalse(); // default
}

Pruebas con mocks

Cuando se prueba un servicio que utiliza un constructor, se burla de la interfaz del constructor para verificar las interacciones:

@Test
void testReportServiceUsesBuilderCorrectly() {
 ReportBuilder mockBuilder = mock(ReportBuilder.class);
 when(mockBuilder.setTitle(any())).thenReturn(mockBuilder);
 when(mockBuilder.setDataSource(any())).thenReturn(mockBuilder);
 // ... other stubs
 Report expectedReport = new Report(/* ... */);
 when(mockBuilder.build()).thenReturn(expectedReport);

 ReportService service = new ReportService(/* ... */);
 // inject mockBuilder via a test specific method
 Report result = service.generateReport("detailed", Map.of("title", "Test", "dataSource", "jdbc/db"));

 assertThat(result).isSameAs(expectedReport);
 verify(mockBuilder).setTitle("Test");
 verify(mockBuilder).setDataSource("jdbc/db");
 verify(mockBuilder).build();
}

Consideraciones de la actuación profesional y la caché

La construcción de un objeto en sí es barato, es simplemente asimilar datos de configuración. La parte cara es ejecutar la consulta subyacente, transformar datos y generar el archivo de salida (PDF, XLSX). Por lo tanto, el constructor no debe desencadenar ningún I/O. Esa responsabilidad pertenece a un servicio separado o similar.

Si se solicita la configuración del mismo informe repetidamente (por ejemplo, el mismo informe mensual de ventas para la misma región), puede cachear el objeto (la configuración) y reutilizarlo. Para el caché puede utilizar el de Spring en el método de administración o método de servicio. Debido a que es inmutable, es seguro cache sin copias defensión.

@Cacheable("reportConfigs")
public Report getMonthlySalesConfig(String region) {
 return detailedReportDirector.constructMonthlySalesReport(region);
}

Caching la configuración permite al constructor ejecutar sólo una vez por conjunto de parámetros distintos, acelerando las solicitudes posteriores incluso antes de la ejecución de la consulta.

Comparando el patrón del constructor con otros enfoques

Al diseñar un motor de reportaje, usted podría considerar otros patrones:

  • Método de fábrica] – Bien por crear un objeto de informe en un paso, pero no admite la configuración paso a paso.
  • Constructor con muchos parámetros] – Los constructores telescópicos son propensas a errores y difícil de leer. El patrón Builder proporciona un estilo claro, llamado parametrómetro.
  • Patrón de javabeans (setters desmontables)] – Permite una configuración gradual pero rompe la inmutabilidad y puede llevar a objetos parcialmente inicializados.
  • Patrón de Estadio] – Podría combinarse con Builder; el constructor podría aceptar una estrategia para la reproducción o búsqueda de datos.

El patrón de Builder se destaca cuando el producto tiene muchos componentes opcionales, como un informe. También soporta el Principio Abierto/Cerrado – puede añadir nuevos tipos de informe mediante la implementación de un nuevo constructor sin alterar el código existente.

Extensiones en el mundo real

Un motor de presentación de informes de producción a menudo necesita más que una configuración simple. Considere estas extensiones:

  • Los constructores anidados para subreportos – cada subreport puede tener su propio constructor.
  • Un concepto – constructores pre-configurados almacenados en una base de datos o archivos YAML.
  • Integración con Spring Cloud Config para cambiar plantillas de reporte sin redistribuir.
  • Usando La anotación de Lombok ] para autogenerar la clase constructora. Tenga cuidado: Lombok genera un constructor anidado estático, que puede no permitir que los constructores polimorficos para diferentes tipos de informes. Para nuestro propósito, los constructores personalizados dan más control.

Por ejemplo, se podría cargar una plantilla basada en YAML:

monthly-sales:
 title: "Monthly Sales - ${region}"
 dataSource: "jdbc/sales"
 query: "SELECT ..."
 columns: ["Product", "Units Sold"]
 outputFormat: "PDF"
 showTotals: true

Un servicio podría analizar esta plantilla y llamar a los métodos apropiados de construcción, haciendo que el motor de presentación de informes se dirija plenamente de datos.

Conclusión

El patrón de constructor, cuando se aplica a un motor de presentación de datos de Java Spring Boot, proporciona una separación limpia entre la construcción de configuraciones de informes y su representación. Al definir una interfaz fluida e implementar múltiples constructores de hormigón, permite la personalización dinámica y de tiempo de ejecución de informes sin acumular deuda técnica. La clase de director opcional encapsula secuencias de uso común, y la inyección de dependencia de Spring hace que sea trivial para conectar todo.

Este diseño no sólo es extensible – se pueden añadir nuevos tipos de informes escribiendo un nuevo constructor – sino también testable, porque los constructores son objetos Java simples que pueden ser burlados o instantáneamente en aislamiento. Combinados con productos inmutables y caché, el motor sigue siendo performant y seguro.

Ya sea que usted está construyendo un panel de control simple o una plataforma de inteligencia empresarial de pleno derecho, el patrón de Builder le da la flexibilidad para cumplir con requisitos cambiantes manteniendo una base de código con la que trabajar. Para más lectura, vea el Especto de la documentación Marco sobre los alcances de los frijoles y el clásico