Разработка настраиваемого механизма отчетности с шаблоном строителя в Java Spring Boot
Введение: необходимость гибкой системы отчетности
Корпоративные приложения часто требуют движка отчетности, который может адаптироваться к постоянно меняющимся бизнес-требованиям. Статический, жестко закодированный генератор отчетов быстро становится бременем обслуживания, когда заинтересованные стороны требуют новых источников данных, фильтров, выходных форматов или визуальных макетов. Шаблон конструктора, шаблон креационного проектирования из Gang of Four , предлагает чистый способ построения сложных объектов шаг за шагом, сохраняя при этом процесс строительства независимым от представления объекта. При применении к движку отчетности в приложении Java Spring Boot, этот шаблон обеспечивает истинную настраиваемость, проверяемость и расширяемость.
В этой статье мы разработаем механизм отчетности с нуля, начиная с базового класса и гибкого интерфейса . Затем мы реализуем бетонные конструкторы, интегрируем их в бобы Spring Boot, добавим ]Директор для предварительно определенных шаблонов отчетов и обсудим реальные соображения, такие как кэширование, безопасность потоков и тестирование. К концу у вас будет готовый к производству план подсистемы отчетности, которая может расти с вашим бизнесом.
Понимание шаблона строителя в глубине
Паттерн конструктора часто путают с шаблонами абстрактной фабрики или метода фабрики, но его цель различна: он направляет конструкцию продукта шаг за шагом, позволяя клиенту выбирать, какие шаги вызывать и в каком порядке. Классический пример книги «Паттеры проектирования» «Паттеры проектирования» — создание документа — вам может понадобиться версия PDF, версия HTML или версия с простым текстом, все построено из одной и той же последовательности шагов (добавить заголовок, добавить абзац, добавить нижний колонтитул).
Ключевые участники в шаблоне:
- Производство — строящийся сложный объект (наш ).
- Строитель — Абстрактный интерфейс, определяющий этапы строительства.
- Конкретный конструктор — Внедряет интерфейс конструктора, собирает продукт и предоставляет способ извлечения результата.
- Директор (необязательно) — оркеструет этапы строительства с использованием интерфейса Строителя, часто инкапсулирует по умолчанию или часто используемую последовательность строительства.
Это разделение проблем означает, что один и тот же процесс строительства может создавать различные представления просто путем замены ConcreteBuilder. Для двигателя отчетности это означает возможность генерировать «общий отчет» или «детальный отчет» с использованием одного и того же интерфейса , но разных реализаций.
Проектирование системы отчетности
Наш механизм отчетности будет построен вокруг продукта и свободного интерфейса . Свободные интерфейсы (методическая цепочка) являются естественными для шаблона строителя и приводят к читаемому клиентскому коду.
Определение продукта отчета
Класс содержит основные данные, необходимые для создания любого отчета. В реальной системе вы можете добавить поля для заголовков, нижних колонок, определений диаграмм, подотчетов и т. Д. В нашем примере мы сохраняем его сосредоточенным:
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.
}
Обратите внимание на частного строителя. Это означает, что может быть создан только через строителя, гарантируя, что каждый экземпляр правильно настроен.
Создание интерфейса ReportBuilder
Интерфейс конструктора объявляет способы для каждого факультативного этапа конфигурации. Для поддержки цепной цепи метода каждый установщик возвращает сам . Окончательный метод возвращает построенный .
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();
}
Конкретные строители могут игнорировать определенные методы (например, простой конструктор сводных отчетов может игнорировать )) или проверять конфигурацию перед строительством.
Реализация бетонных строителей
Давайте реализуем два конструктора, чтобы продемонстрировать гибкость: a и a . Оба реализуют один и тот же интерфейс, но производят разные виды отчетов.
Подробнее о ReportBuilder
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;
}
}
Краткое содержание ReportBuilder
В сводном отчете могут игнорироваться столбцы, фильтры и итоговые данные, а вместо этого все суммируется в одно число или простую таблицу.
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;
}
}
При таком подходе клиент может выбрать застройщика, который соответствует требуемой сложности вывода, не изменяя последовательности построения. В этом суть шаблона Строителя.
Добавление директора для предварительно определенных шаблонов
Часто вы хотите инкапсулировать общие последовательности конструкций. Класс директора может сделать это:
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();
}
}
Директору может быть введён любой вариант реализации. Это отделяет шаблон от деталей бетонной конструкции.
Строительный шаблон в весенней загрузке: проводка и использование
Инъекция зависимости Spring Boot позволяет легко управлять строителями как бобами и переключать их во время выполнения.
Шаг 1: Определите строителей как весенние бобы
Мы можем аннотировать наших бетоностроителей или объявить их в классе:
@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);
}
}
Использование области FLT:23 важно: каждый вызов на должен создать новый экземпляр строителя со свежим внутренним состоянием. Если бы мы использовали область одиночного масштаба, строитель сохранил бы состояние от предыдущих вызовов, вызывая ошибки.
Чтобы справиться с тем фактом, что требует конкретного строителя, мы можем использовать или заводской образец. Практичный подход заключается в определении нескольких директорных бобов, по одному на тип строителя:
@Bean
public ReportDirector detailedReportDirector(@Qualifier("detailedReportBuilder") ReportBuilder builder) {
return new ReportDirector(builder);
}
@Bean
public ReportDirector summaryReportDirector(@Qualifier("summaryReportBuilder") ReportBuilder builder) {
return new ReportDirector(builder);
}
Шаг 2: Вводите строителей / директоров в контроллеры или службы
Типичный контроллер может принять параметр типа отчета и использовать соответствующий компонент:
@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);
}
}
В качестве альтернативы можно вводить конструкторы напрямую и позволить сервисному слою выбирать.Ключевой момент: клиентский код никогда не знает о внутренних компонентах конструктора — он просто вызывает или метод директора.
Расширенная кастомизация: динамические строители с поставщиком объектов Spring
Иногда выбор конструктора должен происходить во время выполнения на основе свойств конфигурации или ролей пользователя. может помочь вводить прототип бобов лениво:
@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();
}
}
Этот шаблон позволяет избежать необходимости предварительной проводки каждого возможного строителя напрямую, сохраняя при этом чистый и проверяемый код.
Обеспечение неизменности и безопасности потока
Продукт должен быть неизменным после строительства. Поскольку строители обычно используются в одной нити и не совместно используются, нам не нужно синхронизировать самого строителя. Однако, если вы планируете повторно использовать строителя через нити (не рекомендуется), убедитесь, что у строителя нет изменяемого общего состояния.
Чтобы обеспечить неизменность, сделайте класс действительно неизменным:
- Все поля отметьте как .
- Пропускайте все значения через конструктора (строитель называет частного конструктора, который устанавливает все).
- Предоставьте только геттеров, без сеттеров.
- Для коллекций (например, колонок) сделайте защитные копии в конструкторе или используйте .
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...
}
Затем создает через этого полного конструктора, передавая все собранные значения. Это гарантирует, что после постройки отчет не может быть изменен.
Испытание двигателя отчетности
Модель Строителя делает тестирование простым, потому что вы можете вводить макеты строителей или сборщиков, специфичных для тестирования. Для единичных тестов продукта вы можете мгновенно внедрить его непосредственно с помощью строителя. Для интеграционных тестов вы можете проверить, что правильный конструктор вызван и что окончательный отчет соответствует ожиданиям.
Тестирование бетонного строителя
@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
}
Тестирование с помощью моков
При тестировании службы, использующей конструктор, изменяйте интерфейс конструктора для проверки взаимодействий:
@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();
}
Соображения производительности и кэширование
Построение объекта само по себе дешево — это просто сборка конфигурационных данных. Дорогая часть выполняет базовый запрос, преобразует данные и генерирует выходной файл (PDF, XLSX). Поэтому конструктор не должен запускать какой-либо I/O. Эта ответственность принадлежит отдельному или аналогичной службе.
Если одна и та же конфигурация отчета запрашивается повторно (например, один и тот же ежемесячный отчет о продажах для одного и того же региона), вы можете кэшировать объект (конфигурация) и повторно использовать его. Для кэширования вы можете использовать метод Spring по методу директора или методу обслуживания.
@Cacheable("reportConfigs")
public Report getMonthlySalesConfig(String region) {
return detailedReportDirector.constructMonthlySalesReport(region);
}
Кэширование конфигурации позволяет конструктору запускать только один раз на каждый отдельный набор параметров, ускоряя последующие запросы еще до выполнения запроса.
Сравнение шаблона строителя с другими подходами
При разработке двигателя отчетности вы можете рассмотреть другие шаблоны:
- Фабричный метод — хорош для создания объекта отчета за один шаг, но не поддерживает пошагово-настроенную конфигурацию.
- Конструктор со многими параметрами — Телескопические конструкторы подвержены ошибкам и их трудно читать.
- Паттерн JavaBeans (изменяемые множители) — позволяет поэтапно настраивать, но нарушает неизменность и может привести к частично инициализированным объектам.
- Стратегический шаблон — может быть объединен со Строителем; строитель может принять стратегию рендеринга или извлечения данных.
Модель Builder превосходит, когда продукт имеет много дополнительных компонентов, таких как отчет. Он также поддерживает принцип Open / Closed - вы можете добавить новые типы отчетов, внедрив новый конструктор без изменения существующего кода.
Реальные расширения мира
Двигатель производственной отчетности часто нуждается в более чем простой конфигурации. Рассмотрим эти расширения:
- Вложенные строители для подотчетности – каждый подотчет может иметь своего строителя.
- Концепция — предварительно сконфигурированные конструкторы, хранящиеся в базе данных или файлах YAML.
- Интеграция с Spring Cloud Config для изменения шаблонов отчетов без перераспределения.
- Используя аннотацию Lombok для автоматического генерирования класса строителей. Будьте осторожны: Lombok генерирует статический вложенный конструктор, который может не позволять полиморфным строителям для разных типов отчетов. Для нашей цели пользовательские строители дают больше контроля.
Например, шаблон на основе YAML может быть загружен:
monthly-sales:
title: "Monthly Sales - ${region}"
dataSource: "jdbc/sales"
query: "SELECT ..."
columns: ["Product", "Units Sold"]
outputFormat: "PDF"
showTotals: true
Сервис может анализировать этот шаблон и вызывать соответствующие методы сборки, что делает механизм отчетности полностью управляемым данными.
Заключение
Паттерн Строителя, когда применяется к движку отчетности Java Spring Boot, обеспечивает чистое разделение между конфигурацией отчетов и их представлением. Определяя беглый интерфейс и реализуя несколько бетонных сборщиков, вы позволяете динамически настраивать отчеты во время выполнения без накопления технического долга. Факультативный класс директора инкапсулирует обычно используемые последовательности, а впрыск зависимости Spring делает тривиальным объединение всего вместе.
Эта конструкция не только расширяема - вы можете добавить новые типы отчетов, написав новый конструктор - но и тестируема, потому что строители - простые объекты Java, которые можно издеваться или создавать изолированно.В сочетании с неизменяемыми продуктами и кэшированием двигатель остается работоспособным и безопасным.
Независимо от того, создаете ли вы простую панель инструментов или полноценную платформу бизнес-аналитики, шаблон Builder дает вам гибкость для удовлетворения меняющихся требований при сохранении кодовой базы, с которой приятно работать. Для дальнейшего чтения см. официальную документацию по фреймворку весны по диапазонам бобов и классическую книгу «Планы проектирования» для более подробного контекста по шаблонам создания.