【Java设计模式004】建造者模式

大家好,个人gzh是大猪和小猪的小家 ,我们的gzh是朝阳三只大明白,满满全是干货,分享近期的学习知识以及个人总结(包括读研和IT),跪求一波关注,希望和大家一起努力、进步!!

概述

首先来看一个例子,假设我们需要建造一个房子,那么必须建造墙、屋顶、地板、门...如果还需要游泳池、健身室,那么该怎么办呢?最简单的方式是创建一个 House 基类,将公有部分抽象出来,然后根据需求组合的不同构建不同的子类,例如带游泳池的房子、不带游泳池带健身房的房子...这显然会造成类爆炸

为了解决这个问题,我们也可以创建一个包含所有可能参数的房屋基类,提供一个超级构造器并用它控制房屋对象的构造,这样做的问题在于通常情况下,绝大部分的参数都没有使用,这使得对于构造函数的调用十分不简洁。

构造器模式使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。它将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示,该模式针对的主要问题在于:

  1. 由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定;
  2. 构造对象的参数过多,使用折叠构造函数模式和 Javabean 模式容易造成调用者混乱;

第一个问题很好理解,我这里主要介绍一下第二个问题,比方说我们有一个类:

java 复制代码
public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;
	...
}

第一种传递参数的方式是折叠构造函数模式 (telescoping constructor pattern)

java 复制代码
public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    public NutritionFacts(int servingSize, int servings) {
        this(servingSize, servings, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories) {
        this(servingSize, servings, calories, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories, int fat) {
        this(servingSize, servings, calories, fat, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
        this(servingSize, servings, calories, fat, sodium, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.carbohydrate = carbohydrate;
    }
}

它的主要问题在于代码编写难度大,用户很有可能调用错误的参数。第二种设置参数的方式是 JavaBeans 模式:

java 复制代码
public class NutritionFacts {
    private int servingSize = -1;
    private int servings = -1;
    private int calories = -1;
    private int fat = -1;
    private int sodium = -1;
    private int carbohydrate = -1;

    public NutritionFacts() {
        
    }

    public void setServingSize(int servingSize) {
        this.servingSize = servingSize;
    }

    public void setServings(int servings) {
        this.servings = servings;
    }

    public void setCalories(int calories) {
        this.calories = calories;
    }

    public void setFat(int fat) {
        this.fat = fat;
    }

    public void setSodium(int sodium) {
        this.sodium = sodium;
    }

    public void setCarbohydrate(int carbohydrate) {
        this.carbohydrate = carbohydrate;
    }
}

这种方式的问题在于将对象的构建过程拆分到多个调用中,这很容易导致构造过程中对象的不一致,程序员需要付出额外的努力保证线程安全; 为了解决这些问题,建造者模式横空出世,它的主要角色如下:

  1. 抽象建造者类(Builder):这个接口规定要实现复杂对象部分的创建,并不涉及具体的部件对象的创建。
  2. 具体建造者类(ConcreteBuilder):实现/继承 Builder ,完成复杂产品的各个部件的具体创建方法。在构造过程完成后,提供产品的实例。
  3. 产品类(Product):要创建的复杂对象。
  4. 指挥者类(Director):调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。

该模式的主要优点:

  1. 封装性好,将产品和制造者解耦;
  2. 可以精确地控制产品的创建过程;
  3. 使用相同的创建过程可以创建不同的产品对象;
  4. 建造者扩展十分容易,复合开闭原则;

实现

建造者模式的实现思路如下:

  1. 明确产品类的组成零件;
  2. 抽象建造者类中声明各个零件的建造方法;
  3. 为每个具体产品创建具体建造者类,并实现其构造步骤。
  4. 考虑创建指挥者类。它可以使用同一建造者对象来封装多种构造产品的方式;
  5. 客户端通过指挥者类获取产品对象。

以自行车为例,一辆自行车包括车架、座椅和轮子,自行车类如下:

java 复制代码
public class Bike {
    private String seat;
    private String frame;
    private String wheel;

    public Bike() {
    }

    public Bike(String seat, String frame, String wheel) {
        this.seat = seat;
        this.frame = frame;
        this.wheel = wheel;
    }

    public String getSeat() {
        return seat;
    }

    public void setSeat(String seat) {
        this.seat = seat;
    }

    public String getFrame() {
        return frame;
    }

    public void setFrame(String frame) {
        this.frame = frame;
    }

    public String getWheel() {
        return wheel;
    }

    public void setWheel(String wheel) {
        this.wheel = wheel;
    }

    @Override
    public String toString() {
        return "Bike{" +
                "seat='" + seat + '\'' +
                ", frame='" + frame + '\'' +
                ", wheel='" + wheel + '\'' +
                '}';
    }
}

现在假定 BikeA 的建造顺序是车架→轮子→座椅;BikeB 的建造顺序是车架→座椅→轮子;那么抽象建造者的定义如下:

java 复制代码
public abstract class BikeBuilder {
    protected Bike bike = new Bike();

    public abstract void buildSeat();

    public abstract void buildWheel();

    public abstract void buildFrame();

    public abstract Bike build();
}

具体建造者 A 的代码如下:

java 复制代码
public class BikeABuilder extends BikeBuilder{
    @Override
    public void buildSeat() {
        bike.setSeat("BikeA座椅");
        System.out.println("A其他座椅安装操作");
    }

    @Override
    public void buildWheel() {
        bike.setWheel("BikeA轮子");
        System.out.println("A其他轮子安装操作");
    }

    @Override
    public void buildFrame() {
        bike.setFrame("BikeA车架");
        System.out.println("A其他车架安装操作");
    }

    @Override
    public Bike build() {
        System.out.println("A其他构造操作");
        return bike;
    }
}

具体建造者 B 的代码如下:

java 复制代码
public class BikeBBuilder extends BikeBuilder{
    @Override
    public void buildSeat() {
        bike.setSeat("BikeB座椅");
        System.out.println("B其他座椅安装操作");
    }

    @Override
    public void buildWheel() {
        bike.setWheel("BikeB轮子");
        System.out.println("B其他轮子安装操作");
    }

    @Override
    public void buildFrame() {
        bike.setFrame("BikeB车架");
        System.out.println("B其他车架安装操作");
    }

    @Override
    public Bike build() {
        System.out.println("B其他构造操作");
        return bike;
    }
}

指挥者类如下:

java 复制代码
public class Director {
    public Bike construct(String bikeType) {
        if (bikeType == null || bikeType.isEmpty()) {
            throw new IllegalArgumentException();
        }
        BikeBuilder builder;
        if ("a".equals(bikeType)) {
            builder = new BikeABuilder();
            System.out.println("============= 开始构筑A =============");
            builder.buildFrame();
            builder.buildWheel();
            builder.buildSeat();
        } else if ("b".equals(bikeType)) {
            builder = new BikeBBuilder();
            System.out.println("============= 开始构筑B =============");
            builder.buildFrame();
            builder.buildSeat();
            builder.buildWheel();
        } else {
            throw new RuntimeException();
        }

        return builder.build();
    }
}

测试类代码如下:

java 复制代码
public class Client {
    public static void main(String[] args) {
        Director director = new Director();
        Bike bike = director.construct("a");
        System.out.println("=======================================");
        System.out.println(bike);
        bike = director.construct("b");
        System.out.println("=======================================");
        System.out.println(bike);
    }
}

测试结果如下:

ini 复制代码
============= 开始构筑A =============
A其他车架安装操作
A其他轮子安装操作
A其他座椅安装操作
A其他构造操作
=======================================
Bike{seat='BikeA座椅', frame='BikeA车架', wheel='BikeA轮子'}
============= 开始构筑B =============
B其他车架安装操作
B其他座椅安装操作
B其他轮子安装操作
B其他构造操作
=======================================
Bike{seat='BikeB座椅', frame='BikeB车架', wheel='BikeB轮子'}

严格来说,程序中并不一定需要指挥者类。客户端代码可直接以特定顺序调用创建步骤,也可以将指挥者类和建造者类进行合并。不过,指挥者类中非常适合放入各种例行构造流程,以便在程序中反复使用。此外,对于客户端代码来说,指挥者类完全隐藏了产品构造细节。客户端只需使用指挥者类来构造产品。

除此之外还可以将建造者类作为产品类的一个静态内部类,并在产品类中添加一个接收建造者类的构造器,以 Option 为例:

java 复制代码
class Option{
	private Option(Builder builder) {
        this.numberOfArgs = -1;
        this.type = String.class;
        this.values = new ArrayList();
        this.argName = builder.argName;
        this.description = builder.description;
        this.longOpt = builder.longOpt;
        this.numberOfArgs = builder.numberOfArgs;
        this.opt = builder.opt;
        this.optionalArg = builder.optionalArg;
        this.required = builder.required;
        this.type = builder.type;
        this.valuesep = builder.valuesep;
    }
    
	public static final class Builder {
        private final String opt;
        private String description;
        private String longOpt;
        private String argName;
        private boolean required;
        private boolean optionalArg;
        private int numberOfArgs;
        private Class<?> type;
        private char valuesep;

        private Builder(String opt) throws IllegalArgumentException {
            this.numberOfArgs = -1;
            this.type = String.class;
            OptionValidator.validateOption(opt);
            this.opt = opt;
        }

        public Builder argName(String argName) {
            this.argName = argName;
            return this;
        }

        public Builder desc(String description) {
            this.description = description;
            return this;
        }

        public Builder longOpt(String longOpt) {
            this.longOpt = longOpt;
            return this;
        }

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

        public Builder optionalArg(boolean isOptional) {
            this.optionalArg = isOptional;
            return this;
        }

        public Builder required() {
            return this.required(true);
        }

        public Builder required(boolean required) {
            this.required = required;
            return this;
        }

        public Builder type(Class<?> type) {
            this.type = type;
            return this;
        }

        public Builder valueSeparator() {
            return this.valueSeparator('=');
        }

        public Builder valueSeparator(char sep) {
            this.valuesep = sep;
            return this;
        }

        public Builder hasArg() {
            return this.hasArg(true);
        }

        public Builder hasArg(boolean hasArg) {
            this.numberOfArgs = hasArg ? 1 : -1;
            return this;
        }

        public Builder hasArgs() {
            this.numberOfArgs = -2;
            return this;
        }

        public Option build() {
            if (this.opt == null && this.longOpt == null) {
                throw new IllegalArgumentException("Either opt or longOpt must be specified");
            } else {
                return new Option(this);
            }
        }
    }
}

这样通过链式调用就可以成功创建出产品对象,解决了传递参数过多的问题。

java 复制代码
Option classOption = Option.builder("c")
                .hasArg()
                .valueSeparator(' ')
                .argName("classpath hishishis")
                .required()
                .desc("class Path")
                .build();

总结

建造者模式的应用很广,当你希望使用代码创建不同形式的产品 (例如石头或木头房屋) 时,可使用建造者模式。此外,使用建造者模式可避免 "重叠构造函数 (telescoping constructor)" 的出现。但是建造者模式的缺点也很明显:

  1. 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似;如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制;
  2. 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。

很多人会将建造者模式和与工厂模式相比,虽然二者的目的都是构造出一个产品对象,但是二者的着重点不同,工厂方法模式注重的是整体对象的创建方式;而建造者模式注重的是部件构建的过程,意在通过一步一步地精确构造创建出一个复杂的对象。

往期回顾

  1. 【Java设计模式003】原型模式
  2. 【Java设计模式002】工厂模式
  3. 【Java设计模式001】单例模式

文中难免会出现一些描述不当之处(尽管我已反复检查多次),欢迎在留言区指正,相关的知识点也可进行分享,希望大家都能有所收获!!如果觉得我的文章写得还行,不妨支持一下。你的每一个转发、关注、点赞、评论都是对我最大的支持!

相关推荐
qq_441996051 小时前
Mybatis官方生成器使用示例
java·mybatis
巨大八爪鱼1 小时前
XP系统下用mod_jk 1.2.40整合apache2.2.16和tomcat 6.0.29,让apache可以同时访问php和jsp页面
java·tomcat·apache·mod_jk
码上一元3 小时前
SpringBoot自动装配原理解析
java·spring boot·后端
计算机-秋大田3 小时前
基于微信小程序的养老院管理系统的设计与实现,LW+源码+讲解
java·spring boot·微信小程序·小程序·vue
魔道不误砍柴功5 小时前
简单叙述 Spring Boot 启动过程
java·数据库·spring boot
失落的香蕉5 小时前
C语言串讲-2之指针和结构体
java·c语言·开发语言
枫叶_v5 小时前
【SpringBoot】22 Txt、Csv文件的读取和写入
java·spring boot·后端
wclass-zhengge5 小时前
SpringCloud篇(配置中心 - Nacos)
java·spring·spring cloud
路在脚下@5 小时前
Springboot 的Servlet Web 应用、响应式 Web 应用(Reactive)以及非 Web 应用(None)的特点和适用场景
java·spring boot·servlet
黑马师兄5 小时前
SpringBoot
java·spring