第二部分:創建型模式 - 生成器模式 (Builder Pattern)
前面我們學習了單例、工廠方法和抽象工廠模式,它們都關注如何創建對象。生成器模式(也常被稱為建造者模式)是另一種創建型模式,它專注于將一個復雜對象的構建過程與其表示分離,使得同樣的構建過程可以創建不同的表示。
- 核心思想:將一個復雜對象的構建層與其表示層分離,使得同樣的構建過程可以創建不同的表示。
生成器模式 (Builder Pattern)
“將一個復雜對象的構建與其表示分離,使得同樣的構建過程可以創建不同的對象表示。”
想象一下去快餐店點餐,比如賽百味 (Subway) 或者定制漢堡店:
- 復雜對象:你最終得到的定制三明治或漢堡。
- 構建過程:選擇面包類型 -> 選擇肉類 -> 選擇蔬菜 -> 選擇醬料 -> 完成。
- 表示:
- 三明治A:全麥面包 + 雞肉 + 生菜番茄 + 蜂蜜芥末醬。
- 三明治B:白面包 + 牛肉 + 洋蔥青椒 + 西南醬。
- 漢堡C:芝麻面包 + 雙層牛肉餅 + 酸黃瓜 + 特制醬。
服務員(Director)會按照固定的步驟(選擇面包、肉、菜、醬)來詢問你。你(作為構建指令的提供者,或者說,你指導一個Builder)告訴服務員每一步你的選擇。最終,服務員根據你的選擇組裝出你想要的三明治或漢堡。
這個模式的關鍵在于,構建過程(點餐步驟)是標準化的,但每一步的具體選擇(面包種類、肉類種類等)是靈活的,從而可以產生多種不同的最終產品(表示)。
1. 目的 (Intent)
生成器模式的主要目的:
- 封裝復雜對象的創建過程:當一個對象的創建過程非常復雜,包含多個步驟或多個部分時,使用生成器模式可以將這個復雜的構建邏輯封裝起來。
- 分步構建對象:允許你分步驟、按順序地構建一個對象,而不是一次性通過一個巨大的構造函數來創建。
- 創建不同表示:使得同樣的構建過程可以創建出內部結構不同(即屬性不同)的多種對象表示。
- 更好的控制構建過程:Director 控制構建的順序和步驟,Builder 負責實現每個步驟。
2. 生活中的例子 (Real-world Analogy)
-
組裝電腦:
- Product (產品):一臺組裝好的電腦。
- Builder (抽象建造者):
ComputerBuilder
接口,定義了安裝CPU、主板、內存、硬盤、顯卡等步驟的方法。 - ConcreteBuilder (具體建造者):
GamingComputerBuilder
(選擇高性能CPU、高端顯卡、大內存),OfficeComputerBuilder
(選擇性價比CPU、集成顯卡、普通內存)。 - Director (指揮者):電腦裝機員。他按照固定的順序(先裝CPU到主板,再裝內存條,再裝入機箱…)來指導
Builder
進行組裝。他不需要知道具體用的是什么牌子的CPU或顯卡,這些由具體的Builder
決定。
客戶只需要告訴裝機員他想要一臺“游戲電腦”還是“辦公電腦”(選擇了哪個ConcreteBuilder
),裝機員就能按部就班地組裝出來。
-
編寫一份復雜的文檔 (如簡歷、報告):
- Product:最終的文檔。
- Builder:
DocumentBuilder
接口,定義了buildHeader()
,buildBodyParagraph(text)
,buildListItem(item)
,buildFooter()
等方法。 - ConcreteBuilder:
ResumeBuilder
(簡歷的頁眉是個人信息,頁腳是聯系方式),ReportBuilder
(報告的頁眉是標題和日期,頁腳是頁碼)。 - Director:文檔生成程序。它調用
Builder
的方法來按順序構建文檔的各個部分。
-
URL 構建:Java中的
UriComponentsBuilder
或類似工具,允許你分步設置 scheme, host, port, path, query parameters 等來構建一個URL。
3. 結構 (Structure)
生成器模式通常包含以下角色:
- Product (產品):表示被構建的復雜對象。ConcreteBuilder 創建該產品的內部表示并定義它的裝配過程。
- Builder (抽象建造者):為創建一個 Product對象的各個部件指定抽象接口。它通常包含一系列
buildPartX()
方法和一個getResult()
方法用于返回構建好的產品。 - ConcreteBuilder (具體建造者):實現 Builder 接口,構造和裝配產品的各個部件。定義并明確它所創建的表示,并提供一個檢索產品的接口。
- Director (指揮者/導演):構造一個使用 Builder 接口的對象。Director 類負責調用具體建造者角色以創建產品對象。Director 并不保存對具體建造者角色的引用,而是通過其抽象接口與之協作。
構建流程:
- 客戶端創建一個
ConcreteBuilder
對象。 - 客戶端創建一個
Director
對象,并將ConcreteBuilder
對象傳遞給它。 Director
調用Builder
接口中定義的方法,按特定順序指導ConcreteBuilder
構建產品。ConcreteBuilder
逐步構建產品的內部表示。- 客戶端從
ConcreteBuilder
中獲取構建完成的Product
。
4. 適用場景 (When to Use)
- 當創建復雜對象的算法應該獨立于該對象的組成部分以及它們的裝配方式時。
- 當構造過程必須允許被構造的對象有不同的表示時。
- 對象的構建過程非常復雜,包含多個可選步驟或配置。例如,創建一個復雜的配置對象,其中某些配置項是可選的,或者有多種組合方式。
- 需要分步創建一個對象,并且在每一步之后可能需要進行一些中間操作或驗證。
- 希望隱藏對象的內部表示和構建細節。
- 一個對象有非常多的構造參數,其中大部分是可選的。如果用構造函數,可能會導致構造函數參數列表過長,或者需要多個重載的構造函數(伸縮構造函數問題)。生成器模式可以提供更優雅的鏈式調用方式。
5. 優缺點 (Pros and Cons)
優點:
- 封裝性好:使得客戶端不必知道產品內部組成的細節,產品本身和創建過程解耦。
- 易于控制構建過程:Director 可以精確控制構建的順序和步驟。
- 可以創建不同表示:同樣的構建過程可以應用于不同的 ConcreteBuilder,從而得到不同的產品表示。
- 更好的可讀性和易用性:對于有很多可選參數的復雜對象,使用鏈式調用的生成器比使用長參數列表的構造函數更清晰。
- 分步構建:可以將產品的構建過程分解為多個獨立的步驟,使得構建過程更加靈活。
缺點:
- 類的數量增多:需要為每個產品創建一個 ConcreteBuilder 類,如果產品種類很多,會導致類的數量增加。
- 產品必須有共同點:生成器模式創建的產品一般具有較多的共同點,其組成部分相似;如果產品之間的差異性很大,則不適合使用生成器模式。
- 模式本身相對復雜:相比于工廠模式,生成器模式的結構更復雜,包含的角色更多。
6. 實現方式 (Implementations)
讓我們通過一個構建“報告文檔”的例子來看看生成器模式的實現。報告可以有標題、作者、日期、多個段落內容、以及頁腳。
Product (ReportDocument)
// report_document.go
package reportimport ("fmt""strings"
)// ReportDocument 產品
type ReportDocument struct {Title stringAuthor stringDate stringContents []stringFooter string
}func (rd *ReportDocument) AddContent(paragraph string) {rd.Contents = append(rd.Contents, paragraph)
}func (rd *ReportDocument) Display() {fmt.Println("========================================")if rd.Title != "" {fmt.Printf("Title: %s\n", rd.Title)}if rd.Author != "" {fmt.Printf("Author: %s\n", rd.Author)}if rd.Date != "" {fmt.Printf("Date: %s\n", rd.Date)}fmt.Println("----------------------------------------")for _, content := range rd.Contents {fmt.Println(content)}fmt.Println("----------------------------------------")if rd.Footer != "" {fmt.Printf("Footer: %s\n", rd.Footer)}fmt.Println("========================================")
}
// ReportDocument.java
package com.example.report;import java.util.ArrayList;
import java.util.List;// 產品
public class ReportDocument {private String title;private String author;private String date;private List<String> contents = new ArrayList<>();private String footer;public void setTitle(String title) { this.title = title; }public void setAuthor(String author) { this.author = author; }public void setDate(String date) { this.date = date; }public void addContent(String paragraph) { this.contents.add(paragraph); }public void setFooter(String footer) { this.footer = footer; }public void display() {System.out.println("========================================");if (title != null && !title.isEmpty()) {System.out.println("Title: " + title);}if (author != null && !author.isEmpty()) {System.out.println("Author: " + author);}if (date != null && !date.isEmpty()) {System.out.println("Date: " + date);}System.out.println("----------------------------------------");for (String content : contents) {System.out.println(content);}System.out.println("----------------------------------------");if (footer != null && !footer.isEmpty()) {System.out.println("Footer: " + footer);}System.out.println("========================================");}
}
Builder (ReportBuilder)
// report_builder.go
package report// ReportBuilder 抽象建造者接口
type ReportBuilder interface {SetTitle(title string)SetAuthor(author string)SetDate(date string)AddParagraph(paragraph string)SetFooter(footer string)GetReport() *ReportDocument
}
// ReportBuilder.java
package com.example.report;// 抽象建造者接口
public interface ReportBuilder {void setTitle(String title);void setAuthor(String author);void setDate(String date);void addParagraph(String paragraph);void setFooter(String footer);ReportDocument getReport();
}
ConcreteBuilder (SimpleReportBuilder, DetailedReportBuilder)
// simple_report_builder.go
package report// SimpleReportBuilder 具體建造者 - 構建簡單報告
type SimpleReportBuilder struct {document *ReportDocument
}func NewSimpleReportBuilder() *SimpleReportBuilder {return &SimpleReportBuilder{document: &ReportDocument{}}
}func (b *SimpleReportBuilder) SetTitle(title string) { b.document.Title = "Simple Report: " + title }
func (b *SimpleReportBuilder) SetAuthor(author string) { /* 簡單報告不包含作者 */ }
func (b *SimpleReportBuilder) SetDate(date string) { b.document.Date = date }
func (b *SimpleReportBuilder) AddParagraph(paragraph string) { b.document.AddContent(paragraph) }
func (b *SimpleReportBuilder) SetFooter(footer string) { b.document.Footer = "End of Simple Report." }
func (b *SimpleReportBuilder) GetReport() *ReportDocument { return b.document }// detailed_report_builder.go
package report// DetailedReportBuilder 具體建造者 - 構建詳細報告
type DetailedReportBuilder struct {document *ReportDocument
}func NewDetailedReportBuilder() *DetailedReportBuilder {return &DetailedReportBuilder{document: &ReportDocument{}}
}func (b *DetailedReportBuilder) SetTitle(title string) { b.document.Title = "Detailed Analysis: " + title }
func (b *DetailedReportBuilder) SetAuthor(author string) { b.document.Author = author }
func (b *DetailedReportBuilder) SetDate(date string) { b.document.Date = "Generated on: " + date }
func (b *DetailedReportBuilder) AddParagraph(paragraph string) { b.document.AddContent("\t- " + paragraph) }
func (b *DetailedReportBuilder) SetFooter(footer string) { b.document.Footer = fmt.Sprintf("Report Concluded. %s. (c) MyCompany", footer)
}
func (b *DetailedReportBuilder) GetReport() *ReportDocument { return b.document }
// SimpleReportBuilder.java
package com.example.report;// 具體建造者 - 構建簡單報告
public class SimpleReportBuilder implements ReportBuilder {private ReportDocument document;public SimpleReportBuilder() {this.document = new ReportDocument();System.out.println("SimpleReportBuilder: Initialized.");}@Overridepublic void setTitle(String title) {document.setTitle("Simple Report: " + title);}@Overridepublic void setAuthor(String author) {// 簡單報告不包含作者System.out.println("SimpleReportBuilder: Author field is ignored for simple reports.");}@Overridepublic void setDate(String date) {document.setDate(date);}@Overridepublic void addParagraph(String paragraph) {document.addContent(paragraph);}@Overridepublic void setFooter(String footer) {document.setFooter("End of Simple Report.");}@Overridepublic ReportDocument getReport() {return document;}
}// DetailedReportBuilder.java
package com.example.report;// 具體建造者 - 構建詳細報告
public class DetailedReportBuilder implements ReportBuilder {private ReportDocument document;public DetailedReportBuilder() {this.document = new ReportDocument();System.out.println("DetailedReportBuilder: Initialized.");}@Overridepublic void setTitle(String title) {document.setTitle("Detailed Analysis: " + title);}@Overridepublic void setAuthor(String author) {document.setAuthor(author);}@Overridepublic void setDate(String date) {document.setDate("Generated on: " + date);}@Overridepublic void addParagraph(String paragraph) {document.addContent("\t- " + paragraph); // 添加縮進和項目符號}@Overridepublic void setFooter(String footer) {document.setFooter(String.format("Report Concluded. %s. (c) MyCompany", footer));}@Overridepublic ReportDocument getReport() {return document;}
}
Director (ReportDirector)
// report_director.go
package report// ReportDirector 指揮者
type ReportDirector struct {builder ReportBuilder
}func NewReportDirector(builder ReportBuilder) *ReportDirector {return &ReportDirector{builder: builder}
}// ConstructMonthlyReport 指揮構建月度報告
func (d *ReportDirector) ConstructMonthlyReport(title, author, date string, contents []string, footerDetails string) *ReportDocument {d.builder.SetTitle(title)d.builder.SetAuthor(author) // Builder 內部可能忽略此項d.builder.SetDate(date)for _, p := range contents {d.builder.AddParagraph(p)}d.builder.SetFooter(footerDetails)return d.builder.GetReport()
}// ConstructQuickSummary 指揮構建快速摘要
func (d *ReportDirector) ConstructQuickSummary(title, date string, summaryContent string) *ReportDocument {d.builder.SetTitle(title)// 快速摘要可能不需要作者和完整頁腳d.builder.SetDate(date)d.builder.AddParagraph(summaryContent)d.builder.SetFooter("Quick Summary") // 簡化頁腳return d.builder.GetReport()
}
// ReportDirector.java
package com.example.report;import java.util.List;// 指揮者
public class ReportDirector {private ReportBuilder builder;public ReportDirector(ReportBuilder builder) {this.builder = builder;System.out.println("ReportDirector: Configured with builder: " + builder.getClass().getSimpleName());}// 指揮構建月度報告public ReportDocument constructMonthlyReport(String title, String author, String date, List<String> contents, String footerDetails) {System.out.println("ReportDirector: Constructing Monthly Report...");builder.setTitle(title);builder.setAuthor(author); // Builder 內部可能忽略此項builder.setDate(date);for (String p : contents) {builder.addParagraph(p);}builder.setFooter(footerDetails);return builder.getReport();}// 指揮構建快速摘要public ReportDocument constructQuickSummary(String title, String date, String summaryContent) {System.out.println("ReportDirector: Constructing Quick Summary...");builder.setTitle(title);// 快速摘要可能不需要作者和完整頁腳builder.setDate(date);builder.addParagraph(summaryContent);builder.setFooter("Quick Summary"); // 簡化頁腳return builder.getReport();}
}
客戶端使用
// main.go (示例用法)
/*
package mainimport ("./report""time"
)func main() {// 構建簡單月度報告simpleBuilder := report.NewSimpleReportBuilder()director1 := report.NewReportDirector(simpleBuilder)monthlyContents := []string{"Sales are up by 10%.","Customer satisfaction is high.",}simpleMonthlyReport := director1.ConstructMonthlyReport("October Sales","Sales Team", // SimpleBuilder 會忽略作者time.Now().Format("2006-01-02"),monthlyContents,"Internal Use Only",)simpleMonthlyReport.Display()fmt.Println("\n-----------------------------------\n")// 構建詳細年度報告detailedBuilder := report.NewDetailedReportBuilder()director2 := report.NewReportDirector(detailedBuilder)annualContents := []string{"Market share increased by 5%.","New product line launched successfully with positive feedback.","Research and Development made significant progress on Project X.",}detailedAnnualReport := director2.ConstructMonthlyReport( // 復用構建邏輯,但用不同的builder"Annual Financials 2023","Dr. Alice Smith, CFO",time.Now().Format("Jan 02, 2006"),annualContents,"For Shareholders",)detailedAnnualReport.Display()fmt.Println("\n-----------------------------------\n")// 使用同一個 detailedBuilder 構建另一種類型的報告 (快速摘要)quickSummary := director2.ConstructQuickSummary("Q3 Highlights",time.Now().Format("2006-01-02"),"Overall positive quarter with key targets met.",)quickSummary.Display()
}
*/
// Main.java (示例用法)
/*
package com.example;import com.example.report.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;public class Main {public static void main(String[] args) {String currentDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE);// 構建簡單月度報告System.out.println("--- Building Simple Monthly Report ---");ReportBuilder simpleBuilder = new SimpleReportBuilder();ReportDirector director1 = new ReportDirector(simpleBuilder);List<String> monthlyContents = Arrays.asList("Sales are up by 10%.","Customer satisfaction is high.");ReportDocument simpleMonthlyReport = director1.constructMonthlyReport("October Sales","Sales Team", // SimpleBuilder 會忽略作者currentDate,monthlyContents,"Internal Use Only");simpleMonthlyReport.display();System.out.println("\n--- Building Detailed Annual Report ---");// 構建詳細年度報告ReportBuilder detailedBuilder = new DetailedReportBuilder();ReportDirector director2 = new ReportDirector(detailedBuilder);List<String> annualContents = Arrays.asList("Market share increased by 5%.","New product line launched successfully with positive feedback.","Research and Development made significant progress on Project X.");ReportDocument detailedAnnualReport = director2.constructMonthlyReport( // 復用構建邏輯,但用不同的builder"Annual Financials 2023","Dr. Alice Smith, CFO",LocalDate.now().format(DateTimeFormatter.ofPattern("MMM dd, yyyy")),annualContents,"For Shareholders");detailedAnnualReport.display();System.out.println("\n--- Building Quick Summary with Detailed Builder ---");// 使用同一個 detailedBuilder 構建另一種類型的報告 (快速摘要)ReportDocument quickSummary = director2.constructQuickSummary("Q3 Highlights",currentDate,"Overall positive quarter with key targets met.");quickSummary.display();}
}
*/
關于鏈式調用 (Fluent Interface)
在很多現代語言的實現中,Builder
的 buildPartX()
方法通常會返回 this
(或 self
),以支持鏈式調用,這樣客戶端代碼可以更簡潔。這種情況下,Director
角色有時會被弱化,甚至省略,客戶端直接通過鏈式調用來指導 Builder
。
Java 鏈式調用示例 (不使用顯式 Director):
// MailMessage.java (Product)
package com.example.mail;public class MailMessage {private String from;private String to;private String subject;private String body;private String cc;// 私有構造,強制使用Builderprivate MailMessage(Builder builder) {this.from = builder.from;this.to = builder.to;this.subject = builder.subject;this.body = builder.body;this.cc = builder.cc;}@Overridepublic String toString() {return "MailMessage{" +"from='" + from + '\'' +", to='" + to + '\'' +", subject='" + subject + '\'' +", body='" + body + '\'' +(cc != null ? ", cc='" + cc + '\'' : "") +'}';}// 靜態內部 Builder 類public static class Builder {private String from;private String to; // 必填private String subject;private String body;private String cc; // 可選public Builder(String to) { // 必填項通過構造函數傳入this.to = to;}public Builder from(String from) {this.from = from;return this;}public Builder subject(String subject) {this.subject = subject;return this;}public Builder body(String body) {this.body = body;return this;}public Builder cc(String cc) {this.cc = cc;return this;}public MailMessage build() {if (this.from == null || this.from.isEmpty()) {throw new IllegalStateException("From address cannot be empty");}// 可以在這里添加更多校驗邏輯return new MailMessage(this);}}
}// Main.java (示例用法)
/*
package com.example;import com.example.mail.MailMessage;public class Main {public static void main(String[] args) {MailMessage message1 = new MailMessage.Builder("recipient@example.com").from("sender@example.com").subject("Hello from Builder Pattern!").body("This is a demonstration of the fluent builder pattern.").cc("manager@example.com").build();System.out.println(message1);MailMessage message2 = new MailMessage.Builder("another@example.com").from("noreply@example.com").subject("Important Update")// body 和 cc 是可選的.build();System.out.println(message2);try {MailMessage message3 = new MailMessage.Builder("test@example.com")// .from("testsender@example.com") // 故意不設置 from.subject("Test").build();System.out.println(message3);} catch (IllegalStateException e) {System.err.println("Error building message: " + e.getMessage());}}
}
*/
這種鏈式調用的方式在Java中非常流行,例如 StringBuilder
, OkHttp Request.Builder
, Lombok @Builder
注解等。
7. 與抽象工廠模式的區別
-
抽象工廠模式 (Abstract Factory):
- 關注點:創建產品族 (一系列相關的產品對象)。
- 產品創建:通常是一次性獲取整個產品族中的某個產品 (例如
factory.createButton()
)。 - 目的:保證創建出來的產品屬于同一個系列,相互兼容。
-
生成器模式 (Builder):
- 關注點:創建單個復雜對象,其構建過程包含多個步驟。
- 產品創建:分步驟構建,最后通過
getResult()
或build()
獲取完整對象。 - 目的:將復雜對象的構建過程和其表示分離,允許同樣的構建過程創建不同的表示。
關鍵區別:
- 抽象工廠返回的是多個不同類型的產品(但它們屬于一個系列)。
- 生成器返回的是一個復雜的產品,這個產品是逐步構建起來的。
- 抽象工廠通常在客戶端決定使用哪個具體工廠后,由工廠直接創建出產品。而生成器模式中,Director 控制構建步驟,Builder 實現這些步驟。
有時,生成器模式的 buildPartX()
方法內部可能會使用工廠方法來創建部件。
8. 總結
生成器模式是一種強大的創建型模式,適用于構建具有多個組成部分、構建過程復雜或需要多種表示的復雜對象。它通過將構建過程與對象的表示分離,提高了代碼的模塊化程度和靈活性。當遇到有很多可選參數的構造函數時,或者當對象的創建邏輯比較復雜時,可以考慮使用生成器模式來簡化對象的創建和提高代碼的可讀性。
記住它的核心:分步構建復雜對象,不同表示。