engineering-design-and-analysis
Projetando um motor de relatórios personalizável com o padrão Builder na inicialização de mola Java
Table of Contents
Introdução: A necessidade de um motor de relatório flexível
As aplicações empresariais exigem frequentemente um mecanismo de comunicação que possa adaptar-se aos requisitos de negócio em constante mudança. Um gerador de relatórios estático e codificado rapidamente torna-se uma carga de manutenção quando os stakeholders exigem novas fontes de dados, filtros, formatos de saída ou layouts visuais. O padrão do Construtor, um padrão de design criado a partir do Gang of Four, oferece uma maneira limpa de construir objetos complexos passo a passo, mantendo o processo de construção independente da representação do objeto. Quando aplicado a um motor de comunicação em uma aplicação Java Spring Boot, este padrão permite verdadeira personalização, testabilidade e extensibilidade.
Neste artigo, vamos projetar um motor de relatório a partir do zero, começando com uma classe de núcleo e uma interface flexível . Depois, vamos implementar construtores de concreto, integrá-los como feijão Spring Boot, adicionar um ]Director para modelos de relatório pré-definidos, e discutir considerações do mundo real, como cache, segurança de thread e testes. No final, você terá um projeto pronto para a produção de um subsistema de relatório que pode crescer com o seu negócio.
Entender o padrão do construtor na profundidade
O Padrão do Construtor é muitas vezes confundido com os padrões de Fábrica Abstrata ou Método de Fábrica, mas seu propósito é distinto: ele orienta a construção de um produto passo a passo, permitindo ao cliente escolher quais passos invocar e em que ordem. Um clássico “Padrões de Design” livro exemplo é criar um documento – você pode querer uma versão PDF, uma versão HTML, ou uma versão em texto simples, tudo construído a partir da mesma sequência de passos (adicionar cabeçalho, adicionar parágrafo, adicionar rodapé).
Participantes-chave no padrão:
- Produto – O objeto complexo que está sendo construído (nosso ]).
- Builder – Interface abstrata que define as etapas de construção.
- ConcreteBuilder – Implementa a interface do Construtor, monta o produto e fornece um método para recuperar o resultado.
- Director (opcional) – Orquestra as etapas de construção usando a interface do Construtor, muitas vezes encapsula uma sequência de construção padrão ou frequentemente usada.
Esta separação de preocupações significa que o mesmo processo de construção pode produzir diferentes representações simplesmente trocando o ConcreteBuilder. Para um motor de relatório, isso se traduz em ser capaz de gerar um “relatório sumário” ou um “relatório detalhado” usando a mesma interface ], mas implementações diferentes.
Desenho do motor de comunicação
Nosso motor de relatório será construído em torno do produto e uma interface fluente . Interfaces de Fluente (cadeamento de método) são um ajuste natural para o padrão do construtor e levam a código de cliente legível.
Definição do produto do relatório
A classe contém os dados principais necessários para gerar qualquer relatório. Num sistema real, poderá adicionar campos para cabeçalhos, rodapés, definições de gráficos, subrelatos, etc. Para o nosso exemplo, mantemos o seu foco:
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 o construtor privado. Isso obriga que um só pode ser criado através de um construtor, garantindo que cada instância esteja devidamente configurada.
Criando a Interface do Construtor de Relatórios
A interface do construtor declara os métodos para cada etapa de configuração opcional. Para suportar o encadeamento do método, cada setter retorna em si. Um método final retorna o construído .
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 interface é intencionalmente ampla. Construtores de concreto podem optar por ignorar certos métodos (por exemplo, um simples construtor de relatório sumário pode ignorar ]) ou validar a configuração antes de construir.
Implementação de Construtores de Concreto
Vamos implementar dois construtores para demonstrar flexibilidade: a e a . Ambos implementam a mesma interface, mas produzem diferentes tipos de relatórios.
Construtor de Relatórios Detalhados
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;
}
}
ResumoReportBuilder
Um relatório sumário pode ignorar colunas, filtrar e totais, e, em vez disso, agregar tudo em um único número ou uma tabela simples.
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;
}
}
Com esta abordagem, um cliente pode escolher o construtor que corresponde à complexidade de saída necessária sem alterar a sequência de construção. Esta é a essência do Padrão do Construtor.
Adicionar um Director para Modelos Predefinidos
Muitas vezes, você deseja encapsular sequências de construção comuns. Uma classe de Diretor pode fazer isso:
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();
}
}
O diretor pode ser injetado com qualquer implementação . Isso desvincula o modelo dos detalhes de construção de concreto.
Padrão do construtor na inicialização da mola: fio e uso
A injeção de dependência da Spring Boot facilita o gerenciamento de construtores como feijão e muda-los no tempo de execução.
Passo 1: Definir os construtores como Feijões Primavera
Podemos anotar nossos construtores de concreto com ou declará-los em uma 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);
}
}
Usando escopo é importante: cada chamada para deve criar uma nova instância de construtor com um novo estado interno. Se usássemos escopo singleton, o construtor iria manter o estado de chamadas anteriores, causando bugs.
Para lidar com o fato de que requer um construtor específico, podemos usar ou um padrão de fábrica. Uma abordagem prática é definir vários grãos de diretor, um por tipo de construtor:
@Bean
public ReportDirector detailedReportDirector(@Qualifier("detailedReportBuilder") ReportBuilder builder) {
return new ReportDirector(builder);
}
@Bean
public ReportDirector summaryReportDirector(@Qualifier("summaryReportBuilder") ReportBuilder builder) {
return new ReportDirector(builder);
}
Passo 2: Injetar Construtores/Diretores em Controladores ou Serviços
Um controlador típico pode aceitar um parâmetro de tipo de relatório e usar o componente apropriado:
@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, você pode injetar construtores diretamente e deixar a camada de serviço escolher. O ponto chave: o código do cliente nunca sabe sobre os internos do construtor – ele apenas chama ou um método diretor.
Personalização avançada: Construtores dinâmicos com o fornecedor de objetos da primavera
Às vezes, a seleção do construtor deve acontecer em tempo de execução com base em propriedades de configuração ou funções do usuário. Spring’s pode ajudar a injetar um protótipo de feijão vagamente:
@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 padrão evita ter de pré-fiar todos os possíveis construtores diretamente, mantendo o código limpo e testável.
Garantir Imutabilidade e Segurança do Rolo
O produto deve ser imutável após a construção. Como os construtores são normalmente usados em um único thread e não são compartilhados, não precisamos sincronizar o próprio builder. No entanto, se você planeja reutilizar um builder entre threads (não recomendado), certifique-se de que o builder não tenha estado mutável compartilhado.
Para impor a imutabilidade, torne a classe verdadeiramente imutável:
- Marcar todos os campos como .
- Passe todos os valores através do construtor (o construtor chama um construtor privado que define tudo).
- Fornecer apenas getters, sem setters.
- Para as colecções (por exemplo, colunas), fazer cópias defensivas no construtor ou usar .
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...
}
Então o cria o através deste construtor completo, passando todos os valores recolhidos. Isso garante que uma vez construído, o relatório não pode ser alterado.
Testar o motor de comunicação
O padrão do construtor torna os testes simples porque você pode injetar construtores simulados ou construtores específicos para testes. Para testes unitários do produto , você pode instanciá-lo diretamente usando um construtor. Para testes de integração, você pode verificar se o construtor correto é chamado e que o relatório final atende às expectativas.
Unidade Testando um Construtor de 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
}
Teste com Mocks
Ao testar um serviço que usa um construtor, zombe da interface do construtor para verificar interações:
@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();
}
Considerações de desempenho e cache
Construir um objeto em si é barato – é apenas a montagem de dados de configuração. A parte cara é executar a consulta subjacente, transformando dados e gerando o arquivo de saída (PDF, XLSX). Portanto, o construtor não deve ativar qualquer E/S. Essa responsabilidade pertence a um serviço separado ou similar.
Se a mesma configuração do relatório for solicitada repetidamente (por exemplo, o mesmo relatório de vendas mensal para a mesma região), você pode armazenar o objeto (a configuração) e reutilizá-lo. Para cachear, você pode usar o Spring no método diretor ou método de serviço. Como o ] é imutável, é seguro guardar sem cópias defensivas.
@Cacheable("reportConfigs")
public Report getMonthlySalesConfig(String region) {
return detailedReportDirector.constructMonthlySalesReport(region);
}
O cache da configuração permite que o construtor execute apenas uma vez por conjunto distinto de parâmetros, acelerando as solicitações subsequentes mesmo antes da execução da consulta.
Comparando o padrão do construtor com outras abordagens
Ao projetar um motor de relatório, você pode considerar outros padrões:
- Factory Method – Bom para criar um objeto de relatório em um passo, mas não suporta configuração passo-a-passo.
- Construtor com muitos parâmetros – Construtores de telescopia são propensas a erros e difíceis de ler. O padrão do Construtor fornece um estilo claro e denominado de parâmetro.
- Padrão JavaBeans (setters mutáveis) – Permite configuração passo a passo, mas quebra a imutabilidade e pode levar a objetos parcialmente inicializados.
- Padrão Estratégico – Pode ser combinado com o Construtor; o construtor poderia aceitar uma estratégia para renderização ou coleta de dados.
O padrão Builder se destaca quando o produto tem muitos componentes opcionais, como um relatório. Ele também suporta o Princípio Aberto/Fechado – você pode adicionar novos tipos de relatório implementando um novo construtor sem alterar o código existente.
Extensões do Mundo Real
Um motor de relatórios de produção precisa muitas vezes de mais do que configuração simples. Considere estas extensões:
- Construtores aninhados para subnotificações – cada subnotificação pode ter seu próprio construtor.
- Um conceito – construtores pré-configurados armazenados em uma base de dados ou arquivos YAML.
- Integração com o Spring Cloud Config para alterar modelos de relatórios sem reinstalação.
- Usando Anotação de Lombok para gerar automaticamente a classe de construtor. Tenha cuidado: Lombok gera um construtor estático aninhado, que pode não permitir construtores polimórficos para diferentes tipos de relatórios. Para o nosso propósito, os construtores personalizados dão mais controle.
Por exemplo, um modelo baseado em YAML pode ser carregado:
monthly-sales:
title: "Monthly Sales - ${region}"
dataSource: "jdbc/sales"
query: "SELECT ..."
columns: ["Product", "Units Sold"]
outputFormat: "PDF"
showTotals: true
Um serviço poderia analisar este modelo e chamar os métodos de construção apropriados, tornando o motor de comunicação totalmente orientado para dados.
Conclusão
O Padrão do Construtor, quando aplicado a um motor de relatórios de inicialização de molas Java, fornece uma separação limpa entre a construção de configurações de relatórios e sua representação. Ao definir uma interface fluente e implementar vários construtores de concreto, você permite a personalização dinâmica e em tempo de execução de relatórios sem acumular dívida técnica. A classe opcional de Diretor encapsula sequências comumente usadas, e a injeção de dependência da Spring torna trivial a ligação de tudo.
Este design não é apenas extensível – você pode adicionar novos tipos de relatórios escrevendo um novo construtor –, mas também testável, porque os construtores são objetos Java simples que podem ser zombados ou instanciados em isolamento. Combinado com produtos imutáveis e cache, o motor permanece performante e seguro.
Quer esteja a construir um painel de bordo simples ou uma plataforma de inteligência empresarial completa, o padrão do Construtor dá-lhe a flexibilidade para satisfazer os requisitos em evolução, mantendo uma base de códigos que é um prazer trabalhar com. Para mais leitura, consulte a documentação oficial Framework Primavera sobre escopos de feijoeiros e o clássico Livro de padrões de design[] para mais contexto sobre padrões de criação.