TS 设计模式02 - 建造者模式

1. 简介

工厂模式,为我们将客户端的生产行为封装起来,交给了工厂。它本质上是服务于客户端的,并没有降低产品生产的难度,产品的生产逻辑仍然在自己的类内部实现。
对于一些复杂的产品类(工序多,参数多),我们需要在内部维护其复杂的构建逻辑,是很容易出错的。
举一个简单的例子,生产牛肉汉堡,我们不管是由客户端去生产,还是工厂帮我们生产,建造的逻辑始终写在其 constructor 内部。全部生产步骤可能包含,做面包,做牛肉,放蔬菜,每个步骤可能有不同的参数控制,比如几片面包,几片牛肉或者几片蔬菜。如果我们发现之前的工序不好,需要调整工序,要么在类内部进行修改(违法开闭),要么新增一个类(成本太大,也不好维护),或者说我们要做猪肉汉堡,步骤和工序是一样的,我们新建一个类时由于步骤复杂,可能漏了或写错了。
那怎么办呢,建造者模式就是帮助我们创建一个复杂对象的,它将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。比如我规定汉堡的构建工序是可以稳定地划分为,做面包,做肉,放蔬菜的,至于你具体是面包是做成方形的圆形的,肉要是什么类型,蔬菜是什么类型,这是具体的实现步骤,对不同的汉堡具体的实现不一样。

2. 建造者模式

image.png

这里抽象建造者使用接口也是 okay 的。可以看到这里不管抽象的建造者还是具体的建造者依赖(关联)的都是 Hamburg,这个 Hamburg 其实也可以是一个父类或者抽象类。

class Hamburg {
    name: string;
    // 可选参数也可以使用 set,但是 ts 中直接在这里声明更方便, 当然如果是 private,需要使用 set 来封装
    meatType?: string;
    vegetableType?: string;
    private breadNum?: number;

    constructor(name: string) { // 如果我们不用建造者模式,那么产品类的 constructor 这里将要传入所有参数
        this.name = name; // 必选参数可以放在这里,步骤具体实现可变的就抽出来
    }

    // 利用 ts 的 set 当然也是 okay 的,比如 set num(num){ this.breadNum = num; }
    setBreadNum(num: number) {
        this.breadNum = num;
    }
}

// 原本放在产品类的构建步骤被转移到了建造者类,由具体的建造者实现
abstract class HamburgBuilder {
    abstract buildBread(breadNum: number): void;
    abstract buildMeat(meatType: string): void;
    abstract buildVegetable(vegetableTYpe: string): void;
    abstract createHamburg(): Hamburg;
}

class BeefHamburgBuilder extends HamburgBuilder {
    // 这里如果可以确定 name,就不需要用户再传入了
    private hamburg: Hamburg = new Hamburg('牛肉汉堡');

    buildBread(breadNum: number): void {
        console.log(`制作牛肉汉堡需要的 ${breadNum} 片面包`);
        this.hamburg.setBreadNum(breadNum);
    }

    buildMeat(meatType: string): void {
        console.log(`制作牛肉汉堡需要的 ${meatType}`);
        this.hamburg.meatType = meatType;
    }

    buildVegetable(vegetableType: string): void {
        console.log(`制作牛肉汉堡需要的 ${vegetableType}`);
        this.hamburg.vegetableType = vegetableType;
    }

    createHamburg(): Hamburg {
        return this.hamburg;
    }
}

class PorkHamburgBuilder extends HamburgBuilder {
    private hamburg: Hamburg = new Hamburg('猪肉汉堡');

    buildBread(breadNum: number): void {
        console.log(`制作猪肉汉堡需要的 ${breadNum} 片面包`);
        this.hamburg.setBreadNum(breadNum);
    }

    buildMeat(meatType: string): void {
        console.log(`制作猪肉汉堡需要的 ${meatType}`);
        this.hamburg.meatType = meatType;
    }

    buildVegetable(vegetableType: string): void {
        console.log(`制作猪肉汉堡需要的 ${vegetableType}`);
        this.hamburg.vegetableType = vegetableType;
    }

    createHamburg(): Hamburg {
        return this.hamburg;
    }
}

// 我们用 director 来封装顺序,如果要改变工序,只要新增一个 director 或者新增一个 construct 即可
class HamburgDirector {
    // 顺序1,包含三道工序
    static construct1(builder: HamburgBuilder, breadNum: number, meatType: string, vegetableType: string): Hamburg {
        builder.buildBread(breadNum);
        builder.buildMeat(meatType);
        builder.buildVegetable(vegetableType);
        return builder.createHamburg();
    }

    // 顺序2,包含两道工序
    static construct2(builder: HamburgBuilder, breadNum: number, meatType: string): Hamburg {
        builder.buildMeat(meatType);
        builder.buildBread(breadNum);
        return builder.createHamburg();
    }
}

const beefHamburgBuilder = new BeefHamburgBuilder();
const porkHamburgBuilder = new PorkHamburgBuilder();

HamburgDirector.construct1(beefHamburgBuilder, 2, 'beef', 'carrot');
HamburgDirector.construct2(porkHamburgBuilder, 3, 'pork');
image.png

3. 改进构造调用

目前导演类在调用建造者进行建造时,建造步骤如果一多会显得不清晰。我们可以使用链式调用方法来进行优化。

class Hamburg {
    name: string;
    // 可选参数也可以使用 set,但是 ts 中直接在这里声明更方便, 当然如果是 private,需要使用 set 来封装
    meatType?: string;
    vegetableType?: string;
    private breadNum?: number;

    constructor(name: string) { // 如果我们不用建造者模式,那么产品类的 constructor 这里将要传入所有参数
        this.name = name; // 必选参数可以放在这里,步骤具体实现可变的就抽出来
    }

    // 利用 ts 的 set 当然也是 okay 的,比如 set num(num){ this.breadNum = num; }
    setBreadNum(num: number) {
        this.breadNum = num;
    }
}

// 原本放在产品类的构建步骤被转移到了建造者类,由具体的建造者实现
abstract class HamburgBuilder {
    abstract buildBread(breadNum: number): HamburgBuilder;
    abstract buildMeat(meatType: string): HamburgBuilder;
    abstract buildVegetable(vegetableTYpe: string): HamburgBuilder;
    abstract createHamburg(): Hamburg;
}

class BeefHamburgBuilder extends HamburgBuilder {
    // 这里如果可以确定 name,就不需要用户再传入了
    private hamburg: Hamburg = new Hamburg('牛肉汉堡');

    buildBread(breadNum: number): HamburgBuilder {
        console.log(`制作牛肉汉堡需要的 ${breadNum} 片面包`);
        this.hamburg.setBreadNum(breadNum);
        return this;
    }

    buildMeat(meatType: string): HamburgBuilder {
        console.log(`制作牛肉汉堡需要的 ${meatType}`);
        this.hamburg.meatType = meatType;
        return this;
    }

    buildVegetable(vegetableType: string): HamburgBuilder {
        console.log(`制作牛肉汉堡需要的 ${vegetableType}`);
        this.hamburg.vegetableType = vegetableType;
        return this;
    }

    createHamburg(): Hamburg {
        return this.hamburg;
    }
}

class PorkHamburgBuilder extends HamburgBuilder {
    private hamburg: Hamburg = new Hamburg('猪肉汉堡');

    buildBread(breadNum: number): HamburgBuilder {
        console.log(`制作猪肉汉堡需要的 ${breadNum} 片面包`);
        this.hamburg.setBreadNum(breadNum);
        return this;
    }

    buildMeat(meatType: string): HamburgBuilder {
        console.log(`制作猪肉汉堡需要的 ${meatType}`);
        this.hamburg.meatType = meatType;
        return this;
    }

    buildVegetable(vegetableType: string): HamburgBuilder {
        console.log(`制作猪肉汉堡需要的 ${vegetableType}`);
        this.hamburg.vegetableType = vegetableType;
        return this;
    }

    createHamburg(): Hamburg {
        return this.hamburg;
    }
}

// 我们用 director 来封装顺序,如果要改变工序,只要新增一个 director 或者新增一个 construct 即可
class HamburgDirector {
    // 顺序1,包含三道工序
    static construct1(builder: HamburgBuilder, breadNum: number, meatType: string, vegetableType: string): Hamburg {
        return builder.buildBread(breadNum)
            .buildMeat(meatType)
            .buildVegetable(vegetableType)
            .createHamburg();
    }

    // 顺序2,包含两道工序
    static construct2(builder: HamburgBuilder, breadNum: number, meatType: string): Hamburg {
        return builder.buildMeat(meatType)
            .buildBread(breadNum)
            .createHamburg();
    }
}

const beefHamburgBuilder = new BeefHamburgBuilder();
const porkHamburgBuilder = new PorkHamburgBuilder();

HamburgDirector.construct1(beefHamburgBuilder, 2, 'beef', 'carrot');
HamburgDirector.construct2(porkHamburgBuilder, 3, 'pork');

4. 小结

建造者模式的核心其实就是将具体的建造过程提取出来,进行封装。构建步骤封装在建造者,构建顺序封装在导演类。你可以一个导演类对应于一个建造者,也可以对应对个建造者,甚至你如果不用导演类,由客户端来选择构建顺序也是 okay 的。

参考

一篇文章就彻底弄懂建造者模式(Builder Pattern)
建造者模式 | 菜鸟教程
[book - 大话设计模式]
[book - 设计模式之禅]
建造者模式的简单例子
秒懂设计模式之建造者模式(Builder pattern)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,744评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,505评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,105评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,242评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,269评论 6 389
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,215评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,096评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,939评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,354评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,573评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,745评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,448评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,048评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,683评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,838评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,776评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,652评论 2 354

推荐阅读更多精彩内容