Bootstrap

白话设计模式之生成器模式:复杂对象构建的“神奇画笔”

白话设计模式之生成器模式:复杂对象构建的“神奇画笔”

大家好!在技术学习的漫漫长路上,我一直相信分享能让我们收获更多,进步更快。今天,咱们就来深入探索设计模式中的生成器模式,它就像代码世界里的一支“神奇画笔”,能把复杂对象的构建过程描绘得清晰又灵活。希望通过这篇博客,能帮助大家轻松理解并掌握这个强大的设计模式。

一、生成器模式的引入

(一)实际开发中的复杂对象构建难题

在软件开发过程中,构建复杂对象是常有的挑战。就像文档里提到的创建保险合同对象,它包含多个属性,而且这些属性之间有着复杂的约束关系,比如一份保险合同不能同时与个人和公司签订,保险失效日期必须大于生效日期等。这就好比搭建一座结构复杂的桥梁,每个部件都有特定的规格和安装要求,相互之间紧密关联,牵一发而动全身。在代码实现中,如果直接处理这些复杂的构建逻辑,会使代码变得混乱不堪,难以维护和扩展。比如,当需求发生变化,需要修改某个属性的约束条件时,可能需要在大量的代码中进行查找和修改,这不仅增加了开发成本,还容易引入新的错误。

(二)传统构建方式的局限

如果不借助合适的设计模式,直接在代码里构建复杂对象,会带来诸多问题。以保险合同对象为例,若在一个方法中直接创建对象并设置其多个属性,代码会变得冗长且难以阅读。而且,很难保证在设置属性的过程中,所有的约束条件都能被正确处理。当需要创建多个不同的保险合同对象时,代码的重复度会很高,这不仅增加了开发工作量,还降低了代码的可维护性。这就如同没有清晰的施工图纸去建造一座复杂的建筑,施工过程会变得混乱无序,后期的维护和修改也会困难重重。

二、生成器模式的概念

(一)生成器模式的定义

生成器模式的定义是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。简单来说,就好比制作一个精美的蛋糕,制作蛋糕的基本流程(搅拌原料、烘焙、装饰等)是固定的,但通过不同的装饰(如裱花、摆放水果)可以呈现出不同样式的蛋糕。在代码的世界里,生成器模式把复杂对象的创建步骤抽象出来,形成一个通用的构建流程,同时允许根据不同的需求生成不同形式的对象。

(二)生成器模式的结构

生成器模式主要包含四个角色:

  1. Builder(生成器):这是一个抽象类或接口,定义了构建复杂对象各个部分的抽象方法。它类似于蛋糕制作流程的大纲,规定了制作蛋糕需要进行哪些步骤,但不涉及具体的制作细节。在创建保险合同对象的场景中,生成器接口可能定义了设置合同编号、被保险方信息、生效和失效日期等方法。
  2. ConcreteBuilder(具体生成器):实现了Builder接口,具体实现构建复杂对象各个部分的方法。好比不同风格蛋糕的制作师傅,按照基本流程大纲,用各自的技巧制作出不同风格的蛋糕。在保险合同场景中,具体生成器类会根据保险合同的具体要求,实现设置各个属性的方法。
  3. Product(产品):就是要构建的复杂对象。在这个例子中,保险合同对象就是产品,它包含多个属性和相关的操作方法。
  4. 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();
    }
}

五、生成器模式的应用场景与优缺点

(一)应用场景

  1. 创建复杂对象的场景:当需要创建的对象结构复杂,包含多个部分,且这些部分的构建过程比较复杂时,生成器模式非常适用。比如在一个电商系统中,创建订单对象时,订单不仅包含基本的订单信息(如订单编号、客户信息),还可能包含订单明细(商品列表、价格计算)、支付信息、物流信息等多个部分。使用生成器模式,可以将这些复杂的构建过程分离,使得代码更加清晰,易于维护。
  2. 需要创建不同表示形式的场景:如果同样的构建过程需要创建不同表示形式的对象,生成器模式就派上用场了。例如在一个文档生成系统中,需要将相同的数据以Word文档、PDF文档和HTML网页三种不同的格式输出。通过生成器模式,可以定义一个通用的构建过程,然后由不同的具体生成器来生成不同格式的文档。
  3. 对象构建过程需要分步进行的场景:当对象的构建过程需要按照一定的顺序分步进行时,生成器模式可以很好地满足需求。比如在一个游戏开发中,创建角色的过程可能需要先初始化基本属性(如生命值、魔法值),再添加装备,最后设置技能。使用生成器模式,指挥者可以按照这个顺序调用生成器的方法来构建角色。

(二)优点

  1. 解耦构建过程和表示:将复杂对象的构建过程和最终表示分离,使得代码更加清晰,易于维护。就像制作蛋糕,制作师傅专注于制作过程,装饰师傅负责蛋糕的外观,各司其职,代码结构也更加清晰。
  2. 提高可扩展性:当需要添加新的表示形式时,只需要添加新的具体生成器类,符合开闭原则。例如在文档生成系统中,如果要新增一种文档格式,只需要创建一个新的具体生成器类,而不需要修改其他代码。
  3. 便于控制构建过程:通过指挥者可以方便地控制
;