[设计模式] 建造者模式 生成器模式 创建型 设计模式(五)

计算机科学 计算机科学 6821 人阅读 | 0 人回复

建造者模式 Builder 也叫做生成器模式 在正式开始建造者模式之前,先回顾下抽象工厂模式 本人的所有系列文章都是自己学习的记录过程,均有比较严格的先后顺序,如果不清楚抽象工厂模式可以先往前翻翻

从抽象工厂演化

抽象工厂模式是工厂模式的进一步抽象扩展 不仅仅可以创建某种等级结构的产品,可以创建一整个产品族的产品 如下图所示 比如ConcreteCreator1可以创建ConcreteProductA1和ConcreteProductB1 比如ConcreteCreator2可以创建ConcreteProductA2和ConcreteProductB2

一个产品族的两个产品,是相关联的或者有共同的约束,比如同一个厂家,运行在同一个平台下 image.png

示例

考虑这样一种场景: 你是一个品牌电脑的组装厂家(组装代工厂),你拥有电脑的各种零部件(简单起见,仅仅以主板和显示器为例) 各种零部件都由多个厂家生产(简单起见,假设只有华硕和戴尔) 请你根据用户需求提供一台“品牌”电脑

涉及到多个产品等级结构,而且购买品牌电脑,同一品牌就是同一厂家,这是共同限制,有产品族的概念,所以可以使用抽象工厂模式 工厂生产相关联的产品族下的产品,然后进行组装 如下图所示: image.png

对于具体的工厂ConcreteCreator1可以创建华硕产品族的产品,主板和显示器 对于具体的工厂ConcreteCreator2可以创建戴尔产品族的产品,主板和显示器

代码

image.png

package builder;

public interface MainBoard {
    String desc();
}

package builder;

public class DellMainBoard implements MainBoard {
    @Override
    public String desc() {
        return "DELL mainBoard";
    }
}

package builder;

public class AsusMainBoard implements MainBoard {
    @Override
    public String desc() {
        return "ASUS mainBoard";
    }
}

image.png

package builder;

public interface DisplayDevice {
    String Desc();
}
package builder;

public class DellDisplayDevice implements DisplayDevice {
    @Override
    public String Desc() {
        return "DELL display device";
    }
}

package builder;

public class AsusDisplayDevice implements DisplayDevice {
    @Override
    public String Desc() {
        return "ASUS display device";
    }
}

image.png

package builder;

public interface Creator {
    MainBoard createMainBoard();

    DisplayDevice createDisplayDevice();
}

package builder;

public class ConcreateCreatorDell implements Creator {
    @Override
    public MainBoard createMainBoard() {
        return new DellMainBoard();
    }

    @Override
    public DisplayDevice createDisplayDevice() {
        return new DellDisplayDevice();
    }
}

package builder;

public class ConcreateCreatorAsus implements Creator {
    @Override
    public MainBoard createMainBoard() {
        return new AsusMainBoard();
    }

    @Override
    public DisplayDevice createDisplayDevice() {
        return new AsusDisplayDevice();
    }
}

image.png 用户需要的是一台电脑,电脑类为Computer Computer包含主板和显示器部件 重写了toString方便查看信息,toString中调用了主板和显示器的desc()方法

package builder;

public class Computer {
    private MainBoard mainBoard;
    private DisplayDevice displayDevice;

    public MainBoard getMainBoard() {
        return mainBoard;
    }

    public void setMainBoard(MainBoard mainBoard) {
        this.mainBoard = mainBoard;
    }

    public DisplayDevice getDisplayDevice() {
        return displayDevice;
    }

    public void setDisplayDevice(DisplayDevice displayDevice) {
        this.displayDevice = displayDevice;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("Computer{");
        sb.append("mainBoard=").append(mainBoard.desc());
        sb.append(", displayDevice=").append(displayDevice.Desc());
        sb.append('}');
        return sb.toString();
    }
}

image.png 以上,我们就完成了需求 根据用户的需求,创建指定的产品族的产品,并且将这一系列的产品进行组合生成最终的用户需要的产品

抽象工厂的问题

通过,抽象工厂模式进行产品族零部件的生产,然后在客户端进行加工 完成了我们的需求,但是这其中有明显的问题

整个的组装细节与过程,全部暴露在客户端程序 客户端程序知道你所有的零部件类型,也知道你所有零部件实现的细节 顾客只是想购买一台电脑而已,人家为什么要关心电脑到底有哪些部件,到底如何组装的? 简言之,你自己隐私暴露,人家还不稀罕看,嫌烦!

而且,如果需要在多个场景中完成这个组装生成的过程,怎么办?将会出现大量的冗余代码 再者,如果组装逻辑发生变动,需要维护多个地方,难度非常大 所以一个很自然的想法就是将整个的组装逻辑进行封装

为此,我们新增加一个组装电脑类AssembleComputer 接受一个Creator 作为参数,借助于Creator进行零部件产品族的创建以及组装

package builder;

public class AssembleComputer {
    Creator creator;

    AssembleComputer(Creator creator) {
        this.creator = creator;
    }

    public Computer getComputer() {
        Computer computer = new Computer();
        MainBoard mainBoard = creator.createMainBoard();
        DisplayDevice displayDevice = creator.createDisplayDevice();
        computer.setMainBoard(mainBoard);
        computer.setDisplayDevice(displayDevice);
        return computer;
    }
}

测试代码 image.png 经过封装后,这段代码看起来清爽多了,组装的细节被封装到了组装类AssembleComputer之中 客户端不在需要大量冗余的代码,而且后续扩展和维护也比较容易

封装下的重构

上面的封装的组装逻辑中,我们先把所有的零部件全部都生产出来,然后在一口气进行组装 虽然是把产品的组装细节对客户端程序隐藏了 但是,产品的表示生产和组装过程仍旧是耦合在一起的,都耦合在了getComputer()方法中 是否还可以进一步的将Computer的各个组成部分与组装逻辑分离呢? 我们把工厂等级结构和组装电脑类重构下

package builder;

public interface CreatorRefactor {
    void assembleMainBoard();

    void assembleDisplayDevice();

    Computer getComputer();
}

package builder;

public class ConcreateCreatorDellRefactor implements CreatorRefactor {
    private Computer computer = new Computer();

    @Override
    public void assembleMainBoard() {
        computer.setMainBoard(new DellMainBoard());
    }

    @Override
    public void assembleDisplayDevice() {
        computer.setDisplayDevice(new DellDisplayDevice());
    }

    @Override
    public Computer getComputer() {
        return computer;
    }
}

package builder;

public class ConcreateCreatorAsusRefactor implements CreatorRefactor {
    private Computer computer = new Computer();

    @Override
    public void assembleMainBoard() {
        computer.setMainBoard(new AsusMainBoard());
    }

    @Override
    public void assembleDisplayDevice() {
        computer.setDisplayDevice(new AsusDisplayDevice());
    }

    @Override
    public Computer getComputer() {
        return computer;
    }
}

可以看得出来重构之后的代码中 对于工厂角色来说,不仅仅是生产零部件,生产已经成为基础功能,还需要完成这一步骤的组装工作 并且提供最终返回完整产品的方法 具体的工厂中,创建了一个具体的产品引用,并且实现了抽象工厂的规定的协议-->组装每个步骤以及最终返回具体产品 组装电脑类重构

package builder;

public class AssembleComputerRefactor {
    CreatorRefactor creatorRefactor;

    AssembleComputerRefactor(CreatorRefactor creatorRefactor) {
        this.creatorRefactor = creatorRefactor;
    }

    public Computer getComputer() {
        creatorRefactor.assembleMainBoard();
        creatorRefactor.assembleDisplayDevice();
        return creatorRefactor.getComputer();
    }
}

重构后的代码,组装电脑类AssembleComputerRefactor不在涉及到具体零部件的生产了 生产和每一个零件步骤的组装已经移交到抽象工厂角色中了 组装电脑类仅仅涉及的就是组装流程!流程!流程! 完全不关注具体的到底是什么东西 image.png image.png

小结

最终重构后的代码形式中: 生产产品的构造工厂CreatorRefactor,规定了负责构建一个完整产品的所有的步骤,并且返回最终产品 实际负责生产的具体工厂角色,实现规定的每个步骤,并且返回最终的具体的产品

AssembleComputerRefactor仅仅包含产品的构建逻辑,也就是加工步骤 将生产产品的所有的步骤,合理的组织在一起 也即是说,它所有的流程的每一个步骤的细节,都是工厂提供的,组装器这个流水线只是负责步骤的梳理安排

比如,穿衣服的所有步骤有:戴帽子,穿鞋子,穿袜子,穿裤子,穿内裤.... 那么,这个组装器 就是将这些步骤合理组织安排 不能先穿鞋子在穿袜子的对吧,应该是 穿内裤,穿裤子,穿袜子,穿鞋子,戴帽子.... 你看,步骤都是一样的,但是顺序可能是有要求的,最终返回结果 其实这就是建造者模式 将复杂产品的构建过程和具体的产品进行分离 管你到底是穿什么鞋子呢,反正你有穿鞋子这一步骤 管你到底穿哪条裤子,反正你得穿裤子,而且穿衣服的场景下得是先穿裤子才能穿鞋子 作者:程序员潇然 疯狂的字节X https://crazybytex.com/

意图

将复杂对象的构建与他的表示进行分离,使得同样的构建过程可以创建不同的表示 对象的构建与表示进行分离,就是类似组装过程与内部的零件的分离,一台电脑的内部表示是各种零件,构建就是组装的过程

结构

我们将前面的示例,转换为标准的建造者模式的称呼 image.png

角色含义 抽象建造者角色Builder 给出一个抽象接口,以规范产品对象各个组成部分之间的构造 这个抽象的接口给出来构造一个产品的所有步骤以及最终产品的获取协议(就是其中定义的方法)(上面示例中的CreatorRefactor) 具体的建造者ConcreteBuilder 创建具体的产品的工厂、建造者 1.需要实现Builder中规定的产品创建的所有步骤 2.建造完成后,提供产品实例对象 (上面示例中的ConcreateCreatorDellRefactor 和 ConcreateCreatorAsusRefactor) 指挥者、导演Director 指挥产品的整个建造过程,不涉及具体产品的细节,只关注抽象建造者角色Builder定义的各个步骤的组织安排 具体的ConcreteBuilder 才会关注生产的细节(上面示例中的AssembleComputerRefactor) 产品 Product 最终创建起来的一个复杂的产品对象实例(上面示例中的Computer)

代码示例

image.png

package buildPattern;

public interface Builder {
    void buildPart1();

    void buildPart2();

    Product buildProduct();
}

package buildPattern;

public class ConcreateBuilder implements Builder {
    private Product product = new Product();

    @Override
    public void buildPart1() {
//...
    }

    @Override
    public void buildPart2() {
//...
    }

    @Override
    public Product buildProduct() {
        return product;
    }
}

package buildPattern;

public class Product {
}

package buildPattern;

public class Director {

    private Builder builder;

    Director(Builder builder) {
        this.builder = builder;
    }

    public Product getProduct() {
        builder.buildPart1();
        builder.buildPart2();
        return builder.buildProduct();
    }
}

注意事项

  1. 上面示例中只有一个ConcreteBuilder,实际上当然可以有多个,他们都继承自抽象角色Builder
  2. 示例中Product为一个具体的类,当然可以变为抽象角色,这样所有的产品都是属于Product的
  3. Builder角色中,我们以组装电脑为例子,看起来好像必须是同一类产品,其实不是必然的 Builder与具体的业务逻辑没关系,你可以把它简单的理解为步骤 比如它定义了五个步骤buildPart1(); buildPart2(); ........ buildPart5(); 那么,比如汽车可能由五个生产步骤组成,比如房子可以有五个步骤建造 Builder约定的只是流程,只是步骤,具体的细节由具体的实现工厂ConcreteBuilder决定了 到底是五个盖房子的步骤,还是五个造车子的步骤,具体的ConcreteBuilder说了算
  4. 如果是非常抽象的几个步骤,完全都不是一个类型的东西,那么这个抽象的产品怎么办?他们都没有任何的共性 你可以将返回结果产品的步骤,也就是最终的步骤,从抽象角色Builder中拿出来,每个ConcreteBuilder自己返回自己的产品 或者 你可以提供一个标记接口,标记接口,标记接口,什么都不做,你就说 房子,车子,都是一种Product~~这样也可以解决

与抽象工厂的对比

最开始我们以抽象工厂模式引申出建造者模式 在建造者模式中重要角色为Director和Builder,其实你会发现,其中的Builder与抽象工厂的Creator是有相似点的 Director只不过是把ConcreteCreator中生产的产品族 进行组装

但是建造者模式中的Builder,重点不在于生产的零部件是什么,而是在于步骤的划分 当然每个步骤可能也是需要“生产”的 可以认为Builder是抽象工厂模式中的Creator的一个变种 Director是抽象工厂模式生产产品后的进一步加工 他的重点在于步骤的组织安排

抽象工厂模式仅仅关注到我生产出来了这一个产品族的各个产品 建造者模式则进一步关注这些东西怎么构成、组装成为一个更加复杂的产品的步骤

如果以生产汽车为例 抽象工厂模式在于产生某一产品族的零部件,比如 轮胎 发动机 底盘 建造者模式在于安排建造的过程,安装底盘 安装轮胎 安装发动机 建造者模式的组装的每个步骤中,可能需要先生产在组装,也可能只是多个加工步骤 与抽象工厂模式的对比是为了加深理解,如果反倒容易混淆,可以无视

使用场景

对于每种模式的使用场景,只需要理解透彻每种模式的意图即可 建造者模式的意图在于复杂对象的内部表示与创建过程进行分离,前提就是面对复杂对象的创建 比如 有很多品牌的笔记本电脑,电脑包括很多零部件 cpu 显卡 内存条 显示器等等 有很多品牌的汽车,汽车包括很多零部件 底盘 发动机 轮胎 轮毂 等等 游戏中有很多个人物角色 他们都有 性别 发型 肤色 衣服 皮肤 等等

如何构造这些复杂的对象 而且还能够容易新增加新品牌的笔记本电脑和汽车,增加新的人物角色,也就是扩展性好 你就可以考虑建造者模式 建造者模式中的Director作为指挥者、导演,仅仅关心步骤的顺序安排 不管什么品牌的笔记本电脑,步骤都是一样的,安装cpu 安装显卡 安装内存条...等等 不管是什么品牌的汽车,生产步骤是一样的,安装底盘,安装发动机...等等 不管什么样子的人物角色,创建步骤是一样的,设置性别,设置肤色...等等 具体的建造者ConcreteBuilder才会关心每个步骤到底做的是什么事情

将步骤与具体表示分离,当需要扩展时,Director部分完全不需要变动,只需要增加新的ConcreteBuilder 即可 通过新的ConcreteBuilder , Director就可以创建出来新的产品、人物角色

建造者模式的关键就在于,复杂的对象,构建过程与内部表示的分离 所以当有复杂的内部结构时,或者步骤之间具有严格的顺序关系就可以考虑建造者模式

步骤不一样是否可用? 上面反复强调,他们拥有相同的步骤 那么,如果一个产品拥有三个步骤,另外一个产品拥有五个步骤 是否还能够使用建造者模式呢? 当然也是可以的 在抽象的Builder角色中,你仍然需要设置五个步骤 但是对于生产只需要三个步骤的产品的那个ConcreteBuilder 你可以将 buildPart4();buildPart5(); 实现为空方法即可 比如 void buildPart4(){ //什么都不做。。。。 }

所以,假如说,你定义了一个抽象角色Builder,他有N个步骤,那么他就可以构造1~N个步骤下,可以实现的所有产品!!! 细节由具体的ConcreteBuilder决定就好了,当然,一般你并不会那么做

简化形式

设计模式都不是一成不变的,可以根据实际情况进行调整甚至变种 如果确定系统中,只需要一个具体的建造者的话,那么就可以省略抽象的Builder角色 抽象的Builder就是为了规范多个具体Builder建造者的行为,如果只有一个具体的建造者,则失去了意义 此时,这个具体的建造者,也充当了抽象的Builder的角色 image.png 如果已经省略了抽象的Builder 那么还可以继续省略Director角色 ConcreteBuilder,也充当了这个Director角色 ConcreteBuilder自己不仅仅实现所有步骤的细节,并且还负责组装 说白了就是Director中的方法逻辑移植到ConcreteBuilder中, 客户端从ConcreteBuilder中获取产品

建造者与构造方法

假设有一个MyObject类,他有很多属性,假定目前有v1~v7 ,总共7个 其中v1 和 v2 是必选,其余为可选属性 对于这种情况,我们经常使用层叠的构造方法 层层嵌套调用 但是这种方式不够清晰,比较容易犯错,而且,很多时候即使参数写颠倒了,也并不会一定导致编译器报错 另外一种方式就是借助于建造者模式的简化形式 如下面示例

package simplebuilder;

/**
 * Created by noteless on 2018/10/17.
 * Description:假定有一个MyObject类,有7个属性前面两个v1 和 v2 是必选,其余可选
 *
 * @author noteless
 */
public class MyObject {

    private int v1;//必选
    private int v2;//必选
    private int v3;//可选
    private int v4;//可选
    private int v5;//可选
    private int v6;//可选
    private int v7;//可选

    private static class Builder {

        private int v1;
        private int v2;

        private int v3 = 0;
        private int v4 = 0;
        private int v5 = 0;
        private int v6 = 0;
        private int v7 = 0;

        public Builder(int v1, int v2) {
            this.v1 = v1;
            this.v2 = v2;
        }

        public Builder setV3(int v3) {
            this.v3 = v3;
            return this;
        }

        public Builder setV4(int v4) {
            this.v4 = v4;
            return this;
        }

        public Builder setV5(int v5) {
            this.v5 = v5;
            return this;
        }

        public Builder setV6(int v6) {
            this.v6 = v6;
            return this;
        }

        public Builder setV7(int v7) {
            this.v7 = v7;
            return this;
        }

        public MyObject build() {
            return new MyObject(this);
        }
    }

    private MyObject(Builder builder) {
        v1 = builder.v1;
        v2 = builder.v2;
        v3 = builder.v3;
        v4 = builder.v4;
        v5 = builder.v5;
        v6 = builder.v6;
        v7 = builder.v7;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("MyObject{");
        sb.append("v1=").append(v1);
        sb.append(", v2=").append(v2);
        sb.append(", v3=").append(v3);
        sb.append(", v4=").append(v4);
        sb.append(", v5=").append(v5);
        sb.append(", v6=").append(v6);
        sb.append(", v7=").append(v7);
        sb.append('}');
        return sb.toString();
    }

    public static void main(String[] args) {

        MyObject my = new MyObject.Builder(1, 2).
                setV3(3).setV4(4).setV5(5).setV6(6).setV7(7).build();
        System.out.println(my.toString());
    }
}

省略了抽象的Builder,也省略了Director角色 示例中的Builder 就是模式中的ConcreteBuilder角色 他负责每一个步骤的实现细节,并且提供方法build() 获取最终的产品角色对象 借助于简化的工厂模式进行构造方法的替换解决方案的巧妙之处在于: public MyObject build() {

return new MyObject(this); }

它借助于建造者模式将实现与过程进行分离 但是在build() 方法中又并没有严格的规定步骤的过程 只是在构造Builder时必须传递两个必须参数,其余的参数你可以设置,也可以不设置 达到了多层嵌套构造方法的效果 而且,还非常清晰,你不会那么轻易地就在设置参数时犯错,因为你需要调用指定的方法

总结

本文通过抽象工厂模式演化到建造者模式,看到了建造者模式与抽象工厂模式的细节差异 建造者本身并不复杂,只需要理解本意即可“复杂对象的构建过程与表示进行分离” 建造者模式是将“步骤”这一事物进行抽象化,抽象化为Builder,将事物的表示延迟到子类ConcreteBuilder中,并通过Director进行组装 核心就是将“步骤”这一事物抽象 对于涉及到复杂对象的表示的场景,都可以考虑建造者模式 从抽象工厂的演进我们可以看得出来,建造者模式,可以借助于抽象工厂模式进行实现

common_log.png 转载务必注明出处:程序员潇然,疯狂的字节X,https://www.crazybytex.com/thread-16-1-1.html

关注下面的标签,发现更多相似文章

文章被以下专栏收录:

    黄小斜学Java

    疯狂的字节X

  • 目前专注于分享Java领域干货,公众号同步更新。原创以及收集整理,把最好的留下。
    包括但不限于JVM、计算机科学、算法、数据库、分布式、Spring全家桶、微服务、高并发、Docker容器、ELK、大数据等相关知识,一起进步,一起成长。
热门推荐
海康摄像头接入 wvp-GB28181-pro平台测试验
[md]### 简介 开箱即用的28181协议视频平台 `https://github.c
[CXX1300] CMake '3.18.1' was not
[md][CXX1300] CMake '3.18.1' was not found in SDK, PATH, or
[若依]微服务springcloud版新建增添加一个
[md]若依框架是一个比较出名的后台管理系统,有多个不同版本。