Table of Contents
מבוא: הצורך במנוע דוח גמיש
יישומים ארגוניים דורשים לעתים קרובות מנוע דיווח שיכול להתאים לדרישות עסקיות משתנות אי פעם.א.ר. דוח סטטי, קודר קשה הופך במהירות לנטל תחזוקה כאשר בעלי העניין דורשים מקורות נתונים חדשים, מסננים, פורמטים של תפוקה, או הפריסה חזותית.תבנית הבונים, דפוס עיצוב בריא מן ה-FLT:0Gang של FourFLT:1, מציעה דרך נקייה לבנות אובייקטים מורכבים תוך שמירה על תהליך עיצוב יצירתי של בדיקה יעילה זו, כאשר הוא מאפשר יישום מותאם אישית של יישום של יישום יעיל של יישום.
במאמר זה אנו מתכננים מנוע דיווח מן הקרקע, החל עם הליבה (FLT:0 כיתה וממשק גמיש FLT:1; אנו ניישם בונה קונקרטיים, לשלב אותם כמו קפיץ בוט, להוסיף FLT:0DirectorFLT:1 עבור תבניות דו"ח מוגדר מראש, לדון בשיקולים של עולם אמיתי כגון caching, בטיחות, חוט בטיחות, ובדיקה על ידי הדפסה מרחוק יכול להיות בעל מערכת ההפעלה שלך.
הבנת תבנית ה-ERERER IN TER
התבנית הבונה מבולבלת לעתים קרובות עם תבניות החרושת או החרושת הפשט, אך מטרתו ברורה: היא מנחה את בנייתו של צעד מוצר צעד אחר צעד, ומאפשרת ללקוח לבחור באילו צעדים להפעילו ובאיזה סדר סדר קלאסי A FLT:0 "תבניות עיצוב" סעיףFLT:1 יוצר מסמך - ייתכן שתרצה גרסה PDF, HTML, או גרסה פשוטה, בנוי מרצף של צעדים זהים (ד), להוסיף רגל).
משתתפים מרכזיים בדפוס:
- (ב) ,0) ,[עריכת קוד מקור | עריכה]
- (ב) [ה]] [ה]] [ה]]], [המילה] היא הממשק הפשטני הקובע את מדרגות הבנייה.
- (ב) ,0) ,ConcreteBuilderFLT:1 - יישום ממשק הבונים, הרכיב את המוצר, ומספק שיטה כדי לשחזר את התוצאה.
- (FLT:0)DirectorveFLT:1 (אופציונלי) - מציג את השלבים של הבניין באמצעות ממשק הרוכש, לעתים קרובות מטביעה ברירת מחדל או לעתים קרובות שימוש ברצף בנייה.
הפרדה זו של חששות פירושה שתהליך הבנייה יכול לייצר ייצוגים שונים פשוט על ידי החלפת קונקרטבניר.עבור מנוע דיווח, זה מתורגם להיות מסוגל לייצר "דו"ח ראשוני" או "דו"ח מופחת" באמצעות ממשק זההFLT:3 אבל יישומים שונים.
עיצוב מנוע הדוח
מנוע הדיווח שלנו ייבנה סביב המוצר (FLT:4) וממשק שוטפת (FLT:5 ממשק פלורנט (שרשרת מקול) הם התאמה טבעית לתבנית הרוכשת ומוביל לקוד לקוח לקריאה.
Defining the Report Product
שיעור ה-FLT:6 מחזיק את נתוני הליבה הדרושים כדי ליצור כל דיווח.במערכת אמיתית אתה יכול להוסיף שדות עבור ראשיים, רגלים, הגדרות תרשים, תת-דיווחים, וכו 'למשל, אנו שומרים אותו ממוקד:
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.
}
שימו לב ליוצר הפרטי, זה מחייב את העובדה ש-FLT:8 יכול להיווצר רק באמצעות בונה, ולהבטיח שכל מקרה מוגדר כראוי.
יצירת לוח הבקרה
ממשק הבונים מכריז על שיטות לכל שלב תצורה אופציונלית, על מנת לתמוך בשיטת השרשרת, כל אחד מהם חוזר (FLT:9) עצמו.
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();
}
ממשק זה רחב במכוון, בונה קונקטר יכולים לבחור להתעלם משיטות מסוימות (למשל, בונה דו"ח סיכום פשוט עשוי להתעלם (FLT:13) או לאמת את התצורה לפני הבנייה.
המונחים: Concrete Builders
בואו הטמיע שני בנינים כדי להפגין גמישות: AFLT:14 ו-AfLT:15 שניהם ליישם את אותו ממשק, אך מייצרים סוגים שונים של דוחות.
פרטים נוספים: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();
}
}
ניתן להזריק עם כל יישום של FLT:19 זה מקלקל את התבנית מפרטי הבנייה קונקרטיים.
תבנית בונה באביב Boot: Wiring and Usage
הזרקת התלות של האביב עושה את זה קל לנהל את הבנאים כעושבים ולעבור אותם בזמן ריצה.
שלב 1: יוצרי Define כ- Spring Beans
אנו יכולים לחדור את הבנאים הבטניים שלנו עם FLT:20 או להכריז עליהם בכיתה .
@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 חשוב: כל קריאה ל-FLT:24 צריכה ליצור מקרה חדש של בונה עם מדינה פנימית חדשה, אם השתמשנו בהיקף של טוטון, הבונים ישמרו על המדינה משיחות קודמות, מה שגורם באגים.
כדי לטפל בעובדה כי (FLT:25 דורש בונה ספציפי, אנו יכולים להשתמש ב-FLT:26 או תבנית מפעל. גישה מעשית היא להגדיר מספר רב של פולירקטור, אחד לכל סוג בונה:
@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: Inject Builders / Directors into Controllers or Services
בקר טיפוסי יכול לקבל פרמטר סוג דו"ח ולהשתמש ברכיב המתאים:
@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);
}
}
לחלופין, תוכל להזריק את הבנאים ישירות ולתת לשכבת השירות לבחור: קוד הלקוח לעולם לא יודע על הפנימיות של הבונים – הוא רק קורא ל-FLT:29 או שיטת מנהל.
התאמה מתקדמת: דינמינים עם האובייקטיבי של האביב
לפעמים בחירת הבונים חייבת להתרחש בזמן ריצה בהתבסס על תכונות תצורה או על תפקידי משתמשים.אביב: 30 יכול לעזור להזריק אבטיפוס בעצלות:
@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();
}
}
דפוס זה נמנע מלהיות מחווט כל בונה אפשרי ישירות, בעוד שעדיין שומר הקוד נקי ומבחן.
הבטחת חסינות וקידום בטיחות
המוצר צריך להיות בלתי-מוגדר לאחר הבנייה. כי הבנאים משמשים בדרך כלל חוט אחד ואינם משותפים, אנחנו לא צריכים לסנכרן את הבונים עצמם.
כדי לאכוף את חוסר יכולתה, להפוך את המעמד לחסר באמת:
- כל התחומים כ-FLT:34
- להעביר את כל הערכים באמצעות הבונים (הבנייה קוראת ליוצר פרטי שמציב הכל).
- רק מפטרים, אין מפרשים.
- עבור אוספים (למשל, עמודות), לבצע עותקים של הגנה במסדר או להשתמש ב-FLT:35.
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...
}
לאחר מכן, ה-FLT:37 יוצר את ה-FLT:38 באמצעות הבונים המלאים האלה, אשר עברו את כל הערכים שנאספו.
בדיקת מנוע הדוח
התבנית הבונה עושה בדיקה ישירה כי אתה יכול להזריק בוני לעג או בונה ספציפיים לבדיקה.עבור בדיקות יחידה של המוצר LT:39 אתה יכול מידל את זה ישירות באמצעות בונה.עבור בדיקות אינטגרציה, אתה יכול לוודא כי בונה הנכון נקרא וכי הדו"ח הסופי עונה הציפיות.
יחידה בודקת בונה Concrete
@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
}
בדיקות עם Mocks
כאשר בודקים שירות המשתמש בבן, ללעג את ממשק הבונים כדי לאמת אינטראקציות:
@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();
}
שיקולים ו Caching
בניית אובייקט עצמו זול - הוא רק מידע תצורה מחלחל.חלק יקר מבצע את השאילתה הבסיסית, שינוי נתונים, ומייצר את קובץ הפלט (PDF, XLSX) לכן, הבונים לא צריך לגרום לאף I/O. כי האחריות שייכת ל-FLT:43 נפרד או שירות דומה.
אם אותה תצורה של דו"ח מתבקש שוב ושוב (למשל, אותו דו"ח מכירות חודשי עבור אותו אזור), אתה יכול לטמון את האובייקט FLT:44:44 (התצורה) ולהשתמש בו מחדש, כי אתה יכול להשתמש ב-FLT:45 של האביב על שיטת הניהול או שיטת השירות.
@Cacheable("reportConfigs")
public Report getMonthlySalesConfig(String region) {
return detailedReportDirector.constructMonthlySalesReport(region);
}
גילוח התצורה מאפשר לBuilder לרוץ רק פעם אחת למערך נפרד של פרמטרים, להאיץ את הבקשות הבאות אפילו לפני ביצוע השאילתה.
השוואת דפוס ה-Build Pattern with other Approaches
בעת תכנון מנוע דיווח, ייתכן שתחשבו בדפוסים אחרים:
- (FLT:0) שיטת ניהול 1 (FLT) – טוב ליצירת אובייקט דו"ח בשלב אחד, אך אינו תומך בתצורה של צעד-wise.
- (FLT:0) הוראה עם פרמטרים רבים של 10FLT:1 - מבנים Telescoping הם הסתברות שגיאה וקשה לקרוא.תבנית ה-Build מספקת עיצוב ברור, הנקרא parameter.
- (ב) [ה]:0 [JavaBeans] דפוס (הצבים המוערכים) אנדרופול (הראשונה ל-1:1) – מאפשר תצורה חכמה, אך שוברת חוסר יכולת ויכולות ויכול להוביל לאובייקטים ראשוניים חלקית.
- (ב) ניתן לשלב עם בונה; הבונים יכולים לקבל אסטרטגיה לקביעת או איסוף נתונים.
דפוס הממציא מצטיין כאשר המוצר יש רכיבים אופציונליים רבים, כמו דו"ח זה גם תומך בקוד הפתוח / הסגור - אתה יכול להוסיף סוגים חדשים של דו"ח על ידי יישום בונה חדש מבלי לשנות קוד קיים.
הרחבה של Real-World
מנוע דיווח ייצור צריך לעתים קרובות יותר מתצורה פשוטה.חשבו על הרחבות אלה:
- בונים גנובים ל- subreports - לכל תת-ספר יכול להיות בעל בונה משלו.
- מושג (FLT:48) - בונה בעלי ערך מראש מאוחסנים במסד נתונים או ב-YAML קבצים.
- שילוב עם Spring Cloud Config כדי לשנות את תבניות הדיווח ללא תיקון.
- (השתמש ב-FLT:0) של למבומק (Lombok'sFLT:49eur) , 1:1 אנטנות לאוטומטי את מעמד הבונים.Be זהירה: Lombok יוצר בונה סטטי, אשר עשוי לא לאפשר לפולימורפיליים עבור סוגים שונים של דו"ח.
לדוגמה, ניתן לטעון תבנית מבוססת YAML:
monthly-sales:
title: "Monthly Sales - ${region}"
dataSource: "jdbc/sales"
query: "SELECT ..."
columns: ["Product", "Units Sold"]
outputFormat: "PDF"
showTotals: true
שירות יכול לפצח תבנית זו ולקרוא לשיטות הבונים המתאימות, מה שהופך את מנוע הדיווח למניעה מלאה של נתונים.
מסקנה
התבנית הבונה, כאשר חל על מנוע דיווח של Java Spring Boot, מספק הפרדה נקייה בין בניית תצורה של דו"ח לבין ייצוגם. על ידי הגדרת ממשק שוטפת של אלפבית 3.0 וליישם מספר רב של בונה קונקרטיים, אתה מאפשר התאמה דינמית, במשרה מלאה של דוחות ללא חוב טכני מצטבר.
עיצוב זה אינו רק צפוי - אתה יכול להוסיף סוגים חדשים של דו"ח על ידי כתיבת בונה חדש - אבל גם מבחן, כי בונה הם אובייקטים Java פשוטים שניתן ללעג או מיידית בבידוד. בשילוב עם מוצרים לא מובנים ו caching, המנוע נשאר להופיע בטוח.
בין אם אתם בונים לוח נתונים פשוט או פלטפורמה עסקית מלאה, דפוס היצרנים נותן לך את הגמישות לעמוד בדרישות מתפתחות תוך שמירה על בסיס קוד כי הוא תענוג לעבוד עם.לקריאה נוספת, לראות את הרשמי (FLT:0 ניצול תיעוד מסגרת על יקפי בטאאן FLT:1 ואת הקלאסי FLT:2Designs ספר LT3 עבור דפוסים נוספים על יצירת קשר על פורמטים יותר.