当前位置: 首页 > 工具软件 > Builder > 使用案例 >

【Builder设计模式】

曹季同
2023-12-01

1. 简介

最近在做IM系统的sdk, 要求也是非常的简单,要求sdk简单易用。

简单的要求实现起来可没有那么简单。

这里就先说一下,其中遇到的一个难题:

系统中对象的创建?

在sdk中对象的创建需要精细到每一个属性上,不同于接口可以直接在接口文档中写明传输对象需要传输的字段内容即可,在sdk中不仅仅要写明每一个要传输的对象的字段内容,还需要对每个对象的属性进行验证。

在web应用中,比如IM的服务端中采用的是Jsr303来对传输的对象进行验证。但是sdk只需要采用最基本的Java中的知识点即可,要求就是轻量,简便。

Builder的设计模式针对这种情况便有了很好的解决。

2. Builder的用法

简介:

Builder设计模式又叫创建者模式。简单来说,就是一步步创建一个对象,它对用户屏蔽了里面构建的细节,但是却可以精细的控制对象的构造过程。

简单来说一下,我对这个概念的理解。在面向对象思维中,我们肯定是先创建对象,拥有了对象之后,我们在一步步的往对象中填充具体的属性。

而Builder的设计模式,是先创建对象中的属性,然后再构建对象。整个对象的创建过程逆了过来。

下面结合代码来进行分析:

User user = new User();

user.setName("li");
user.setAge(18);
user.setSex("男");

先创建了user对象,然后再调用set方法往其中填充一个个的属性。

或者采用构造方法的形式,提供一个构造方法,在对象创建的时候属性直接填充进去。

User user = new User("li",18,"男");

这两种创建对象的方式都存在问题:

第一种方法,先创建对象再填充属性。假如在创建的过程中,对象创建好了,但是某个属性忘记填充了。这就会导致后面操作的错误,而且如果要进行验证,就只能在调用对象的地方进行验证,就会导致业务代码和功能性代码进行耦合。这种方式肯定不是最好的。而且对于使用简单的要求也没有很好的实现。

第二种方法,在创建对象的时候,我们可以在构造方法中对其中的属性进行验证。比如下面:

public class User{
    private String name;
    private int age;
    private String sex;
    
    public User(String name,int age,String sex){
        if(StrUtil.isBlank(name)){
            throw new RuntimeException("name not set");
        }
        //......
    }
}

这种创建对象的方式避免了第一种方式的缺点,但是还存在一个致命的缺陷——不够灵活。

比如上面我们要求sex属性,可有可无。上面的有参构造函数就不适用了,我们就需要在添加另外一个构造函数。

public User(String name,int age){
    this.User(name,age,null);
}

通过添加构造方法的形式,我们能够处理不够灵活的缺陷。但是这个问题其实并没有很好解决,我们只是把这个问题转移到了使用用户哪里,用户在使用起来的时候就要认真选择使用哪一个构造方法。而且一旦对象中的属性多了起来,构造方法也变得会很多了起来。

我们接下来看看Builder设计模式是如何做的?

先看看Builder设计模式的组成部分:

  1. 在实体类中创建一个xxxBuilder名称的静态内部类,并且和实体类具有相同的属性(称为构建器)。
  2. 对于实体类中的每个参数,构建器都要创建类似于setter的方法。只不过方法名与该参数名相同,并且返回值是xxxBuilder构建起本身(便于链式调用)。
  3. 在构建器中,需要创建一个build()方法,调用此方法就会根据设置的值创建实体对象。
  4. 在实体类中,会创建一个builder()静态方法,它的目的就是用来创建构造器。

举例:

//实体类
public class User{
    private String name;
    private int age;
    private String sex;
    private User(String name,int age,String sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    //实体类的get方法,方便获取对象的属性
    public String getName(){
        return this.name;
    }
    public int getAge(){
        return this.age;
    }
    public String getSex(){
        return this.sex;
    }
    //创建builder方法,返回一个构建器
    public static UserBuilder builder(){
        return new UserBuilder();
    }
    
    //构建器
    public static class UserBuilder{
        //属性和实体类中属性相同
        private String name;
        private int age;
        private String sex;
        
        //提供与setter方法类似的功能
        public UserBuilder name(String name){
            this.name = name;
            return this;
        }
        public UserBuilder age(int age){
            this.age = age;
             return this;
        }
        public UserBuilder sex(String sex){
            this.sex = sex;
             return this;
        }
        
       	//提供一个build方法,创建实体类对象
        public User build(){
            return new User(this.name,this.age,this.sex);
        }
    }
}

这样一个简单的Builder模式的类就完成了。我们下面来看看如何使用:

public static void mian(String[] args){
    //调用方式一
    User user = User.builder().name("li").age(18).sex("男").build();
}

上面就是使用链式拼接的方式来创建User对象了。

剩下的还有两个问题没有解决:

  1. 对象属性的验证?
  2. 灵活的创建对象?

针对第一个问题,我们可以对构造器进行如下的扩展。

 //构建器
public static class UserBuilder{
    //属性和实体类中属性相同
    private String name;
    private int age;
    private String sex;

    //提供与setter方法类似的功能
    public UserBuilder name(String name){
        if(StrUtil.isBlank(name)){
            throw new RuntimeException("name must be not null or blank!");
        }
        this.name = name;
        return this;
    }
    public UserBuilder age(int age){
        if(age==null){
            throw new RuntimeException("age must be not null or blank!");
        }
        this.age = age;
        return this;
    }
    public UserBuilder sex(String sex){
        if(StrUtil(sex)){
            throw new RuntimeException("sex must be not null or blank!")
        }
        this.sex = sex;
        return this;
    }

    //提供一个build方法,创建实体类对象
    public User build(){
        if(StrUtil.isBlank(name)){
            throw new RuntimeException("name not set!");
        }
        if(age==null){
            throw new RuntimeException("age not set!");
        }
        if(StrUtil(sex)){
            throw new RuntimeException("sex not set!")
        }
        return new User(this.name,this.age,this.sex);
    }
}

这样第一个问题就解决了,把对象的属性验证放在对象的创建过程中,避免了和业务代码的耦合。

针对第二个问题,我们在对构建器进行如下改造:

比如User对象中的sex字段是选填字段。

 //构建器
public static class UserBuilder{
    //属性和实体类中属性相同
    private String name;
    private int age;
    private String sex;

    //提供与setter方法类似的功能
    public UserBuilder name(String name){
        if(StrUtil.isBlank(name)){
            throw new RuntimeException("name must be not null or blank!");
        }
        this.name = name;
        return this;
    }
    public UserBuilder age(int age){
        if(age==null){
            throw new RuntimeException("age must be not null or blank!");
        }
        this.age = age;
        return this;
    }
    public UserBuilder sex(String sex){
        this.sex = sex;
        return this;
    }

    //提供一个build方法,创建实体类对象
    public User build(){
        if(StrUtil.isBlank(name)){
            throw new RuntimeException("name not set!");
        }
        if(age==null){
            throw new RuntimeException("age not set!");
        }
        if(StrUtil(sex)){
           this.sex = "未知";
        }
        return new User`(this.name,this.age,this.sex);
    }
}
public static void main(String[] args){
    //这样调用,灵活性的问题也就迎刃而解了。
    User user = User.builder().name("li").age(18).build();
}

3. Lombok中的@Builder注解

1. 基础使用

@Builder注解会生成相对复杂的构建器。

@Builder注解可以让我们调用下面的代码来初始化对象的实例:

User.builder().name("admin").age(18).build();

@Builder注解可以放在类,构造函数或方法上。

2. @Builder内部帮我们做了什么?
  1. 创建一个名为 xxxBuilder 的内部静态类,并具有和实体类相同的属性(称为构建器)。

  2. 在构建器中:对于目标类中的所有的属性和未初始化的 final 字段,都会在构建器中创建对应属性。

  3. 在构建器中:创建一个无参的 default 构造函数。

  4. 在构建器中:对于实体类中的每个参数,都会对应创建类似于 setter 的方法,只不过方法名与该参数名相同。 并且返回值是构建器本身(便于链式调用)。

  5. 在构建器中:一个 build() 方法,调用此方法,就会根据设置的值进行创建实体对象。

  6. 在构建器中:同时也会生成一个 toString() 方法。

  7. 在实体类中:会创建一个 builder() 方法,它的目的是用来创建构建器。

可以通过以下例子来理解:

@Builder
public class User {
    private final Integer code = 200;
    private String username;
    private String password;
}

// 编译后:
public class User {
    private String username;
    private String password;
    User(String username, String password) {
        this.username = username; this.password = password;
    }
    
    public static User.UserBuilder builder() {
        return new User.UserBuilder();
    }

    public static class UserBuilder {
        private String username;
        private String password;
        
        UserBuilder() {}

        public User.UserBuilder username(String username) {
            this.username = username;
            return this;
        }
        public User.UserBuilder password(String password) {
            this.password = password;
            return this;
        }
        
        public User build() {
            return new User(this.username, this.password);
        }
        
        public String toString() {
            return "User.UserBuilder(username=" + this.username + ", password=" + this.password + ")";
        }
    }
}
3. 组合用法
  1. @Builder中使用@Singular注释集合

    @Builder也可以为集合类型的参数或者属性生成一种特殊的方法,生成的特殊方法可以修改集合中的单个元素(增加一个集合中的元素或者删除集合中的一个元素)。

    在@Builder字段注释类,同时使用@Singular注解注释一个集合字段,lombok会为该集合字段生成两个adder放啊,而不是setter方法。

    一个是向集合中添加单个元素。

    一个是将另一个集合的所有元素添加到集合中。

    同时也会生成一个clear方法。

    使用了@Singular相对来说是比较复杂的,主要是为了保证以下特性:

    1. 在调用build()方法时,生成的集合将是不可变的。
    2. 在调用build()之后调用其中一个adder方法或者clear方法不会修改任何已经生成的对象。如果对集合修改之后,在调用build(),则会创建一个基于上一个对象创建的对象实体。
    3. 生成的集合将被压缩到最小的可行格式,同时保持高效。

    @Singular只能应用于lombok已知的集合类型。

    目前支持的类型有:

    java.util:

    • Iterable,Collection和List(一般情况下,生成不可修改的ArrayList支持)
    • Set,SortedSet和NavigableSet(一般情况下,生成可变大小不可修改的HashSet或者TreeSet)
    • Map,SortMap和NavigableMap(一般情况下,生成可变大小可不可修改的HashMap或者TreeMap)

    来看看使用@Singular直接之后的编译情况

    @Builder
    public class User {
        private final Integer id;
        private final String zipCode = "123456";
        private String username;
        private String password;
        @Singular
        private List<String> hobbies;
    }
    
    // 编译后:
    public class User {
        private final Integer id;
        private final String zipCode = "123456";
        private String username;
        private String password;
        private List<String> hobbies;
        User(Integer id, String username, String password, List<String> hobbies) {
            this.id = id; this.username = username;
            this.password = password; this.hobbies = hobbies;
        }
    
        public static User.UserBuilder builder() {return new User.UserBuilder();}
    
        public static class UserBuilder {
            private Integer id;
            private String username;
            private String password;
            private ArrayList<String> hobbies;
            UserBuilder() {}
            public User.UserBuilder id(Integer id) { this.id = id; return this; }
            public User.UserBuilder username(String username) { this.username = username; return this; }
            public User.UserBuilder password(String password) { this.password = password; return this; }
    
            public User.UserBuilder hobby(String hobby) {
                if (this.hobbies == null) {
                    this.hobbies = new ArrayList();
                }
                this.hobbies.add(hobby);
                return this;
            }
    
            public User.UserBuilder hobbies(Collection<? extends String> hobbies) {
                if (this.hobbies == null) {
                    this.hobbies = new ArrayList();
                }
                this.hobbies.addAll(hobbies);
                return this;
            }
    
            public User.UserBuilder clearHobbies() {
                if (this.hobbies != null) {
                    this.hobbies.clear();
                }
                return this;
            }
    
            public User build() {
                List hobbies;
                switch(this.hobbies == null ? 0 : this.hobbies.size()) {
                case 0:
                    hobbies = Collections.emptyList();
                    break;
                case 1:
                    hobbies = Collections.singletonList(this.hobbies.get(0));
                    break;
                default:
                    hobbies = Collections.unmodifiableList(new ArrayList(this.hobbies));
                }
                return new User(this.id, this.username, this.password, hobbies);
            }
            public String toString() {
                return "User.UserBuilder(id=" + this.id + ", username=" + this.username + ", password=" + this.password + ", hobbies=" + this.hobbies + ")";
            }
        }
    }
    
    

    使用@Singular进行build()来创建实例对象时,并没有直接使用Collections.unmodifiableList(Collection)此方法来创建实例,而是分为三种情况。

    第一种:当集合中没有元素时,创建一个空list。

    第二种:当集合中存在一个元素时,创建一个不可变的单元素list。

    第三种:根据当前集合的元素数量创建对应合适大小的list。

    编译后的代码,同时生成了三个关于集合操作的方法:

    hobby(String bobby);向集合中添加一个元素

    hobbies(Collection<? extends String> hobbies);添加一个集合中的所有元素

    clearHobbies();清空当前集合数据

  2. @Singular注解配置value属性

    我们先看看@Singular注解的详情:

    @Target({FIELD, PARAMETER})
    @Retention(SOURCE)
    public @interface Singular {
        // 修改添加集合元素的方法名
        String value() default "";
    }
    

    测试如何使用注解属性value

    @Builder
    public class User {
        private final Integer id;
        private final String zipCode = "123456";
        private String username;
        private String password;
        @Singular(value = "testHobbies")
        private List<String> hobbies;
    }
    
    // 测试类
    public class BuilderTest {
        public static void main(String[] args) {
            User user = User.builder()
                    .testHobbies("reading")
                    .testHobbies("eat")
                    .id(1)
                    .password("admin")
                    .username("admin")
                    .build();
            System.out.println(user);
        }
    }
    

    说明,当我们使用了注解属性 value 之后,我们在使用添加集合元素时的方法名发生相应的改变。但是,同时生成的添加整个集合的方法名发生改变了吗?我们再来看看编译后的代码:

    / 编译后:
    public class User {
        // 省略部分代码,只看关键部分
        public static class UserBuilder {
            public User.UserBuilder testHobbies(String testHobbies) {
                if (this.hobbies == null) {
                    this.hobbies = new ArrayList();
                }
                this.hobbies.add(testHobbies);
                return this;
            }
    
            public User.UserBuilder hobbies(Collection<? extends String> hobbies) {
                if (this.hobbies == null) {
                    this.hobbies = new ArrayList();
                }
                this.hobbies.addAll(hobbies);
                return this;
            }
            
            public User.UserBuilder clearHobbies() {
                if (this.hobbies != null) {
                    this.hobbies.clear();
                }
                return this;
            }
        }
    }
    

    可以看到,只有添加一个元素的方法名发生了改变。

  3. @Builder.Default 的使用

    比如有这样一个实体类:

    @Builder
    @ToString
    public class User {
        @Builder.Default
        private final String id = UUID.randomUUID().toString();
        private String username;
        private String password;
        @Builder.Default
        private long insertTime = System.currentTimeMillis();
    }
    

    在类中我在 id 和 insertTime 上都添加注解 @Builder.Default ,当我在使用这个实体对象时,我就不需要在为这两个字段进行初始化值,

    如下面这样:

    public class BuilderTest {
        public static void main(String[] args) {
            User user = User.builder()
                    .password("admin")
                    .username("admin")
                    .build();
            System.out.println(user);
        }
    }
    
    // 输出内容:
    User(id=416219e1-bc64-43fd-b2c3-9f8dc109c2e8, username=admin, password=admin, insertTime=1546869309868)
    
    

    lombok 在实例化对象时就为我们初始化了这两个字段值。

    当然,你如果再对这两个字段进行设值的话,那么默认定义的值将会被覆盖掉,如下面这样:

    public class BuilderTest {
        public static void main(String[] args) {
            User user = User.builder()
                    .id("admin")
                    .password("admin")
                    .username("admin")
                    .build();
            System.out.println(user);
        }
    }
    // 输出内容
    User(id=admin, username=admin, password=admin, insertTime=1546869642151)
    
  4. @Builder 详细配置

    下面我们再来详细看看 @Builder 这个注解类地详细实现:

    @Target({TYPE, METHOD, CONSTRUCTOR})
    @Retention(SOURCE)
    public @interface Builder {
        // 如果@Builder注解在类上,可以使用 @Builder.Default指定初始化表达式
        @Target(FIELD)
        @Retention(SOURCE)
        public @interface Default {}
        // 指定实体类中创建 Builder 的方法的名称,默认为: builder (个人觉得没必要修改)
        String builderMethodName() default "builder";
        // 指定 Builder 中用来构件实体类的方法的名称,默认为:build (个人觉得没必要修改)
        String buildMethodName() default "build";
        // 指定创建的建造者类的名称,默认为:实体类名+Builder
        String builderClassName() default "";
        // 使用toBuilder可以实现以一个实例为基础继续创建一个对象。(也就是重用原来对象的值)
        boolean toBuilder() default false;
        
        @Target({FIELD, PARAMETER})
        @Retention(SOURCE)
        public @interface ObtainVia {
            // 告诉lombok使用表达式获取值
            String field() default "";
            // 告诉lombok使用表达式获取值
            String method() default "";
    
            boolean isStatic() default false;
        }
    }
    
  5. @Builder 全局配置

    # 是否禁止使用@Builder
    lombok.builder.flagUsage = [warning | error] (default: not set)
    # 是否使用Guaua
    lombok.singular.useGuava = [true | false] (default: false)
    # 是否自动使用singular,默认是使用
    lombok.singular.auto = [true | false] (default: true)
    
 类似资料: