零基础设计模式——创建型模式 - 生成器模式
第二部分:创建型模式 - 生成器模式 (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. 总结
生成器模式是一种强大的创建型模式,适用于构建具有多个组成部分、构建过程复杂或需要多种表示的复杂对象。它通过将构建过程与对象的表示分离,提高了代码的模块化程度和灵活性。当遇到有很多可选参数的构造函数时,或者当对象的创建逻辑比较复杂时,可以考虑使用生成器模式来简化对象的创建和提高代码的可读性。
记住它的核心:分步构建复杂对象,不同表示。