Java设计模式再相识 (五)——建造者模式

339 阅读7分钟

「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战」。

在项目开发的过程中,我们经常会遇到需要按照一定的步骤创建对象的需求,并且这个对象由多个子部件组成。例如一台完整的现代计算机硬件系统通常由:CPU、主板、内存条、硬盘、显卡、外设组装而成,用户通常选好需求配置就可以向电脑公司购买,而不需要用户亲自组装计算机,用户也无需关心内部的实现。在软件开发中,我们称这样的模式为建造者模式。这里以一个通俗易懂的例子引入,下文将详细讲解建造者模式以及其在软件开发中的具体应用。

PS: 如果有阅读我前面的几篇文章会发现我文章的特点:例子都会以软件中的实际问题进行讲解,而不是随便举一个生活中的例子。

建造者模式

建造者(Builder)模式定义:建造者模式是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。建造者模式隐藏了复杂对象的创建过程,它把复杂对象的创建过程加以抽象,通过子类继承或者重载的方式,动态地创建具有复合属性的对象。

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

建造者模式与工厂模式同为创建型模式,区别主要体现在以下几个方面:

  • 建造者模式的关注点在类的组合,而工厂模式的关注点则是在类的创建。
  • 创建对象的力度不同:建造者创建更复杂的对象,对象通常由多个部件组成,而工厂模式创建出的对象结构基本相同。
  • 建造者模式构建对象具有顺序区别,不同顺序创建出来的对象大不相同。工厂模式创建的对象无法体现出顺序区别。

建造者模式的组成结构

建造者模式的结构如下:

  1. 产品(Product):建造者模式的产品是由多个部件组合而成的复杂对象。将由具体的建造者来创建其各个部件。
  2. 抽象的建造者(Builder):抽象建造者包含创建产品各个部件的抽象方法/接口,还准备了用于生成实例的方法。
  3. 具体的建造者(ConcreteBuilder):具体建造者负责实现由抽象建造者提供的接口。在具体的建造者中定义了各个部件实例的具体创建方法。此外,具体的建造者通常还会提供获取最终生成结果的方法。
  4. 监工(导演-Director):监工就像电影中的导演一样,指挥建造者完成复杂对象的构建。监工不直接关注产品的任何实现,它仅负责调用建造者对象中的方法完成对象创建。

实际应用场景

建造者模式在软件开发中的实际应用场景举例:

  • 分页参数的构建:在实际项目中,我们通常需要使用分页来控制一页显示的数据。在分页返回的信息时,我们通常要返回给前端如下信息:当前页,一页显示多少内容,数据条目总计,总共分了多少页,下一页是第几页....等信息。这时,我们就很适合使用建造者模式来将这些信息按照一定的顺序构建出来,并一同返回。
  • 数据库语句的拼接:在早些时候,ORM对象关系映射模型还没出现,我们操作SQL语句通常都使用字符串进行拼接,但这可能造成SQL注入漏洞的产生。所以,早些时候,我们使用建造者模式进行数据库SQL语句的构建,通过SqlBuilder拼接出复杂的语句,再提交给数据库进行执行。使用建造者模式拼接SQL语句而不是直接使用字符串拼接有助于防止注入漏洞的产生。

...

示例

在本节中,我们将动手实现一个ToStringBuilder,来满足将Java实体类中的成员变量转换为字符串拼接起来并输出。

我们首先准备好一个用户实体类:

User.java

package com.yeliheng.builder;

/**
 * 用户实体类
 */
public class User {

    private String username; //用户名

    private String password; //密码

    private String nickname; //昵称

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }
}

在上述实体类中,我们声明了用户名、密码、昵称三个成员变量。这时候,如果我们直接用toString()方法将这个类输出,输出结果如下所示:

output.png

这显然不是我们预期的结果。这时,我们开始使用建造者模式将输出改造成预期结果。

我们创建一个抽象建造者Builder类。

Builder.java


package com.yeliheng.builder;

public interface Builder {
    String append(User user);
}

抽象建造者类中提供了append方法,即将字符串进行拼接的方法。

接着,我们创建具体的建造者:ToStringBuilder.java


package com.yeliheng.builder;

public class ToStringBuilder implements Builder{
    @Override
    public String append(User user) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("用户名: " + user.getUsername() + "\n");
        buffer.append("密码: " + user.getPassword() + "\n");
        buffer.append("昵称: "+ user.getNickname() + "\n");
        return buffer.toString();
    }
}

在ToStringBuilder中,我们实现了append方法,将具体用户实体类中的用户名,密码以及昵称全部转换为String字符串,并通过StringBuffer进行拼接。

最后,我们开始使用具体的建造者。

Main.java

package com.yeliheng.builder;

public class Main {
    public static void main(String[] args) {
        User user = new User();
        user.setUsername("yeliheng");
        user.setPassword("123456");
        user.setNickname("测试用户");
        System.out.println(user.toString());
    }
}

最终程序的输出结果如下:

output.png

对实例的引申

我们使用建造者模式重写了内置的toString()方法,实现了用户实体类的输出。在本例中,User实体类充当了监工(导演)的角色,实体类中的toString()方法调用了ToStringBuilder的append()方法,User并不知道append()方法具体是怎样拼接字符串的,也无需知道。

在实际开发中,我们常常有类似本例的拼接字符串的需求,其实不需要自己实现,org.apache.commons.lang3就提供了一系列的工具类。其中,工具类大量使用了Builder模式,也是本例的来源。上述这个例子只是简单地实现了ToStringBuilder,实际的ToStringBuilder并没有这么简单。在真正的ToStringBuilder中,我们不能将实体类写死,而是需要动态地传入,并且ToStringBuilder返回的是自己的实例,以便于多次调用append()方法。它需要处理各种风格的输出以及对各种传入的数据类型进行处理。下图展示部分ToStringBuilder的真实代码作为了解,感兴趣的同学可以将工具包引入自行研究。

ToStringBuilder中的append方法:

WX20220214-215602@2x.png

ToStirngStyle中的append方法:

WX20220214-215650@2x.png

使用:

@Override
public String toString() {
    return new ToStringBuilder(this, ToStringStyle.JSON_STYLE)
        .append("用户名", getUsername())
        .append("密码", getPassword())
        .append("昵称", getNickname())
        .toString();
}

继续回到我们的建造者模式,我们来分析一下建造者模式的优缺点:

建造者模式的优缺点

优点

  • 良好的封装性:可以将构建与表示分离。这也是建造者模式的核心定义。
  • 良好的拓展性:建造者之间相互独立,有利于降低系统的耦合性,提高可拓展性。
  • 使用者无需知道类的组成部分,建造者可对产品创建的步骤进行封装,不会影响其他模块的创建,这也提高了系统的内聚性。

缺点

  • 一个类的组成如果发生了修改,则建造者也要同步修改。

总结

本文详细介绍了建造者模式,及其在实际开发中的应用场景,并且引出了apache工具类的实现。有助于深入理解建造者模式。

本文实例的完整代码参见:Github