建造者模式--师父让我来炼丹

2022-05-16 14:00:53 浏览数 (1)

引子

话说小帅在华山脚下的仙草药房兢兢业业工作了多年,从一个打杂的升级成了一个工厂的负责人,负责生产药房的镇店之宝----超级黑玉断续膏。

超级黑玉断续膏是稀世秘药,有再造之力,神奇无比,其价堪比黄金,服用后立即恢复生命2500点,且具备超级功效,能够同时恢复1000点内力。

此药如此神奇,其炼制程序极其复杂,而且品质很难把控,厂里炼药的师傅虽然经验丰富,但是总不免有所遗漏,导致产品功效时好时坏,客户对此多有怨言。师傅下令小帅三月之内改造流程工艺,杜绝此类问题。

小帅临危受命,立刻展开工作,把超级黑玉断续膏的所有工序流程都整理了一遍:

  1. 将各种名贵药材按比例调配
  2. 用华山脚下的山泉浸泡七天七夜
  3. 泡完之后加入秘药混合均匀
  4. 在八卦炉中炼制七七四十九天
  5. 在华山之巅晾晒十天,吸收日月之精华
  6. 装入特质木盒密封

由于工序流程很复杂,小帅把生产流程做成了一个清单,大家都按照这个清单操作,不管是经验丰富的老师傅,还是新带的徒弟,都严格按照清单上的步骤制作,就不会有差错了。

至于这个清单该如何用于生产呢?小帅以前是个程序员,就用代码把整个过程写了出来,这里还用到一个设计模式,我们一起来看看吧。

建造模式

造者模式(Builder Pattern):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

(图片来源:https://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/builder.html)

建造者模式通常有两种应用场景:

  1. 对象创建的步骤比较复杂,需要经过步骤一、二、三等多个步骤才能创建成功
  2. 对象本身有很多属性,属性之间可能还有制约关系

我们先来看第一种创建复杂对象的情况。

制作黑玉断续膏

制作黑玉断续膏的代码如下:

药品类:

代码语言:javascript复制
/**
 * 药品类
 */
public class Drug {

    /**
     * 药品名称
     */
    private String name;

    /**
     * 是否已混合
     */
    private boolean isMixed;

    /**
     * 是否已浸泡
     */
    private boolean isSoakd;

    /**
     * 是否已加入秘药
     */
    private boolean hasSecretMedicine;

    /**
     * 是否已炼制
     */
    private boolean isRefine;

    /**
     * 是否已晾晒
     */
    private boolean isDry;

    /**
     * 是否已密封
     */
    private boolean isSeal;

    public Drug(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        StringBuffer display = new StringBuffer();
        display.append("---- 获得 "   name   " 生产情况 ----n");
        display.append("是否已混合:"   isMixed  "n");
        display.append("是否已浸泡:"   isSoakd  "n");
        display.append("是否已加入秘药:"   hasSecretMedicine  "n");
        display.append("是否已炼制:"   isRefine  "n");
        display.append("是否已晾晒:"   isDry  "n");
        display.append("是否已密封:"   isSeal  "n");
        return display.toString();
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isMixed() {
        return isMixed;
    }

    public void setMixed(boolean mixed) {
        isMixed = mixed;
    }

    public boolean isSoakd() {
        return isSoakd;
    }

    public void setSoakd(boolean soakd) {
        isSoakd = soakd;
    }

    public boolean isHasSecretMedicine() {
        return hasSecretMedicine;
    }

    public void setHasSecretMedicine(boolean hasSecretMedicine) {
        this.hasSecretMedicine = hasSecretMedicine;
    }

    public boolean isRefine() {
        return isRefine;
    }

    public void setRefine(boolean refine) {
        isRefine = refine;
    }

    public boolean isDry() {
        return isDry;
    }

    public void setDry(boolean dry) {
        isDry = dry;
    }

    public boolean isSeal() {
        return isSeal;
    }

    public void setSeal(boolean seal) {
        isSeal = seal;
    }

}

药品建造接口:

代码语言:javascript复制
/**
 * 药品建造接口
 */
public interface DrugBuilder {

    /**
     * 混合
     */
    public void mix();

    /**
     * 浸泡
     */
    public void soak();

    /**
     * 加入秘药
     */
    public void addSecretMedicine();

    /**
     * 炼制
     */
    public void refine();

    /**
     * 晾晒
     */
    public void dry();

    /**
     * 密封
     */
    public void seal();
}

黑玉断续膏建造者类:

代码语言:javascript复制
/**
 * 黑玉断续膏建造者类
 */
public class HeiYuDuanXuGaoBulider implements DrugBuilder{

    private Drug drug;

    public HeiYuDuanXuGaoBulider() {
        this.drug = new Drug("黑玉断续膏");
    }

    /**
     * 混合
     */
    @Override
    public void mix() {
        this.drug.setMixed(true);
    }

    /**
     * 浸泡
     */
    @Override
    public void soak() {
        this.drug.setSoakd(true);
    }

    /**
     * 加入秘药
     */
    @Override
    public void addSecretMedicine() {
        this.drug.setHasSecretMedicine(true);
    }

    /**
     * 炼制
     */
    @Override
    public void refine() {
        this.drug.setRefine(true);
    }

    /**
     * 晾晒
     */
    @Override
    public void dry() {
        this.drug.setDry(true);
    }

    /**
     * 密封
     */
    @Override
    public void seal() {
        this.drug.setSeal(true);
    }

    /**
     * 获取药品
     * @return
     */
    public Drug getResult() {
        return this.drug;
    }
}

导向器类:

代码语言:javascript复制
/**
 * 导向器类
 */
public class Director {

    /**
     * 建造药品
     * @param drugBuilder
     */
    public void constructDrug(DrugBuilder drugBuilder) {
        // 混合
        drugBuilder.mix();
        // 浸泡
        drugBuilder.soak();
        // 加入秘药
        drugBuilder.addSecretMedicine();
        // 炼制
        drugBuilder.refine();
        // 晾晒
        drugBuilder.dry();
        // 密封
        drugBuilder.seal();
    }
}

客户端类:

代码语言:javascript复制
/**
 * 客户端类
 */
public class Demo {
    public static void main(String[] args) {
        // 创建导向器类
        Director director = new Director();
        // 创建黑玉断续膏建造者类
        HeiYuDuanXuGaoBulider heiYuDuanXuGaoBulider = new HeiYuDuanXuGaoBulider();
        // 建造药品
        director.constructDrug(heiYuDuanXuGaoBulider);
        // 获取药品
        Drug drug = heiYuDuanXuGaoBulider.getResult();
        System.out.println(drug);
    }
}

运行结果:

代码语言:javascript复制
---- 获得 黑玉断续膏 生产情况 ----
是否已混合:true
是否已浸泡:true
是否已加入秘药:true
是否已炼制:true
是否已晾晒:true
是否已密封:true

DrugBuilder:定义为了创建一个Drug对象所需要的方法步骤。

HeiYuDuanXuGaoBulider:实现DrugBuilder的接口,根据黑玉断续膏的特性实现接口方法。

Director:指导类,构造使用DrugBuilder接口的对象,按照接口里定义的步骤,一步步地操作,生产药品。

Drug:表示要被构造的复杂对象即黑玉断续膏。

客户端只要调用Director类的constructDrug方法来创建对象就好了,具体的生产步骤和流程都由Director类来控制,对客户端来说都是透明的,大大减轻了客户端的负担。

多个参数

最近,有些武林人士反映,他们有时候备了治疗内伤的药,却受了外伤,内伤的药用不起来,过了保质期就白白浪费掉了;

如果备了治疗外伤的药,有时候又受内伤,也会造成浪费;

如果同时备治疗内伤和外伤的药,则会大幅增加成本(现在混江湖也不容易),而且一般情况下不会即受内伤又受外伤(那样也太惨了吧)。

为了提高服务质量,满足客户个性化的需求,小帅的师父最近研发了一款新品,客户可以在现场调配出治疗内伤或外伤药品的便携炼丹炉。

原理是这样的,治疗内伤的药和治疗外伤的药其实大部分成分是一样的,只有一小部分不一样,治疗内伤的药添加的是仙灵草,治疗外伤的药添加的是大力丸。

所以只需要把仙灵草和大力丸磨成粉末,装成小包,给客户带在身上,根据需要添加仙灵草或大力丸,然后用我们生产的特制混合壶混合就行了。

为了满足个性化需求,我们还为客户提供了甜和咸两种口味哦!

说了这么多,我们来看看代码吧:

药品类:

代码语言:javascript复制
/**
 * 药品类
 */
public class Drug {

    /**
     * 药品名称
     */
    private String name;

    /**
     * 治疗类型(0:治疗内伤;1:治疗外伤)
     */
    private int type;

    /**
     * 口味(1:甜味;2:咸味)
     */
    private int taste;

    /**
     * 药品颗粒尺寸(单位cm)
     */
    private int size;

    /**
     * 数量
     */
    private int num;

 /**
     * 构造方法,通过Builder类的属性来构造对象
     * @param drugBuilder
     */
    public Drug(DrugBuilder drugBuilder) {
        this.name = drugBuilder.name;
        this.type = drugBuilder.type;
        this.taste = drugBuilder.taste;
        this.size = drugBuilder.size;
        this.num = drugBuilder.num;
    }

    @Override
    public String toString() {
        StringBuffer display = new StringBuffer();
        display.append("---- 您获得了药品:"   name   " ----n");
        display.append("能够治疗:"   (type == 0 ? "内伤" : "外伤")   "n");
        display.append("口味:"   (taste == 0 ? "甜味" : "咸味")   "n");
        display.append("颗粒尺寸:"   size   "cmn");
        display.append("数量:"   num   "颗n");
        return display.toString();
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getType() {
        return type;
    }

    public void setType(int type) {
        this.type = type;
    }

    public int getTaste() {
        return taste;
    }

    public void setTaste(int taste) {
        this.taste = taste;
    }

    public int getSize() {
        return size;
    }

    public void setSize(int size) {
        this.size = size;
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }
}

药品建造类:

代码语言:javascript复制
public class DrugBuilder {

    /**
     * 药品名称
     */
    public String name;

    /**
     * 治疗类型(0:治疗内伤;1:治疗外伤)
     */
    public int type;

    /**
     * 口味(1:甜味;2:咸味)
     */
    public int taste;

    /**
     * 药品颗粒尺寸(单位cm)
     */
    public int size;

    /**
     * 数量
     */
    public int num;


    /**
     * 构建药品对象
     * @return
     */
    public Drug build() {

        if(size < 1 || size > 5) {
            throw new IllegalArgumentException("药品颗粒尺寸必须在1-5cm之间");
        }

        if(num < 1 || size > 10) {
            throw new IllegalArgumentException("药品数量必须在1-10之间");
        }

        return new Drug(this);
    }

    /**
     * 设置名称
     * @param name
     * @return
     */
    public DrugBuilder setName(String name) {
        this.name = name;
        return this;
    }

    /**
     * 设置治疗类型
     * @param type
     * @return
     */
    public DrugBuilder setType(int type) {
        this.type = type;
        return this;
    }

    /**
     * 设置口味
     * @param taste
     * @return
     */
    public DrugBuilder setTaste(int taste) {
        this.taste = taste;
        return this;
    }

    /**
     * 设置尺寸
     * @param size
     * @return
     */
    public DrugBuilder setSize(int size) {
        this.size = size;
        return this;
    }

    /**
     * 设置数量
     * @param num
     * @return
     */
    public DrugBuilder setNum(int num) {
        this.num = num;
        return this;
    }
}

客户类:

代码语言:javascript复制
public class Demo {
    public static void main(String[] args) {
        Drug drug = new DrugBuilder()
                .setName("大力丸")
                .setType(1)
                .setTaste(0)
                .setSize(2)
                .setNum(10)
                .build();

        System.out.println(drug);
    }
}

输出:

代码语言:javascript复制
---- 您获得了药品:大力丸 ----
能够治疗:外伤
口味:甜味
颗粒尺寸:2cm
数量:10颗

如果要创建的对象的属性很多,而且属性还有制约条件,比如尺寸必须要在1-5cm之间,数量必须在1-10之间,这样就比较适合用建造者模式,让对象的属性和创建分离。

这样对于开发者而言隐藏了复杂的对象构建细节,降低了学习成本,同时提升了代码的可复用性。

建造者模式在JDK源码中的应用

我们来看一下JDK的StringBuilder类,从名字就能看出来它使用了创建者模式,它提供append()方法,开放构造步骤,最后调用toString()方法就可以获得一个构造好的完整字符串,源码截图如下:

一个使用StringBuilder 的例子:

代码语言:javascript复制
StringBuilder stringBuilder = new StringBuilder();
String str = stringBuilder
        .append("a")
        .append("b")
        .append("c")
        .toString();

总结

建造者模式主要适用于以下应用场景。

  • 相同的方法,不同的执行顺序,产生不同的结果。
  • 多个部件或零件,都可以装配到一个对象中,但是产生的结果又不相同。
  • 产品类非常复杂,或者产品类中不同的调用顺序产生不同的作用。
  • 初始化一个对象特别复杂,参数多,而且很多参数都具有默认值。

建造者模式与工厂模式的区别

建造者模式和工厂模式都是负责创建对象的,那么它们之间有什么区别呢?

工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象,工厂模式会马上返回创建的对象。

建造者模式重点关注如何分步生成复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象,允许你在获取对象前执行一些额外构造步骤。

可以看出来,建造者模式和工厂模式的目的是不同的

建造者模式的优点

  • 封装性好,构建和表示分离。
  • 符合单一职责原则,你可以将复杂构造代码从产品的业务逻辑中分离出来。
  • 便于控制细节,建造者可以对创建过程逐步细化,而不对其他模块产生任何影响。

建造者模式的缺点

  • 需要新增多个类, 因此代码整体复杂程度会有所增加。
  • 如果产品内部发生变化,则建造者也要同步修改,后期维护成本较大。

后记

仙草药房推出了随身携带的个性化制药炉之后,在武林中获得众多好评,大家甚至自己开发出了苹果,橘子,水蜜桃口味的丹药,掀起了制作个性化丹药的热潮,也为武林人士闯荡江湖增添了不少乐趣。

0 人点赞