理解设计模式:建造者
原书中描述了23种经典的设计模式Design Patterns: Elements of Reusable Object-Oriented Software
。这些模式为软件开发中经常重复出现的特定问题提供了解决方案。
在本文中,我将描述建造者模式的工作原理以及何时应用它。
建造者模式:基本思想
建造者模式是一种设计模式,旨在为面向对象编程中各种对象创建问题提供灵活的解决方案。建造者模式的目的是将复杂对象的构造与其表示分离。——维基百科
将复杂对象的构造与其表示分离,以便相同的构造过程可以创建不同的表示 - 设计模式:可重用面向对象软件的元素
很多情况下,类的构造函数会包含一长串没有语义值的参数,或者并非所有类的实例都会使用这些参数。这会导致构造函数的参数列表过长,或者必须定义许多具有不同参数的构造函数,从而导致类中的构造函数方法数量激增。
下面的代码显示了一个经典问题,其中有一个带有必须初始化的参数列表的构造函数,即使所讨论的对象不需要在其某些属性中具有值。
new User('carlos', 'Caballero', 26, true, true, false, null, null);
建造者模式可以让我们写出更清晰的代码,因为它避免了上述问题。该模式的UML图如下:
组成该模式的类别如下:
-
产品是构建过程的具体结果。也就是说,它们将成为我们应用程序的模型。
-
Builder是具体建造者的通用接口。
-
ConcreteBuilder是构造过程的不同实现。这些类将负责明确各个对象构造过程的业务逻辑差异。
这些类将负责明确各个对象构造过程的业务逻辑之间的差异。
-
Director定义了构造步骤的执行顺序。其目的是实现特定配置的可重用性。
Director
在此模式的某些实现中可以省略,但强烈建议使用,因为它将客户端从具体的构造步骤中抽象出来。 -
客户端是使用该模式的类。有两种可能性:
1 – 客户端使用ConcreteBuilder
,逐一执行构造步骤。
2 – 客户端使用实现每个构造过程,并充当和类Director
之间的中介。Client
ConcreteBuilder
建造者模式:何时使用
-
Builder 模式解决的问题很容易识别:当必须使用具有非常长的参数列表的构造函数或存在具有不同参数的较长构造函数列表时,应该使用此模式。
-
当需要构建同一对象的不同表示时。也就是说,当需要具有不同特征的同一类的对象时。
建造者模式:优点和缺点
Builder模式有很多优点,可以总结为以下几点:
-
可以逐步创建对象。
-
对象的创建可以推迟到所有构建对象的必要信息都准备好之后。直到
build
执行 Builder 类的方法时才会获取该对象。 -
干净的代码:应用单一职责原则(SRP),因为对象的复杂构造与该对象的业务逻辑隔离。
然而,建造者模式的主要缺点是增加了代码的复杂性,以及所需的类数量。这是应用设计模式时众所周知的缺点,因为这是获得代码抽象必须付出的代价。
接下来我们来说明一下建造者模式的三个应用示例:
-
Builder 模式的基本结构。在此示例中,我们将理论 UML 图转换为 TypeScript 代码,以便识别该模式中涉及的每个类。
-
电子游戏中角色的创建。让我们回想一下经典的魔兽世界(WoW ) 场景,其中玩家可以在两个种族之间进行选择:人类和兽人。
-
在销售点(POS)创建产品(汉堡)。
以下示例将展示如何使用 TypeScript 实现此模式。我们选择使用 TypeScript 而不是 JavaScript 来实现此模式,因为 JavaScript 缺乏接口或抽象类,因此实现接口和抽象类的责任将落在开发人员身上。
示例 1 — 建造者模式的基本结构
在第一个示例中,我们将把理论 UML 图转换为 TypeScript 代码,以测试此模式的潜力。要实现的 UML 图如下:
首先,我们要定义Product
问题的模型()。在这个类中,我们将拥有一个由字符串组成的零件列表。为此,我们定义了经典的addPart
、removePart
和showParts
方法来管理此属性。
但需要注意的是,对象的构造函数不会接收初始参数列表(在 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`);
}
}
下一步是创建定义具体构建器的构建器接口。在构建器中,定义了添加和移除每个部件(A、B 和 C)的操作。
export interface Builder {
addPartA(): void;
addPartB(): void;
addPartC(): void;
removePartA(): void;
removePartB(): void;
removePartC(): void;
}
具体构建器类拥有我们要构建的类的私有对象(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;
}
}
一旦我们有了通过类构建对象的具体操作ConcreteBuild1
,下一步就是定义执行不同构造的具体步骤。Director
类负责定义使用 Builder 对象指定构造步骤的方法。
因此,该类Director
从 Builder 类接收一个对象作为参数(在本例中为 BuilderConcrete1),并且定义了几个构造:
-
BasicObject
→ 它仅由 A 部分组成。 -
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();
}
}
最后,需要定义使用该模式的Client
或类。这个客户端非常简洁,因为您只需定义要使用的对象,并通过 调用对象的创建。Context
Builder
Director
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);
示例 2 — 创建视频游戏英雄
一旦提出了经典的理论示例来理解模式中每个类的职责,我们将提出另一个示例,其中我们用特定的问题来识别每个类。
我们的问题是如何在电子游戏中表现不同的英雄或角色。我们将重点关注经典的《魔兽世界》 (WoW ),其中的英雄可以分为两个种族:人类和兽人。此外,每个英雄的或 属性值会有所不同armor
,具体取决于英雄是人类还是兽人。weapon
skills
如果不应用建造者模式,Hero 类中就会定义一个构造函数,其中包含一长串参数(race
、armor
、skills
...),而这些参数又会定义在构造函数中,用于判断盔甲是人类还是兽人。因此,这个初始解决方案存在耦合问题,因为业务逻辑的任何变化都会导致大量代码重写,而且几乎没有复用的可能性。
如果不应用建造者模式,Hero 类中就会定义一个构造函数,其中包含一长串参数(race
、armor
、skills
...),而这又会导致构造函数中定义逻辑来判断盔甲是人类还是兽人。这种初始解决方案存在耦合问题,因为业务逻辑的任何更改都需要重写大量代码,而且几乎没有复用的可能性。
因此,我们首先要停下来思考一下建造者模式是如何帮助我们解决这个问题的。因此,我们专注于展示解决这个问题的UML图,并开始实现它。
在这个例子中,我们将遵循与前一个例子相同的顺序,并从我们想要灵活构建的模型或对象开始。
Hero 类定义了race
、armor
和属性,在我们的示例中,为了简单起见weapon
,skills
它们都是简单的字符串。所有这些属性都可以是对象,但为了简化示例,我们将它们保留为字符串。
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'}
`;
}
}
该HeroBuilder
接口定义了特定构建器将拥有的方法。我们可以看到,我们将逐步配置 Hero 对象,每个方法都允许配置该对象:setArmor
、setWeapon
和setSkills
;最后,我们将拥有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;
}
一旦定义了构建器(作为抽象类或接口),我们就必须构建问题所需的两个特定构建器: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;
}
}
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;
}
}
该模式的最后一个元素是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();
}
}
最后,我们将展示一个控制台客户端,它利用了我们在本例中构建的两个构建器。在本例中,我们创建了两个构建器:HumanHeroBuilder
和OrcHeroBuilder
;以及导演的类: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());
示例 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;
}
}
在这个例子中,BurgerType
包含了枚举类型,它允许定义应用程序中存在的不同类型的汉堡。
export enum BurgerType {
NORMAL,
CHEESE,
VEGGIE,
DOUBLE,
CHEESE_BACON,
DOTTECH,
GODZILLA
}
在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;
}
}
该类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();
}
}
最后,我们向客户端展示了该模式的使用方法。在本例中,系统会选择一个随机数来定义汉堡的类型,然后调用主管来为我们提供该汉堡。
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);
最后,我创建了三个npm
脚本,可以通过它们执行本文中介绍的代码:
npm run example1
npm run example2
npm run example3
GitHub 仓库:https://github.com/Caballerog/blog/tree/master/builder-pattern
结论
建造者是一种设计模式,它能让你避免使用带有长参数列表(这些参数并非总是必需的)的构造函数。它允许你以更灵活的方式构建特定对象的实例,因为你可以只配置那些绝对必要的属性。
代码更加简洁,因为构造函数中不会包含未使用的参数,只允许使用创建对象所需的参数。此外,由于Director
构建器有一个类,对象创建配置可以复用,因此客户端无需与 Builder 类直接交互。
最后,关于这个模式最重要的不是它的具体实现,而是能够识别这个模式可以解决的问题,以及何时可以应用它。具体实现是最重要的,因为它会根据所使用的编程语言而变化。
文章来源:https://dev.to/carlillo/understanding-design-patterns-builder-5dg8