理解设计模式:建造者

2025-06-04

理解设计模式:建造者

原书中描述了23种经典的设计模式Design Patterns: Elements of Reusable Object-Oriented Software。这些模式为软件开发中经常重复出现的特定问题提供了解决方案。

在本文中,我将描述建造者模式的工作原理以及何时应用它。

建造者模式:基本思想

建造者模式一种设计模式,旨在为面向对象编程中各种对象创建问题提供灵活的解决方案。建造者模式的目的是将复杂对象的构造与其表示分离。——维基百科

将复杂对象的构造与其表示分离,以便相同的构造过程可以创建不同的表示 - 设计模式:可重用面向对象软件的元素

很多情况下,类的构造函数会包含一长串没有语义值的参数,或者并非所有类的实例都会使用这些参数。这会导致构造函数的参数列表过长,或者必须定义许多具有不同参数的构造函数,从而导致类中的构造函数方法数量激增。

下面的代码显示了一个经典问题,其中有一个带有必须初始化的参数列表的构造函数,即使所讨论的对象不需要在其某些属性中具有值。

    new User('carlos', 'Caballero', 26, true, true, false, null, null);
Enter fullscreen mode Exit fullscreen mode

建造者模式可以让我们写出更清晰的代码,因为它避免了上述问题。该模式的UML图如下:

来自设计模式:可重用面向对象软件的元素的 UML 图。

组成该模式的类别如下:

  • 产品是构建过程的具体结果。也就是说,它们将成为我们应用程序的模型。

  • Builder是具体建造者的通用接口。

  • ConcreteBuilder是构造过程的不同实现。这些类将负责明确各个对象构造过程的业务逻辑差异。

这些类将负责明确各个对象构造过程的业务逻辑之间的差异。

  • Director定义了构造步骤的执行顺序。其目的是实现特定配置的可重用性。Director在此模式的某些实现中可以省略,但强烈建议使用,因为它将客户端从具体的构造步骤中抽象出来。

  • 客户端是使用该模式的类。有两种可能性:

1 – 客户端使用ConcreteBuilder,逐一执行构造步骤。

2 – 客户端使用实现每个构造过程,并充当和类Director之间的中介ClientConcreteBuilder

建造者模式:何时使用

  1. Builder 模式解决的问题很容易识别:当必须使用具有非常长的参数列表的构造函数或存在具有不同参数的较长构造函数列表时,应该使用此模式。

  2. 当需要构建同一对象的不同表示时。也就是说,当需要具有不同特征的同一类的对象时。

建造者模式:优点和缺点

Builder模式有很多优点,可以总结为以下几点:

  • 可以逐步创建对象。

  • 对象的创建可以推迟到所有构建对象的必要信息都准备好之后。直到build执行 Builder 类的方法时才会获取该对象。

  • 干净的代码:应用单一职责原则(SRP),因为对象的复杂构造与该对象的业务逻辑隔离。

然而,建造者模式的主要缺点是增加了代码的复杂性,以及所需的类数量。这是应用设计模式时众所周知的缺点,因为这是获得代码抽象必须付出的代价。

接下来我们来说明一下建造者模式的三个应用示例:

  1. Builder 模式的基本结构。在此示例中,我们将理论 UML 图转换为 TypeScript 代码,以便识别该模式中涉及的每个类。

  2. 电子游戏中角色的创建。让我们回想一下经典的魔兽世界(WoW ) 场景,其中玩家可以在两个种族之间进行选择:人类和兽人。

  3. 销售点(POS)创建产品(汉堡)。

以下示例将展示如何使用 TypeScript 实现此模式。我们选择使用 TypeScript 而不是 JavaScript 来实现此模式,因为 JavaScript 缺乏接口或抽象类,因此实现接口和抽象类的责任将落在开发人员身上。


示例 1 — 建造者模式的基本结构

在第一个示例中,我们将把理论 UML 图转换为 TypeScript 代码,以测试此模式的潜力。要实现的 UML 图如下:

建造者模式基本结构的类图。

首先,我们要定义Product问题的模型()。在这个类中,我们将拥有一个由字符串组成的零件列表。为此,我们定义了经典的addPartremovePartshowParts方法来管理此属性。

但需要注意的是,对象的构造函数不会接收初始参数列表(在 TypeScript 中不需要定义它),但模型属性将通过方法进行修改。

 export class Product {
    public parts: string[] = [];

    public addPart(part: string): void {
        this.parts.push(part);
    }
    public removePart(part: string): void {
        this.parts = this.parts.filter(_part => _part !== part);
    }

    public showParts(): void {
        console.log(`Product parts: ${this.parts.join(', ')}\n`);
    }
}
Enter fullscreen mode Exit fullscreen mode

下一步是创建定义具体构建器的构建器接口。在构建器中,定义了添加和移除每个部件(A、B 和 C)的操作。

export interface Builder {
    addPartA(): void;
    addPartB(): void;
    addPartC(): void;
    removePartA(): void;
    removePartB(): void;
    removePartC(): void;
}
Enter fullscreen mode Exit fullscreen mode

具体构建器类拥有我们要构建的类的私有对象(Product)。我们将根据具体情况对其属性进行必要的修改,以构建相应的对象。

请注意,构造函数的作用是初始化产品,并且有一个build方法负责返回类中已配置的对象ConcreteBuilder1,并重置内部对象以便能够构建另一个对象。在调用ConcreteBuilder1该方法之前,类会配置一个具体的对象。build

请注意,构造函数的作用是初始化产品,并且有一个build方法负责返回ConcreteBuilder1类中已配置的对象,并重置内部对象以便能够构建另一个对象。在调用ConcreteBuilder1该方法之前,类会配置一个具体的对象。build

import { Builder } from "./builder.interface";
import { Product } from "./product";

export class ConcreteBuilder1 implements Builder {
    private product: Product;

    constructor() {
        this.reset();
    }

    public reset(): void {
        this.product = new Product();
    }

    /**
     * Steps
     */
    public addPartA(): void {
        this.product.addPart('PartA1');
    }

    public addPartB(): void {
        this.product.addPart('PartB1');
    }

    public addPartC(): void {
        this.product.addPart('PartC1');
    }

    public removePartA(): void {
        this.product.removePart('PartA1');
    }

    public removePartB(): void {
        this.product.removePart('PartB1');
    }

    public removePartC(): void {
        this.product.removePart('PartC1');
    }

    public build(): Product {
        const result = this.product;
        this.reset();
        return result;
    }
}
Enter fullscreen mode Exit fullscreen mode

一旦我们有了通过类构建对象的具体操作ConcreteBuild1,下一步就是定义执行不同构造的具体步骤。Director类负责定义使用 Builder 对象指定构造步骤的方法。

因此,该类Director从 Builder 类接收一个对象作为参数(在本例中为 BuilderConcrete1),并且定义了几个构造:

  1. BasicObject→ 它仅由 A 部分组成。

  2. FullObject→ 它由 A、B 和 C 部分组成。

import { Builder } from "./builder.interface";

export class Director {
    private builder: Builder;

    public setBuilder(builder: Builder): void {
        this.builder = builder;
    }

    public buildBasicObject(): void {
        this.builder.addPartA();
    }

    public buildFullObject(): void {
        this.builder.addPartA();
        this.builder.addPartB();
        this.builder.addPartC();
    }
}
Enter fullscreen mode Exit fullscreen mode

最后,需要定义使用该模式的Client或类。这个客户端非常简洁,因为您只需定义要使用的对象,并通过 调用对象的创建ContextBuilderDirector

import { ConcreteBuilder1 } from './concrete-builder1';
import { Director } from './director';

function client(director: Director) {
    const builder = new ConcreteBuilder1();
    director.setBuilder(builder);

    console.log('A preconfigured basic object:');
    director.buildBasicObject();
    builder.build().showParts();

    console.log('A preconfigured full object:');
    director.buildFullObject();
    builder.build().showParts();

    // A custom object can be create without a Director class.
    console.log('Custom product:');
    builder.addPartA();
    builder.addPartC();
    builder.build().showParts();
}

const director = new Director();
client(director);
Enter fullscreen mode Exit fullscreen mode

示例 2 — 创建视频游戏英雄

一旦提出了经典的理论示例来理解模式中每个类的职责,我们将提出另一个示例,其中我们用特定的问题来识别每个类。

我们的问题是如何在电子游戏中表现不同的英雄或角色。我们将重点关注经典的《魔兽世界》 (WoW ),其中的英雄可以分为两个种族:人类和兽人。此外,每个英雄的或 属性值会有所不同armor具体取决于英雄是人类还是兽人。weaponskills

如果不应用建造者模式Hero 类中就会定义一个构造函数,其中包含一长串参数(racearmorskills...),而这些参数又会定义在构造函数中,用于判断盔甲是人类还是兽人。因此,这个初始解决方案存在耦合问题,因为业务逻辑的任何变化都会导致大量代码重写,而且几乎没有复用的可能性。

如果不应用建造者模式,Hero 类中就会定义一个构造函数,其中包含一长串参数(racearmorskills...),而这又会导致构造函数中定义逻辑来判断盔甲是人类还是兽人。这种初始解决方案存在耦合问题,因为业务逻辑的任何更改都需要重写大量代码,而且几乎没有复用的可能性。

因此,我们首先要停下来思考一下建造者模式是如何帮助我们解决这个问题的。因此,我们专注于展示解决这个问题的UML图,并开始实现它。

建造者模式应用于视频游戏中的英雄创建问题。

在这个例子中,我们将遵循与前一个例子相同的顺序,并从我们想要灵活构建的模型或对象开始。

Hero 类定义了racearmor和属性,在我们的示例中,为了简单起见weaponskills它们都是简单的字符串。所有这些属性都可以是对象,但为了简化示例,我们将它们保留为字符串。

export class Hero {
    public race: string;
    public armor: string;
    public weapon: string;
    public skills: string[];


   public toString(): string {
        return `Hero:
                   race=${this.race ? this.race : 'empty'}
                   armor=${this.armor ? this.armor: 'empty'}
                   weapon=${this.weapon ? this.weapon: 'empty'}
                   skills=${this.skills ? this.skills: 'empty'}
                 `;
    }
}
Enter fullscreen mode Exit fullscreen mode

HeroBuilder接口定义了特定构建器将拥有的方法。我们可以看到,我们将逐步配置 Hero 对象,每个方法都允许配置该对象:setArmorsetWeaponsetSkills;最后,我们将拥有build完成对象配置并提取Hero对象的方法。

import { Hero } from "./hero.model";

export abstract class HeroBuilder {
    protected hero: Hero;

    public abstract setArmor(): void;
    public abstract setWeapon(): void;
    public abstract setSkills(): void;

    public abstract build(): Hero;
}
Enter fullscreen mode Exit fullscreen mode

一旦定义了构建器(作为抽象类或接口),我们就必须构建问题所需的两个特定构建器:HumanHeroBuilder 和 OrcHeroBuilder。在演示代码中,我们根据每个构建器使用不同的字符串完成。需要注意的是,build每个构建器的方法都会返回构建的对象(Hero),并重置该对象的状态以便能够构建另一个对象。

import { Hero } from "./hero.model";
import { HeroBuilder } from "./hero-builder";

export class HumanHeroBuilder extends HeroBuilder {

    constructor() {
        super();
        this.reset();
    }

    public reset() {
        this.hero = new Hero();
        this.hero.race = "Human";
    }

    public setArmor():void {
        this.hero.armor = "Human armor";
    }

    public setWeapon(): void {
        this.hero.weapon = 'Human weapon';
    }

    public setSkills(): void {
        this.hero.skills = ['Human skill1', 'Human skill2'];
    }

    public build(): Hero {
        const hero = this.hero;
        this.reset();
        return hero;
    }
}
Enter fullscreen mode Exit fullscreen mode
import { Hero } from "./hero.model";
import { HeroBuilder } from "./hero-builder";

export class OrcHeroBuilder extends HeroBuilder {

    constructor() {
        super();
        this.reset();
    }

    public reset() {
        this.hero = new Hero();
        this.hero.race = "Orc";
    }

    public setArmor():void {
        this.hero.armor = "Orc armor";
    }

    public setWeapon(): void {
        this.hero.weapon = 'Orc weapon';
    }

    public setSkills(): void {
        this.hero.skills = ['Orc skill1', 'Orc skill2'];
    }

    public build(): Hero {
        const hero = this.hero;
        this.reset();
        return hero;
    }
}
Enter fullscreen mode Exit fullscreen mode

该模式的最后一个元素是Hero-Director允许您存储在整个代码中重复使用的配置的类。在我们的示例中,我们创建了三种Hero创建设置。例如,该createHero方法构建了一个完整的英雄,即它分配了盔甲、技能和武器。此外,我们还通过该createHeroBasic方法创建了一个没有任何装备的英雄。最后,为了说明另一种配置,我们createHeroWithArmor定义了一个方法,它返回一个仅分配了盔甲的英雄。

import { HeroBuilder } from "./hero-builder";

export class HeroDirector {

    public createHero (heroBuilder: HeroBuilder) {
        heroBuilder.setArmor();
        heroBuilder.setSkills();
        heroBuilder.setWeapon();
        return heroBuilder.build();
  }

  public createHeroBasic (heroBuilder: HeroBuilder){
    return heroBuilder.build();
  }

  public createHeroWithArmor(heroBuilder: HeroBuilder){
    heroBuilder.setArmor();
    return heroBuilder.build();
 }

}
Enter fullscreen mode Exit fullscreen mode

最后,我们将展示一个控制台客户端,它利用了我们在本例中构建的两个构建器。在本例中,我们创建了两个构建器:HumanHeroBuilderOrcHeroBuilder;以及导演的类:HeroDirector。作为演示,我们将结合使用这两个构建器和导演来创建该类HeroDirector预先配置的三个英雄配置。

import { HeroDirector } from "./hero-director";
import { HumanHeroBuilder } from "./human-hero-builder";
import { OrcHeroBuilder } from "./orc-hero-builder";

const humanBuilder = new HumanHeroBuilder();
const orcBuilder = new OrcHeroBuilder();
const heroDirector = new HeroDirector();

const humanHero = heroDirector.createHero(humanBuilder);
const humanHeroWithArmor = heroDirector.createHeroWithArmor(humanBuilder);
const humanHeroBasic = heroDirector.createHeroBasic(humanBuilder);

console.log(humanHero.toString());
console.log(humanHeroWithArmor.toString());
console.log(humanHeroBasic.toString());

const orcHero = heroDirector.createHero(orcBuilder);
const orcHeroWithArmor = heroDirector.createHeroWithArmor(orcBuilder);
const orcHeroBasic = heroDirector.createHeroBasic(orcBuilder);

console.log(orcHero.toString());
console.log(orcHeroWithArmor.toString());
console.log(orcHeroBasic.toString());
Enter fullscreen mode Exit fullscreen mode

示例 3 — 制作汉堡(销售点)

在下面的示例中,我们将为一家汉堡餐厅创建一个 POS 系统。与之前的示例相比,此示例的主要变化在于,每个对要创建对象的修改操作都将返回构建器本身,而不是不返回任何值。这样,构建器本身执行的不同操作可以串联起来,因为每个操作都会返回Builder对象。

按照我们在前面的例子中提出的相同方法,我们将首先查看 UML 图,这将有助于我们识别该模式的每个部分。

构建器模式应用于销售点。

在这种情况下,我们要构建的对象将与一个类相对应,Burger该类包含一个用于配置每个汉堡的配料列表。该类Burger将具有与其每个属性相对应的访问器方法。

与此类相关的代码如下:

import { BurgerType } from "./burger-type.interface";

export class Burger {
    public type: BurgerType = BurgerType.NORMAL;
    public cheese = false;
    public lettuce = false;
    public tomato = false;
    public double = false;
    public onion = false;
    public pickle = false;
    public bacon = false;
    public chiliSauce = false;
    public egg = false;

    public setType(type: BurgerType){
        this.type = type;
    }

    public setCheese() {
        this.cheese = true;
    }

    public setLettuce() {
        this.lettuce = true;
    }

    public setTomate() {
        this.tomato = true;
    }

    public setDouble() {
        this.double = true;
    }

    public setOnion() {
        this.onion = true;
    }

    public setPickle() {
        this.pickle = true;
    }

    public setBacon() {
       this. bacon = true;
    }

    public setChiliSauce() {
        this.chiliSauce = true;
    }

    public setEgg() {
        this.egg = true;
    }
}
Enter fullscreen mode Exit fullscreen mode

在这个例子中,BurgerType包含了枚举类型,它允许定义应用程序中存在的不同类型的汉堡。

export enum BurgerType {
    NORMAL,
    CHEESE,
    VEGGIE,
    DOUBLE,
    CHEESE_BACON,
    DOTTECH,
    GODZILLA
}
Enter fullscreen mode Exit fullscreen mode

BurgerBuilder类中,每个方法都会对正在配置的对象执行修改操作,并且会返回构建器以便能够链接不同的操作。当然,该build方法仍然返回Burger类对象。

import { Burger } from "./burger.model";
import { BurgerType } from "./burger-type.interface";

export class BurgerBuilder {
    private burger: Burger;


    public constructor(){
        this.burger = new Burger();
    }

    public setType(type: BurgerType): BurgerBuilder{
        this.burger.setType(type);
        return this;
    }

    public setDouble(): BurgerBuilder{
        this.burger.setDouble();
        return this;
    }

    public addCheese(): BurgerBuilder{
        this.burger.setCheese();
        return this;
    }

    public addLettuce(): BurgerBuilder{
        this.burger.setLettuce();
        return this;
    }

    public addTomato(): BurgerBuilder{
        this.burger.setTomate();
        return this;
    }


    public addOnion(): BurgerBuilder{
        this.burger.setOnion();
        return this;
    }

    public addPickle(): BurgerBuilder{
        this.burger.setPickle();
        return this;
    }

    public addBacon(): BurgerBuilder{
        this.burger.setBacon();
        return this;
    }

    public addChiliSauce(): BurgerBuilder{
        this.burger.setChiliSauce();
        return this;
    }

    public addEgg(): BurgerBuilder{
        this.burger.setEgg();
        return this;
    }

    public build(): Burger{
        return this.burger;
    }
}
Enter fullscreen mode Exit fullscreen mode

该类BurgerDirector负责配置BurgerBuilder类中定义的操作。在这里,您可以看到如何使用链式方法配置不同类型的汉堡,从而方便阅读代码。需要记住的是,在该build方法执行之前,配置的都是同一个汉堡。

import { Burger } from "./burger.model";
import { BurgerBuilder } from "./burger-builder";
import { BurgerType } from "./burger-type.interface";

export class BurgerDirector {

    public constructor(private builder: BurgerBuilder){
        this.builder = builder;
    }

    public serveRegularBurger(): Burger{
        return this.builder
                    .setType(BurgerType.NORMAL)
                    .build();
    }

    public serveCheeseBurger() : Burger{
        return this.builder
                    .addCheese()
                    .setType(BurgerType.CHEESE)
                    .build();
    }

    public serveVeggieBurger(): Burger{
        return this.builder
                    .addCheese()
                    .addLettuce()
                    .addTomato()
                    .setType(BurgerType.VEGGIE)
                    .build();
    }

    public serverDoubleBurger(): Burger{
        return this.builder.setDouble()
                      .setType(BurgerType.DOUBLE)
                      .build();
    }


    public serveCheeseBaconBurger(): Burger{
        return this.builder.addCheese()
                      .addBacon()
                      .setType(BurgerType.CHEESE_BACON)
                      .build();
    }
}
Enter fullscreen mode Exit fullscreen mode

最后,我们向客户端展示了该模式的使用方法。在本例中,系统会选择一个随机数来定义汉堡的类型,然后调用主管来为我们提供该汉堡。

import { Burger } from "./burger.model";
import { BurgerBuilder } from "./burger-builder";
import { BurgerDirector } from "./buger-director";

let burger: Burger;

const burgerType = Math.round(Math.random() * 6);
console.log('BurgerType: ', burgerType);

const burgerBuilder: BurgerBuilder = new BurgerBuilder();
const burgerDirector: BurgerDirector =  new BurgerDirector(burgerBuilder);


switch (burgerType) {
    case 1:
        burger = burgerDirector.serveRegularBurger();
        break;
    case 2:
        burger = burgerDirector.serveCheeseBurger();
        break;
    case 3:
        burger = burgerDirector.serveVeggieBurger();
        break;
    case 4:
        burger = burgerDirector.serverDoubleBurger();
        break;
    case 5:
        burger = burgerDirector.serveCheeseBaconBurger();
        break;
    case 6:
        burger = burgerDirector.serveDotTechBurger();
        break;
    default:
        burger = burgerDirector.serveGozillaBurger();
        break;
}

console.log(burger);
Enter fullscreen mode Exit fullscreen mode

最后,我创建了三个npm脚本,可以通过它们执行本文中介绍的代码:

    npm run example1
    npm run example2
    npm run example3
Enter fullscreen mode Exit fullscreen mode

GitHub 仓库:https://github.com/Caballerog/blog/tree/master/builder-pattern


结论

建造者是一种设计模式,它能让你避免使用带有长参数列表(这些参数并非总是必需的)的构造函数。它允许你以更灵活的方式构建特定对象的实例,因为你可以只配置那些绝对必要的属性。

代码更加简洁,因为构造函数中不会包含未使用的参数,只允许使用创建对象所需的参数。此外,由于Director构建器有一个类,对象创建配置可以复用,因此客户端无需与 Builder 类直接交互。

最后,关于这个模式最重要的不是它的具体实现,而是能够识别这个模式可以解决的问题,以及何时可以应用它。具体实现是最重要的,因为它会根据所使用的编程语言而变化。

文章来源:https://dev.to/carlillo/understanding-design-patterns-builder-5dg8
PREV
理解设计模式:使用 StockTrader 和 R2D2(星球大战)示例的命令模式!
NEXT
理解:Paw Patrol 解释的 JavaScript 中的上下文、范围、执行上下文和 8 种不同的 This 值!