【设计模式(五)】建造者模式

个人学习笔记分享,当前能力有限,请勿贬低,菜鸟互学,大佬绕道

如有勘误,欢迎指出和讨论,本文后期也会进行修正和补充


前言

建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象

如果经常使用组件,Builder这个单词是不是很眼熟?很多第三方组件都会使用builder来构造对象,通过向builder设置参数,即可或自己想要的对象

重点就在于==设置参数==,我们可以通过设置参数修改目标对象的细节,之前介绍的工厂模式只能选择产品类型,而无法修改参数。

例如我们购买一台DIY组装电脑,配件都是cpu、主板、显卡等等,但是具体配件型号是可以选择的,那么我们只需要将需要的配置告诉技术人员,然后技术人员采购并组装好后,交给我们

在这个例子里,技术人员就是一个建造者,告知其产品参数,然后获得一个产品


1.介绍

使用目的:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示

使用时机:创建复杂对象时,基本部件和创建步骤不变,但其细节经常发生变化的时候

解决问题:在构建一个复杂对象时,有时候需要根据需求调整对象细节,但其基本结构和构造过程都是固定的,通常由各个部分的子对象用一定的算法构成,此时其余创建型模式均无法处理

实现方法:建造者(builder)提供设置参数的入口,在内部根据参数进行组装,并返回出需要的对象

应用实例

  1. 快餐店套餐自选,如套餐通常包括汉堡+饮料+小吃,那么不同套餐可以分别选择汉堡、饮料和小吃的种类,快餐店按照需求打包给顾客
  2. http请求中的Request.Builder(),通过设置请求头、url、请求体等参数,生成相应的请求

优点

  1. 封装性好,耦合度低,构造与表示分离
  2. 扩展性好,各个具体的建造者相互独立,便于解耦
  3. 建造细节对客户端不可见,因而建造者可以细节进行控制和调整,不会影响其他模块

缺点

  1. 产品的组件必须相同,适用范围受到限制
  2. 如果产品内部变化复杂,可能需要很多建造者
  3. 产品业务发生变化时,建造者也要作出相应修改,后期维护成本较大


2.结构

建造者(Builder)模式由产品、抽象建造者、具体建造者、指挥者等 4 个要素构成

  • 产品角色(Product):它是包含多个组成部件的复杂对象,由具体建造者来创建其各个零部件。
  • 抽象建造者(Builder):它是一个包含创建产品各个子部件的抽象方法的接口,通常还包含一个返回复杂产品的方法 getResult()
  • 具体建造者(Concrete Builder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。
  • 指挥者(Director)(又称导演):它调用建造者对象中的部件构造与装配方法完成复杂对象的创建,在指挥者中不涉及具体产品的信息。

即,产品用来规范最终产品的内容,抽象建造者负责规范各个零件的创建方法,具体建造者则将这些方法实现,而指挥者负责将这些零件按照流程组装在一起

实际应用中会进行变更和扩展,很多时候会将指挥者抛弃,转由getResult()和客户端方法进行组装


3.实现

步骤

  1. 定义产品角色,包括组件和一个全构造方法,重写toString()方法以用于打印

    class Meal {
        private String food;
        private String drink;
        private String pack;
        private float price;
    
        public Meal(String food, String drink, String pack, float price) {
            this.food = food;
            this.drink = drink;
            this.pack = pack;
            this.price = price;
        }
    
        @Override
        public String toString() {
            return "Meal{" +
                    "food='" + food + '\'' +
                    ", drink='" + drink + '\'' +
                    ", pack='" + pack + '\'' +
                    ", price='" + price + '\'' +
                    '}';
        }
    }
    
  2. 定义抽象建造者,包括每个组件的创建方法和一个返回产品的方法

    abstract class Builder {
        public abstract void buildFood();
    
        public abstract void buildDrink();
    
        public abstract void buildPaper();
    
        public abstract void buildPrice();
    
        public abstract Meal getResult();
    
    
        public float calPrice(String food, String drink) throws Exception {
            float price = 0;
    
            if (food.equals("chicken")) {
                price += 10;
            } else if (food.equals("beef")) {
                price += 15;
            } else {
                throw new Exception("unknown food");
            }
    
            if (drink.equals("coca")) {
                price += 5;
            } else if (drink.equals("milk")) {
                price += 6;
            } else {
                throw new Exception("unknown drink");
            }
    
            return price;
        }
    }
    

    在这里我定义了一个公用的价格计算器,若每个产品计算规则不同,则定义在具体建造者中

  3. 定义具体建造者,实现抽象建造者的接口,并根据需求定义组件的创建方法

    class ConcreteBuilderA extends Builder {
    
        String food;
        String drink;
        String pack;
        float price;
    
        @Override
        public void buildFood() {
            this.food = "beef";
        }
    
        @Override
        public void buildDrink() {
            this.drink = "coca";
        }
    
        @Override
        public void buildPaper() {
            this.pack = "paper";
        }
    
        @Override
        @SneakyThrows
        public void buildPrice() {
            this.price = calPrice(food, drink);
        }
    
        public Meal getResult() {
            return new Meal(food, drink, pack, price);
        }
    }
    
    class ConcreteBuilderB extends Builder {
    
        String food;
        String drink;
        String pack;
        float price;
    
        @Override
        public void buildFood() {
            this.food = "chicken";
        }
    
        @Override
        public void buildDrink() {
            this.drink = "milk";
        }
    
        @Override
        public void buildPaper() {
            this.pack = "paper";
        }
    
        @Override
        @SneakyThrows
        public void buildPrice() {
            this.price = calPrice(food, drink);
        }
    
        public Meal getResult() {
            return new Meal(food, drink, pack, price);
        }
    }
    

    这里定义了两个建造者,分别实现抽象构造者的方法

  4. 定义指挥者,持有建造者,并定义产品构建与组装方法

    class Director {
        private Builder builder;
    
        public Director(Builder builder) {
            this.builder = builder;
        }
    
        public Meal construct() {
            builder.buildFood();
            builder.buildDrink();
            builder.buildPaper();
            builder.buildPrice();
            return builder.getResult();
        }
    }
    
  5. 客户端使用

    public class BuilderTest {
        public static void main(String[] args) {
            Director directorA = new Director(new ConcreteBuilderA());
            Meal mealA = directorA.construct();
            System.out.println(mealA);
    
            Director directorB = new Director(new ConcreteBuilderB());
            Meal mealB = directorB.construct();
            System.out.println(mealB);
        }
    }
    

完整代码

package com.company.test.builder;

import lombok.SneakyThrows;

class Meal {
    private String food;
    private String drink;
    private String pack;
    private float price;

    public Meal(String food, String drink, String pack, float price) {
        this.food = food;
        this.drink = drink;
        this.pack = pack;
        this.price = price;
    }

    @Override
    public String toString() {
        return "Meal{" +
                "food='" + food + '\'' +
                ", drink='" + drink + '\'' +
                ", pack='" + pack + '\'' +
                ", price='" + price + '\'' +
                '}';
    }
}

abstract class Builder {
    public abstract void buildFood();

    public abstract void buildDrink();

    public abstract void buildPaper();

    public abstract void buildPrice();

    public abstract Meal getResult();


    public float calPrice(String food, String drink) throws Exception {
        float price = 0;

        if (food.equals("chicken")) {
            price += 10;
        } else if (food.equals("beef")) {
            price += 15;
        } else {
            throw new Exception("unknown food");
        }

        if (drink.equals("coca")) {
            price += 5;
        } else if (drink.equals("milk")) {
            price += 6;
        } else {
            throw new Exception("unknown drink");
        }

        return price;
    }
}

class ConcreteBuilderA extends Builder {

    String food;
    String drink;
    String pack;
    float price;

    @Override
    public void buildFood() {
        this.food = "beef";
    }

    @Override
    public void buildDrink() {
        this.drink = "coca";
    }

    @Override
    public void buildPaper() {
        this.pack = "paper";
    }

    @Override
    @SneakyThrows
    public void buildPrice() {
        this.price = calPrice(food, drink);
    }

    public Meal getResult() {
        return new Meal(food, drink, pack, price);
    }
}

class ConcreteBuilderB extends Builder {

    private String food;
    private String drink;
    private String pack;
    private float price;

    @Override
    public void buildFood() {
        this.food = "chicken";
    }

    @Override
    public void buildDrink() {
        this.drink = "milk";
    }

    @Override
    public void buildPaper() {
        this.pack = "paper";
    }

    @Override
    @SneakyThrows
    public void buildPrice() {
        this.price = calPrice(food, drink);
    }

    public Meal getResult() {
        return new Meal(food, drink, pack, price);
    }
}

class Director {
    private Builder builder;

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

    public Meal construct() {
        builder.buildFood();
        builder.buildDrink();
        builder.buildPaper();
        builder.buildPrice();
        return builder.getResult();
    }
}

public class BuilderTest {
    public static void main(String[] args) {
        Director directorA = new Director(new ConcreteBuilderA());
        Meal mealA = directorA.construct();
        System.out.println(mealA);

        Director directorB = new Director(new ConcreteBuilderB());
        Meal mealB = directorB.construct();
        System.out.println(mealB);
    }
}

执行结果

image-20201009180628955


4.实际应用中的变型

在上述例子中,我们在具体建造者中定义组件内容,显然,我们仍然只能选择在建造者中预设好的组件方案,那如果产品参数无法预知范围呢?

比如,我们需要一份"coke" + "chicken"套餐,咋办,再定义一个builder?显然代码维护成本太高了

因此实际使用中,很多时候都是使用的建造模式与工厂模式结合的方案,我们修改代码如下

代码如下

package com.company.test.builder2;

import lombok.SneakyThrows;

class Meal {
    private String food;
    private String drink;
    private String pack;
    private float price;

    public Meal(String food, String drink, String pack, float price) {
        this.food = food;
        this.drink = drink;
        this.pack = pack;
        this.price = price;
    }

    @Override
    public String toString() {
        return "Meal{" +
                "food='" + food + '\'' +
                ", drink='" + drink + '\'' +
                ", pack='" + pack + '\'' +
                ", price='" + price + '\'' +
                '}';
    }
}

abstract class Builder {
    public abstract Builder buildFood(String food);

    public abstract Builder buildDrink(String drink);

    public abstract Meal build();


    public float calPrice(String food, String drink) throws Exception {
        float price = 0;

        if (food.equals("chicken")) {
            price += 10;
        } else if (food.equals("beef")) {
            price += 15;
        } else {
            throw new Exception("unknown food");
        }

        if (drink.equals("coca")) {
            price += 5;
        } else if (drink.equals("milk")) {
            price += 6;
        } else {
            throw new Exception("unknown drink");
        }

        return price;
    }
}

class ConcreteBuilder extends Builder {

    private String food="beef";
    private String drink="coca";
    private String pack="paper";

    @Override
    public Builder buildFood(String food) {
        this.food = food;
        return this;
    }

    @Override
    public Builder buildDrink(String drink) {
        this.drink = "coca";
        return this;
    }

    @SneakyThrows
    public Meal build() {
        return new Meal(food, drink, pack, calPrice(food, drink));
    }
}


public class BuilderTest {
    public static void main(String[] args) {
        Meal meal=new ConcreteBuilder()
                .buildDrink("coca")
                .buildFood("chicken")
                .build();
        System.out.println(meal);
    }
}

是不是这样就眼熟很多很多了?

  • 舍弃了指导者
  • 在建造者中设置好组件默认值,并提供接口允许修改组件
  • 修改组件接口使用链式编程,以精简客户端修改组件时的代码
  • 在建造者中提供建造产品方法
  • 在客户端设置参数和建造产品


运行结果

image-20201009182146670

鸡肉+可乐的肥仔快乐餐到手~

5.补充

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

严格意义上的建造者,更加关注零件的装配,目的是将装配过程与组件的创建分离开来,也就是解耦

工厂模式则将组件的创建和装配一起处理

所以,理论上建造者比工厂模式的耦合度更低,也更利于扩展,但是限制了使用范围(只能预设)且业务变更极其麻烦

上面提到的变型则是对这两种模式的整合,保证了其健壮性(参数可自定义),也将组件的创建和装配分离开了(虽然还在建造者中进行)


5.2.建造者模式常见误区

此处参考自https://blog.csdn.net/poyuan97/article/details/78076593

本来只发现别人的博文写的不太对劲,查阅一番之后果然不对劲。。。

5.2.1.director和builder混淆

单一职责原则:builder负责创建组件,而director负责进行组装

也就是说产品细节由builder控制,director只能调整组装顺序,而==不能看到具体零件的细节,更不能对其进行控制==

这个是在其特性里面就说明了的,但很多文章都违背了这一点(自己随便说搜一下。。一搜一大堆)


5.2.2.builder和product关系混淆

builder对product为依赖关系,而非聚合关系

即,builder依赖于product,因为builder最终要生成一个产品

builder聚合的是产品的部件,而非产品整体

很多文章的builder自行持有一个product整体,再提供方法对product进行修改

也就是说,==builder只能持有产品部件,不可持有产品整体==


5.2.3.实际使用

实际使用中很少严格遵守建造者模式,大部分情况下会对其进行变型和扩展,毕竟设计模式本身就只是提供一种参考的解决方案,并非标准或是准则

但是理解之后才能灵活变通的使用,不然变着变着,可能就不知道原先是啥了

比如上面介绍的变型,实际上就已经不算是严格意义上的建造者模式了

其实还可以进一步扩展,原先的建造者模式只能生产预设的产品,变型后只能生产自定义或者默认参数的产品,我们可以对其进行结合,既有预设,又能自定义,岂不美哉?成年人嘛,当然可以全都要


后记

设计模式是一种前辈们总结出来的经验和参考,而不是设计准则和规范

我们进行学习的目的是为了根据场景进行灵活变通,对其进行变型和扩展,以解决实际需求,而并非模板化的量产代码

当然也别放飞自我,随心所欲,设计模式既然是“最佳”经验,那么必然自身的优点,尽可能的维持其优点,然后根据情况补充其不足和短板才是我们需要学习的



作者:Echo_Ye

WX:Echo_YeZ

EMAIL :[email protected]

个人站点:在搭了在搭了。。。(右键 - 新建文件夹)

你可能感兴趣的:(【设计模式(五)】建造者模式)