设计模式与生活⑯:天冷了,出门这身得一层层配

周日晚上,把电车推到楼下充电。

天冷了,电池掉得快。以前三五天充一次,现在隔一天就得充一回。充满,周一早上骑到地铁站,晚上再骑回来,心里才踏实。

回屋翻衣柜。周一得穿厚一点。薄外套收进去,把羽绒服拽出来,袖口还留着上一个冬天的褶。

顺手把围巾和手套翻出来,搁在椅背上。

周一早上闭着眼也知道:先穿秋衣,再穿毛衣,最后套羽绒服;围巾塞进领口,手套戴上,背包甩到肩上。这个顺序一乱,穿上就别扭------先背包再套外套,背带勒在衣服里,取东西还得先把外套掀起来。

一件件穿,跟代码里造一个复杂对象,是一回事。

天冷了,出门前得一层层配

今天聊的生成器模式(Builder,也叫建造者模式):把一个复杂对象的装配过程拆成一步一步,每步只干一件事,最后一次性交出成品。系列第十六篇。

01 先看"一口气把参数塞进去"的办法

最直接的写法,是把这身衣服做成一个构造器,参数一次传齐。

java 复制代码
class Outfit {
    private final String base;
    private final String mid;
    private final String outer;
    private final List<String> extras;

    Outfit(String base, String mid, String outer, List<String> extras) {
        this.base = base;
        this.mid = mid;
        this.outer = outer;
        this.extras = List.copyOf(extras);
    }

    void describe() {
        System.out.println("打底:" + base);
        System.out.println("中层:" + mid);
        System.out.println("外层:" + outer);
        System.out.println("配件:" + (extras.isEmpty() ? "无" : String.join("、", extras)));
    }
}

四个参数,一次传齐

调用:

java 复制代码
Outfit commute = new Outfit(
        "纯棉秋衣", "加绒毛衣", "短款羽绒服",
        List.of("针织围巾", "防风手套"));

能穿,但看不出哪个是哪个

第一眼看上去还行。但三个 String 挨在一起,mid 和 outer 传反了,编译器一声不吭。哪天想在中间加一层"马甲",构造器的参数列表一改,所有 new Outfit(...) 的地方都得跟着改。

再往后,这身衣服可能还要带"是否防水""是否带帽",参数越堆越长。调用处变成一串看不出含义的字面量,写的人得对着构造器数位置。

穿衣服的人不这么干。没人早上站在衣柜前背参数顺序,是一件一件往身上套。

02 把穿衣拆成几步

把这个过程搬到代码里,就是 Builder。

先让每一步都有一个名字:

java 复制代码
class OutfitBuilder {
    private String base;
    private String mid;
    private String outer;
    private final List<String> extras = new ArrayList<>();

    OutfitBuilder base(String layer) {
        this.base = layer;
        return this;
    }

    OutfitBuilder mid(String layer) {
        this.mid = layer;
        return this;
    }

    OutfitBuilder outer(String layer) {
        this.outer = layer;
        return this;
    }

    OutfitBuilder extra(String item) {
        extras.add(item);
        return this;
    }
}

每一步只存一样东西

每个方法返回 this,所以能一路点下去。

调用变成:

java 复制代码
Outfit commute = new OutfitBuilder()
        .base("纯棉秋衣")
        .mid("加绒毛衣")
        .outer("短款羽绒服")
        .extra("针织围巾")
        .extra("防风手套")
        .build();

读起来就是穿衣顺序

打底、中层、外层、配件。哪个参数是什么,方法名自己说了。想在中间加一层,加一个方法就行,已经在用的调用处不用动。

03 build 的时候,检查一遍

build() 是最后那一下:把攒着的字段交出去,生成一个成品。

java 复制代码
Outfit build() {
    if (base == null) {
        throw new IllegalStateException("打底不能少");
    }
    if (outer == null) {
        throw new IllegalStateException("外套不能少");
    }
    return new Outfit(base, mid, outer, extras);
}

出门前看一眼,缺什么直接拦下

这一步构造器里也能写,但 Builder 把"攒"和"交"分开,校验放到最后一步更自然。秋天能少穿一层,冬天不行------打底和外套是必填,中间层和配件可有可无。 这些规则写进 build(),就不会出现"只穿着毛衣就出门"的对象。

成品 Outfit 的字段是 final,构造器没有对外开放。build 出来就是这身,外面改不了。

04 同一套步骤,配出不同组合

周一要骑电车,风大,手套得戴上,围巾得围好。换成不那么冷的日子,同一套步骤又能配出另一身:

java 复制代码
Outfit mildDay = new OutfitBuilder()
        .base("纯棉秋衣")
        .mid("薄羊毛衫")
        .outer("呢子大衣")
        .build();

再试一次只给中层就 build():

java 复制代码
try {
    new OutfitBuilder().mid("加绒毛衣").build();
} catch (IllegalStateException e) {
    System.out.println("拦截:" + e.getMessage());
}

两身都 describe() 一遍,连同刚才被拦下的那次,运行结果:

text 复制代码
打底:纯棉秋衣
中层:加绒毛衣
外层:短款羽绒服
配件:针织围巾、防风手套

打底:纯棉秋衣
中层:薄羊毛衫
外层:呢子大衣
配件:无

拦截:打底不能少

同一套流程,配出两身

如果每天早上都是同一套顺序,还可以再抽一层指挥者(Director),把顺序写死:

java 复制代码
class CommuteOutfit {
    static Outfit build() {
        return new OutfitBuilder()
                .base("纯棉秋衣")
                .mid("加绒毛衣")
                .outer("短款羽绒服")
                .extra("针织围巾")
                .extra("防风手套")
                .build();
    }
}

调用方只说一句 CommuteOutfit.build()。这就是"闭着眼穿的那套"。

但别急着抽。只有一套固定组合时,new OutfitBuilder()...build() 写在调用处已经够清楚,再加一层指挥者,只是把代码挪了个地方。

05 这套穿法,什么时候别用

先把它和前面两个容易混的分开。

装饰器(Day 5)是"穿好之后还能一层层加" :骑电车冬天加挡风被、加加热把套,是运行时往上套,随时能拆。生成器是"装配的时候一层层配",build 完就定型。

拿穿衣打比方,装饰器是到公司热了脱外套、出门冷了再穿上;生成器是出门前把这身配好,配好就不再变。

工厂(Day 2)回答的是"造哪一种",生成器回答的是"这一种怎么一步步造出来"。 点餐时说"来个麦辣鸡腿堡",是工厂;这堡的面包、肉、酱怎么一层层码起来,是生成器。工厂管结果类型,生成器管装配过程。

然后是别用的地方。

字段就两三个,构造器直接 new 更省事。 为了两个参数造一个 Builder 类,纯属多一层。

装配没有顺序、没有必填项,Builder 的校验也用不上。 那它就只是一段更长的构造器调用。

还有一处失真得挑明:现实里衣服穿错了能脱下来重穿,代码里 Builder 的步骤顺序乱了,如果不校验,build 出来的对象照样能跑 ------只是这身衣服不合逻辑。所以"顺序约束"不是白送的,得自己写进 build() 里。

06 周一早上,出门

闹钟响的时候,天还没亮透。

秋衣、毛衣、羽绒服,一件件套上。拉链拉到顶,围巾裹严实,手套戴上,背包甩上肩。

电车昨晚充满了,拧开钥匙,电量满格。

天冷是真的冷。但这身是一层层配出来的,哪层该在里头,哪层该在外头,出门前就想清楚了。

装配归 Builder,成品归 Outfit,中间那一步 build 负责收口

跟前面十五篇对一句:单例管造几个,工厂管怎么造,模板方法管事情怎么走,策略管这一步用什么做法,装饰器管外面加几层,命令管把请求做成对象,代理管谁能碰到真东西,观察者管变化后通知谁,外观管一堆零件只留一个入口,状态管当前状态下怎么做,责任链管请求由哪一环接住,原型管拿哪个现成对象复制,桥接管抽象和实现怎么独立扩展,享元管把能共享的部分只留一份,备忘录管把某个时刻的状态存下来、再原样恢复。本篇生成器,管把复杂对象的装配拆成一步一步,最后一次性成型。

带走一句话:生成器 = 把复杂对象的装配拆成一步一步,每一步只管一件事,最后一次性成型。

衣服一件件穿,日子一天天过。

周一就周一吧。


参考资料

相关推荐
心若自由何处是束缚19 小时前
适配器模式:从原理到实战的完整指南
设计模式·适配器模式
是2的10次方啊1 天前
设计模式与生活⑮:看到哪儿了,得有个地方替你记着
设计模式
2601_967979122 天前
设计模式 - 访问者模式
设计模式·访问者模式·应用场景·对象结构·操作分离
是2的10次方啊2 天前
设计模式与生活⑭:一栋楼,共用一份热
设计模式
是2的10次方啊3 天前
设计模式与生活⑬:你坐几号线,谁来运营是另一回事
设计模式
Tt有了t4 天前
设计模式速查
java·python·设计模式
一条Go4 天前
四个进程,一把锁:Alioth 的告警评测为什么不需要消息队列
后端·设计模式·架构
一缕82年的清风4 天前
被中产唾弃的“十元快剪”,藏着最狠的极客工程学
人工智能·设计模式
是2的10次方啊4 天前
设计模式与生活⑫:周报又到手,别从空白开始
设计模式