Introduzione: La necessità di un motore di segnalazione flessibile

Le applicazioni aziendali richiedono spesso un motore di report che può adattarsi ai requisiti aziendali in continuo cambiamento. Un generatore di report statico, in codice duro diventa rapidamente un onere di manutenzione quando gli stakeholder richiedono nuove fonti di dati, filtri, formati di output o layout visivi. Il modello di Costruttore, un modello di design creatore dall’applicazione Gang of Four[Ftensi:1], offre un modo pulito per costruire oggetti complessi passo dopo passo, mantenendo il processo di costruzione

In questo articolo disegnamo un motore di report da terra, a partire da una classe core e un'interfaccia flessibile . In seguito implementeremo dei costruttori di cemento, li integreremo come fagioli di primavera, aggiungiamo un Direttore] per i modelli di report predefiniti, e discuteremo considerazioni reali come il caching, la sicurezza del file e il test blu.

Capire il modello del costruttore in profondità

Il modello del costruttore è spesso confuso con i modelli di fabbrica astratto o metodo di fabbrica, ma il suo scopo è diverso: guida la costruzione di un prodotto passo dopo passo, permettendo al cliente di scegliere quali passi per invocare e in quale ordine. Un classico “Design Patterns” libro[] esempio è la creazione di un documento – si potrebbe desiderare una versione PDF, una versione HTML, o una versione a testo normale, tutti i passaggi a piedi costruiti

I partecipanti chiave nel modello:

  • Product[] – L'oggetto complesso che viene costruito (il nostro ]).
  • Builder[] – Interfaccia astratta che definisce i passi di costruzione.
  • ConcreteBuilder[[] – Implementa l'interfaccia del Costruttore, assembla il prodotto e fornisce un metodo per recuperare il risultato.
  • Direttore[] (opzionale) – Orchestra i passi dell'edificio utilizzando l'interfaccia del Costruttore, spesso incapsula una sequenza di costruzione predefinita o spesso utilizzata.

Questa separazione delle preoccupazioni significa che lo stesso processo di costruzione può produrre rappresentazioni diverse semplicemente scambiando il ConcreteBuilder. Per un motore di segnalazione, questo si traduce in essere in grado di generare un “relazione di bilancio” o un “reso di dettaglio” utilizzando la stessa interfaccia ma implementazioni diverse.

Progettazione del motore di segnalazione

Il nostro motore di report sarà costruito intorno al prodotto [] e un'interfaccia fluente . Le interfacce fluide (catenazione del metallo) sono una misura naturale per il modello del costruttore e portano al codice client leggibile.

Definizione del prodotto di relazione

La classe contiene i dati fondamentali necessari per generare qualsiasi report. In un sistema reale si potrebbero aggiungere campi per intestazioni, piè di pagina, definizioni dei grafici, sottoreporti, ecc. Per il nostro esempio lo teniamo concentrato:

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.
}

Si noti il costruttore privato. Ciò fa osservare che un può essere creato solo attraverso un costruttore, assicurando che ogni istanza sia configurata correttamente.

Creazione dell'interfaccia di ReportBuilder

Per supportare la catena di metodi, ogni setter ritorna stesso. Un metodo finale restituisce il costruito ].

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();
}

Questa interfaccia è intenzionalmente ampia. I costruttori di cemento possono scegliere di ignorare alcuni metodi (ad esempio, un semplice costruttore di report di sintesi potrebbe ignorare ) o convalidare la configurazione prima di costruire.

Realizzazione di Costruzioni di calcestruzzo

Mettiamo in atto due costruttori per dimostrare flessibilità: un e un . Entrambi implementano la stessa interfaccia ma producono diversi tipi di report.

Dettaglio del costo del trasporto

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;
 }
}

RiepilogoReportBuilder

Un report di sintesi potrebbe ignorare colonne, filtri e totali, e invece aggregare tutto in un unico numero o in una semplice tabella.

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 questo approccio, un cliente può scegliere il costruttore che corrisponde alla complessità di uscita richiesta senza cambiare la sequenza di costruzione.

Aggiungere un Direttore per i modelli predefiniti

Spesso si desidera incapsulare sequenze di costruzione comuni. Una classe di direttore può fare questo:

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();
 }
}

Il Direttore può essere iniettato con qualsiasi implementazione. Questo decouples il modello dai dettagli di costruzione in cemento.

Modello del costruttore in primavera Boot: cablaggio e uso

L'iniezione di dipendenza di Spring Boot rende facile gestire i costruttori come fagioli e passarli a runtime.

Passo 1: Definire i costruttori come fagioli di primavera

Possiamo annotare i nostri costruttori di cemento con ] o dichiararli in una classe :

@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);
 }
}

L'uso di è importante: ogni chiamata a [] dovrebbe creare un nuovo istanza di costruttore con uno stato interno fresco. Se usiamo la portata di singleton, il costruttore manterrebbe lo stato dalle chiamate precedenti, causando bug.

Per gestire il fatto che richiede un costruttore specifico, possiamo usare o un modello di fabbrica.

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

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

Fase 2: Iniettare i costruttori/direttori in controller o servizi

Un tipico controller potrebbe accettare un parametro tipo report e utilizzare il componente appropriato:

@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);
 }
}

In alternativa, è possibile iniettare i costruttori direttamente e lasciare che lo strato di servizio scelga. Il punto chiave: il codice client non conosce mai gli interni dei costruttori – basta chiamare o un metodo di direttore.

Personalizzazione avanzata: Costruttori dinamici con ObjectProvider di primavera

A volte la selezione dei costruttori deve avvenire in runtime in base alle proprietà di configurazione o ai ruoli degli utenti.

@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();
 }
}

Questo modello evita di dover pre-wire ogni possibile costruttore direttamente, mantenendo comunque il codice pulito e testabile.

Garantire la sicurezza e la sicurezza dei filetti

Poiché i costruttori sono generalmente utilizzati in un unico thread e non sono condivisi, non abbiamo bisogno di sincronizzare il costruttore stesso. Tuttavia, se si prevede di riutilizzare un costruttore attraverso i fili (non consigliato), assicurarsi che il costruttore non ha uno stato condiviso mutabile.

Per far rispettare l'immutabilità, rendere la classe veramente immutabile:

  • Segna tutti i campi come .
  • Passare tutti i valori attraverso il costruttore (il costruttore chiama un costruttore privato che imposta tutto).
  • Fornire solo i contatori, niente divani.
  • Per le collezioni (ad esempio, colonne), fare copie difensive nel costruttore o utilizzare .
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...
}

Poi il crea il [] attraverso questo costruttore completo, passando tutti i valori raccolti, che garantisce che una volta costruito, il rapporto non può essere modificato.

Testare il motore di segnalazione

Per i test unitari del prodotto , è possibile istantanelo direttamente utilizzando un costruttore. Per i test di integrazione, è possibile verificare che il corretto costruttore sia chiamato e che il rapporto finale soddisfi le aspettative.

Test di unità un Costruttore di calcestruzzo

@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
}

Prova con i Mocks

Quando si verifica un servizio che utilizza un costruttore, selezionare l'interfaccia del costruttore per verificare le interazioni:

@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();
}

Prestazioni e Caching

La costruzione di un oggetto è economicamente conveniente – è semplicemente assemblare i dati di configurazione. La parte costosa sta eseguendo la query sottostante, trasformando i dati e generando il file di output (PDF, XLSX). Pertanto, il costruttore non dovrebbe innescare alcun I/O. Tale responsabilità appartiene ad un servizio separato o simile.

Se la stessa configurazione del report viene richiesta ripetutamente (ad esempio, lo stesso rapporto mensile di vendita per la stessa regione), è possibile memorizzare l’oggetto [ (la configurazione) e riutilizzarlo. Per il caching è possibile utilizzare la cache sul metodo di direttore o sul metodo di servizio.

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

Caching la configurazione consente al costruttore di eseguire solo una volta per set di parametri distinti, velocizzando le richieste successive anche prima dell'esecuzione delle query.

Confrontare il modello del costruttore con altri approcci

Quando si progetta un motore di segnalazione, si potrebbe considerare altri modelli:

  • Metodo di fabbrica[[] – Buon per creare un oggetto di report in un passo, ma non supporta la configurazione passo-saggio.
  • Constructor con molti parametri[[] – I costruttori di telescoping sono pronivi di errore e difficili da leggere.
  • JavaBeans pattern (setteri variabili)[] – Consente la configurazione a senso passo ma rompe l'immutabilità e può portare a oggetti parzialmente inizializzati.
  • Strategy Pattern[[] – Potrebbe essere combinato con Builder; il costruttore potrebbe accettare una strategia per la rendering o la raccolta dei dati.

Il modello Builder eccelle quando il prodotto ha molti componenti opzionali, come un report, e supporta anche il principio aperto/caso – è possibile aggiungere nuovi tipi di report implementando un nuovo costruttore senza alterare il codice esistente.

Estensioni reali

Un motore di report di produzione spesso ha bisogno di una configurazione più semplice.

  • Culturisti nidi per sottoreporti – ogni sottoreporte può avere un proprio costruttore.
  • Un concetto – costruttori preconfigurati memorizzati in un database o file YAML.
  • Integrazione con Spring Cloud Config per modificare i modelli di report senza ridicolizzare.
  • Usando Lombok []] annotazione per auto-generare la classe costruttore. Attenzione: Lombok genera un costruttore statico nidificata, che non può consentire costruttori polimorfici per diversi tipi di report.

Ad esempio, un modello basato su YAML potrebbe essere caricato:

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

Un servizio potrebbe analizzare questo modello e chiamare i metodi di costruttore appropriati, rendendo il motore di report completamente dati-driven.

Conclusioni

Il modello Builder, applicato a un motore di segnalazione Java Spring Boot, fornisce una separazione pulita tra la costruzione di configurazioni di report e la loro rappresentazione. Definindo un'interfaccia fluente [] e implementando più costruttori di cemento, si abilita la personalizzazione dinamica e runtime dei rapporti senza accumulare debito tecnico. La classe Director opzionale incorpora sequenze comunemente utilizzate, e l'iniezione di dipendenza di primavera lo rende banale per collegare tutto.

Questo design non è solo estensivo – si possono aggiungere nuovi tipi di report scrivendo un nuovo costruttore – ma anche testabile, perché i costruttori sono oggetti Java semplici che possono essere mocked o istantanati in isolamento.

Se state costruendo un semplice cruscotto o una piattaforma di business intelligence a pieno titolo, il modello Builder vi dà la flessibilità di soddisfare i requisiti in evoluzione, mantenendo un codebase con cui lavorare. Per ulteriori informazioni, vedere il libro ufficiale .Principa la documentazione quadro sugli ambiti dei fanghi e il classico ]Design Patterns book