白话设计模式之生成器模式:复杂对象构建的“神奇画笔”
大家好!在技术学习的漫漫长路上,我一直相信分享能让我们收获更多,进步更快。今天,咱们就来深入探索设计模式中的生成器模式,它就像代码世界里的一支“神奇画笔”,能把复杂对象的构建过程描绘得清晰又灵活。希望通过这篇博客,能帮助大家轻松理解并掌握这个强大的设计模式。
一、生成器模式的引入
(一)实际开发中的复杂对象构建难题
在软件开发过程中,构建复杂对象是常有的挑战。就像文档里提到的创建保险合同对象,它包含多个属性,而且这些属性之间有着复杂的约束关系,比如一份保险合同不能同时与个人和公司签订,保险失效日期必须大于生效日期等。这就好比搭建一座结构复杂的桥梁,每个部件都有特定的规格和安装要求,相互之间紧密关联,牵一发而动全身。在代码实现中,如果直接处理这些复杂的构建逻辑,会使代码变得混乱不堪,难以维护和扩展。比如,当需求发生变化,需要修改某个属性的约束条件时,可能需要在大量的代码中进行查找和修改,这不仅增加了开发成本,还容易引入新的错误。
(二)传统构建方式的局限
如果不借助合适的设计模式,直接在代码里构建复杂对象,会带来诸多问题。以保险合同对象为例,若在一个方法中直接创建对象并设置其多个属性,代码会变得冗长且难以阅读。而且,很难保证在设置属性的过程中,所有的约束条件都能被正确处理。当需要创建多个不同的保险合同对象时,代码的重复度会很高,这不仅增加了开发工作量,还降低了代码的可维护性。这就如同没有清晰的施工图纸去建造一座复杂的建筑,施工过程会变得混乱无序,后期的维护和修改也会困难重重。
二、生成器模式的概念
(一)生成器模式的定义
生成器模式的定义是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。简单来说,就好比制作一个精美的蛋糕,制作蛋糕的基本流程(搅拌原料、烘焙、装饰等)是固定的,但通过不同的装饰(如裱花、摆放水果)可以呈现出不同样式的蛋糕。在代码的世界里,生成器模式把复杂对象的创建步骤抽象出来,形成一个通用的构建流程,同时允许根据不同的需求生成不同形式的对象。
(二)生成器模式的结构
生成器模式主要包含四个角色:
- Builder(生成器):这是一个抽象类或接口,定义了构建复杂对象各个部分的抽象方法。它类似于蛋糕制作流程的大纲,规定了制作蛋糕需要进行哪些步骤,但不涉及具体的制作细节。在创建保险合同对象的场景中,生成器接口可能定义了设置合同编号、被保险方信息、生效和失效日期等方法。
- ConcreteBuilder(具体生成器):实现了Builder接口,具体实现构建复杂对象各个部分的方法。好比不同风格蛋糕的制作师傅,按照基本流程大纲,用各自的技巧制作出不同风格的蛋糕。在保险合同场景中,具体生成器类会根据保险合同的具体要求,实现设置各个属性的方法。
- Product(产品):就是要构建的复杂对象。在这个例子中,保险合同对象就是产品,它包含多个属性和相关的操作方法。
- Director(指挥者):负责安排复杂对象的构建顺序。它类似于蛋糕店的主管,根据顾客的需求,告诉制作师傅先做什么,后做什么。在代码中,指挥者利用生成器来构建产品,它不关心具体的构建细节,只关注构建的顺序。
三、生成器模式的代码示例
为了让大家更直观地理解,我们以一个简单的游戏角色创建系统为例。游戏角色有姓名、职业、技能和装备等属性,我们要创建不同类型的角色,并且按照不同的格式输出角色信息。
首先定义产品——游戏角色类:
// 游戏角色类,产品
public class GameCharacter {
private String name;
private String profession;
private String skills;
private String equipment;
public void setName(String name) {
this.name = name;
}
public void setProfession(String profession) {
this.profession = profession;
}
public void setSkills(String skills) {
this.skills = skills;
}
public void setEquipment(String equipment) {
this.equipment = equipment;
}
@Override
public String toString() {
return "角色信息:姓名 - " + name + ",职业 - " + profession + ",技能 - " + skills + ",装备 - " + equipment;
}
}
接着定义生成器接口:
// 游戏角色生成器接口,Builder
public interface CharacterBuilder {
void buildName();
void buildProfession();
void buildSkills();
void buildEquipment();
GameCharacter getCharacter();
}
然后创建具体生成器类:
// 战士角色生成器类,ConcreteBuilder
public class WarriorBuilder implements CharacterBuilder {
private GameCharacter warrior = new GameCharacter();
@Override
public void buildName() {
warrior.setName("勇猛无畏的战士-亚瑟");
}
@Override
public void buildProfession() {
warrior.setProfession("战士");
}
@Override
public void buildSkills() {
warrior.setSkills("剑术精通、冲锋陷阵、斩杀技能");
}
@Override
public void buildEquipment() {
warrior.setEquipment("双手大剑、重型铠甲、战靴");
}
@Override
public GameCharacter getCharacter() {
return warrior;
}
}
// 法师角色生成器类,ConcreteBuilder
public class MageBuilder implements CharacterBuilder {
private GameCharacter mage = new GameCharacter();
@Override
public void buildName() {
mage.setName("神秘莫测的法师-梅林");
}
@Override
public void buildProfession() {
mage.setProfession("法师");
}
@Override
public void buildSkills() {
mage.setSkills("火球术、冰系魔法、魔法护盾");
}
@Override
public void buildEquipment() {
mage.setEquipment("魔法法杖、魔法长袍、魔力宝石");
}
@Override
public GameCharacter getCharacter() {
return mage;
}
}
再定义指挥者类:
// 游戏角色指挥者类,Director
public class CharacterDirector {
private CharacterBuilder characterBuilder;
public CharacterDirector(CharacterBuilder characterBuilder) {
this.characterBuilder = characterBuilder;
}
public GameCharacter constructCharacter() {
characterBuilder.buildName();
characterBuilder.buildProfession();
characterBuilder.buildSkills();
characterBuilder.buildEquipment();
return characterBuilder.getCharacter();
}
}
最后在客户端代码中使用:
public class GameClient {
public static void main(String[] args) {
// 创建战士角色
CharacterBuilder warriorBuilder = new WarriorBuilder();
CharacterDirector warriorDirector = new CharacterDirector(warriorBuilder);
GameCharacter warrior = warriorDirector.constructCharacter();
System.out.println(warrior);
// 创建法师角色
CharacterBuilder mageBuilder = new MageBuilder();
CharacterDirector mageDirector = new CharacterDirector(mageBuilder);
GameCharacter mage = mageDirector.constructCharacter();
System.out.println(mage);
}
}
通过这个示例可以看到,客户端只需要和指挥者以及生成器接口打交道,不用关心具体的构建细节。当需要添加新的角色类型或修改角色信息的输出格式时,只需要添加新的具体生成器类或修改已有生成器类,而不需要修改指挥者和客户端代码,大大提高了代码的可维护性和扩展性。
四、生成器模式在构建保险合同对象中的应用
(一)简化的生成器模式实现
在构建保险合同对象时,文档中采用了简化的生成器模式。不再定义生成器接口,直接创建一个具体的构建器类。同时,去掉了“指挥者”角色,将其功能与客户端合并。具体构建器类通过一系列的设置方法来设置保险合同的各个属性,构造方法用于传入必须的参数,其他可选参数通过相应的设置方法进行设置。这种方式使得保险合同对象的创建过程更加简洁直观,例如:
// 保险合同构建器类
public class InsuranceContractBuilder {
private String contractId;
private String personName;
private String companyName;
private long beginDate;
private long endDate;
private String otherData;
public InsuranceContractBuilder(String contractId, long beginDate, long endDate) {
this.contractId = contractId;
this.beginDate = beginDate;
this.endDate = endDate;
}
public InsuranceContractBuilder setPersonName(String personName) {
this.personName = personName;
return this;
}
public InsuranceContractBuilder setCompanyName(String companyName) {
this.companyName = companyName;
return this;
}
public InsuranceContractBuilder setOtherData(String otherData) {
this.otherData = otherData;
return this;
}
public InsuranceContract build() {
return new InsuranceContract(this);
}
public String getContractId() {
return contractId;
}
public String getPersonName() {
return personName;
}
public String getCompanyName() {
return companyName;
}
public long getBeginDate() {
return beginDate;
}
public long getEndDate() {
return endDate;
}
public String getOtherData() {
return otherData;
}
}
// 保险合同类
public class InsuranceContract {
private String contractId;
private String personName;
private String companyName;
private long beginDate;
private long endDate;
private String otherData;
InsuranceContract(InsuranceContractBuilder builder) {
this.contractId = builder.getContractId();
this.personName = builder.getPersonName();
this.companyName = builder.getCompanyName();
this.beginDate = builder.getBeginDate();
this.endDate = builder.getEndDate();
this.otherData = builder.getOtherData();
}
public void someOperation() {
System.out.println("Now in InsuranceContract someOperation==" + this.contractId);
}
}
在客户端代码中,可以这样创建保险合同对象:
public class Client {
public static void main(String[] args) {
InsuranceContractBuilder builder = new InsuranceContractBuilder("001", 12345L, 67890L);
InsuranceContract contract = builder.setPersonName("李四").setOtherData("test").build();
contract.someOperation();
}
}
(二)添加约束规则的实现
为了确保保险合同对象的属性符合约束规则,在构建器的build
方法中添加了校验逻辑。例如:
public InsuranceContract build() {
if (contractId == null || contractId.trim().length() == 0) {
throw new IllegalArgumentException("合同编号不能为空");
}
boolean signPerson = personName != null && personName.trim().length() > 0;
boolean signCompany = companyName != null && companyName.trim().length() > 0;
if (signPerson && signCompany) {
throw new IllegalArgumentException("一份保险合同不能同时与人和公司签订");
}
if (signPerson == false && signCompany == false) {
throw new IllegalArgumentException("一份保险合同不能没有签订对象");
}
if (beginDate <= 0) {
throw new IllegalArgumentException("合同必须有保险开始生效的日期");
}
if (endDate <= 0) {
throw new IllegalArgumentException("合同必须有保险失效的日期");
}
if (endDate <= beginDate) {
throw new IllegalArgumentException("保险失效的日期必须大于保险生效日期");
}
return new InsuranceContract(this);
}
这样,在创建保险合同对象时,就能保证对象的属性符合各种约束条件,提高了代码的健壮性。
(三)构建器与被构建对象的合并
在实际开发中,为了避免同包内的对象不使用构建器而直接使用new
来构建对象导致的错误,还可以将构建器对象合并到被构建对象里面去。例如:
public class InsuranceContract {
private String contractId;
private String personName;
private String companyName;
private long beginDate;
private long endDate;
private String otherData;
private InsuranceContract() {}
public static class Builder {
private String contractId;
private String personName;
private String companyName;
private long beginDate;
private long endDate;
private String otherData;
public Builder(String contractId, long beginDate, long endDate) {
this.contractId = contractId;
this.beginDate = beginDate;
this.endDate = endDate;
}
public Builder setPersonName(String personName) {
this.personName = personName;
return this;
}
public Builder setCompanyName(String companyName) {
this.companyName = companyName;
return this;
}
public Builder setOtherData(String otherData) {
this.otherData = otherData;
return this;
}
public InsuranceContract build() {
if (contractId == null || contractId.trim().length() == 0) {
throw new IllegalArgumentException("合同编号不能为空");
}
boolean signPerson = personName != null && personName.trim().length() > 0;
boolean signCompany = companyName != null && companyName.trim().length() > 0;
if (signPerson && signCompany) {
throw new IllegalArgumentException("一份保险合同不能同时与人和公司签订");
}
if (signPerson == false && signCompany == false) {
throw new IllegalArgumentException("一份保险合同不能没有签订对象");
}
if (beginDate <= 0) {
throw new IllegalArgumentException("合同必须有保险开始生效的日期");
}
if (endDate <= 0) {
throw new IllegalArgumentException("合同必须有保险失效的日期");
}
if (endDate <= beginDate) {
throw new IllegalArgumentException("保险失效的日期必须大于保险生效日期");
}
InsuranceContract contract = new InsuranceContract();
contract.contractId = this.contractId;
contract.personName = this.personName;
contract.companyName = this.companyName;
contract.beginDate = this.beginDate;
contract.endDate = this.endDate;
contract.otherData = this.otherData;
return contract;
}
}
public void someOperation() {
System.out.println("Now in InsuranceContract someOperation==" + this.contractId);
}
}
在客户端代码中,可以这样使用:
public class Client {
public static void main(String[] args) {
InsuranceContract contract = new InsuranceContract.Builder("001", 12345L, 67890L)
.setPersonName("王五").setOtherData("test").build();
contract.someOperation();
}
}
五、生成器模式的应用场景与优缺点
(一)应用场景
- 创建复杂对象的场景:当需要创建的对象结构复杂,包含多个部分,且这些部分的构建过程比较复杂时,生成器模式非常适用。比如在一个电商系统中,创建订单对象时,订单不仅包含基本的订单信息(如订单编号、客户信息),还可能包含订单明细(商品列表、价格计算)、支付信息、物流信息等多个部分。使用生成器模式,可以将这些复杂的构建过程分离,使得代码更加清晰,易于维护。
- 需要创建不同表示形式的场景:如果同样的构建过程需要创建不同表示形式的对象,生成器模式就派上用场了。例如在一个文档生成系统中,需要将相同的数据以Word文档、PDF文档和HTML网页三种不同的格式输出。通过生成器模式,可以定义一个通用的构建过程,然后由不同的具体生成器来生成不同格式的文档。
- 对象构建过程需要分步进行的场景:当对象的构建过程需要按照一定的顺序分步进行时,生成器模式可以很好地满足需求。比如在一个游戏开发中,创建角色的过程可能需要先初始化基本属性(如生命值、魔法值),再添加装备,最后设置技能。使用生成器模式,指挥者可以按照这个顺序调用生成器的方法来构建角色。
(二)优点
- 解耦构建过程和表示:将复杂对象的构建过程和最终表示分离,使得代码更加清晰,易于维护。就像制作蛋糕,制作师傅专注于制作过程,装饰师傅负责蛋糕的外观,各司其职,代码结构也更加清晰。
- 提高可扩展性:当需要添加新的表示形式时,只需要添加新的具体生成器类,符合开闭原则。例如在文档生成系统中,如果要新增一种文档格式,只需要创建一个新的具体生成器类,而不需要修改其他代码。
- 便于控制构建过程:通过指挥者可以方便地控制