建造者模式解析与实践

最近在学习Mybatis原理的时候,发现其初始化的过程中涉及到创建各种对象,运用了一些创建型的设计模式,其中建造者模式的运用还比较多,应该是比较常用的设计模式,所以来深入了解一下

一、简介

建造者模式(Builder
Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。通过建造者模式,可以让一个包含多构造函数,多可选参数和滥用setters方法的复杂事物简单化。

假设你有一个包含大量属性的类(类新建之后就不可改变),就像下面的User类一样。

public class User {
    private final String firstName;    //required
    private final String lastName;    //required
    private final int age;    //optional
    private final String phone;    //optional
    private final String address;    //optional
    ...
}

现在想象一下,在你的类中有一些属性是必须的,有一些是可选的。你会如何创建这个类的实例?因为所有的属性都被声明成final类型,所以你必须在构造方法中设置它们,但是你也想让这个类的客户端有忽略可选属性的机会。

SqlSessionFactory的创建

既然是在学习Mybatis原理时发现的建造者模式,就先来看看它是如何用代码实现的。Mybatis创建SqlSessionFactory时,会根据情况提供不同的参数,参数组合也会有好几种,由于构造时参数的不确定,可以为其创建一个构造器Builder,将SqlSessionFactory的构建过程和表示分开

/** * Copyright 2009-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */package org.apache.ibatis.session;import java.io.IOException;import java.io.InputStream;import java.io.Reader;import java.util.Properties;import org.apache.ibatis.builder.xml.XMLConfigBuilder;import org.apache.ibatis.exceptions.ExceptionFactory;import org.apache.ibatis.executor.ErrorContext;import org.apache.ibatis.session.defaults.DefaultSqlSessionFactory;/* * Builds {@link SqlSession} instances. * *//** * @author Clinton Begin */public class SqlSessionFactoryBuilder { public SqlSessionFactory build(Reader reader) { return build(reader, null, null); } public SqlSessionFactory build(Reader reader, String environment) { return build(reader, environment, null); } public SqlSessionFactory build(Reader reader, Properties properties) { return build(reader, null, properties); } public SqlSessionFactory build(Reader reader, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); return build(parser.parse; } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance; try { reader.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } } public SqlSessionFactory build(InputStream inputStream) { return build(inputStream, null, null); } public SqlSessionFactory build(InputStream inputStream, String environment) { return build(inputStream, environment, null); } public SqlSessionFactory build(InputStream inputStream, Properties properties) { return build(inputStream, null, properties); } public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); return build(parser.parse; } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance; try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } } public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory; }}

上述代码中,通过传递不同组合的参数,返回了最终所要创建的对象DefaultSessionFactory

1.1 方案一

一个首先想到的可选方案是提供多个构造方法。第一个构造方法是只接收必须属性作为参数,第二个是接收所有必须属性和第一个可选属性,第三个是接收所有必须属性和两个可选属性,依次类推。实现起来如下所示:

public User(String firstName, String lastName) {
    this(firstName, lastName, 0);
}

public User(String firstName, String lastName, int age) {
    this(firstName, lastName, age, '');
}

public User(String firstName, String lastName, int age, String phone) {
    this(firstName, lastName, age, phone, '');
}

public User(String firstName, String lastName, int age, String phone, String address) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
    this.phone = phone;
    this.address = address;
}
Environment的创建

Mybatis在构建Configuration对象的过程中,XMLConfigBuilder解析Mybatis的XML配置文件<environment>节点时,可以看到如何代码

private void environmentsElement(XNode context) throws Exception { if (context != null) { if (environment == null) { environment = context.getStringAttribute("default"); } for (XNode child : context.getChildren { String id = child.getStringAttribute; if (isSpecifiedEnvironment { TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); DataSource dataSource = dsFactory.getDataSource(); Environment.Builder environmentBuilder = new Environment.Builder .transactionFactory(txFactory) .dataSource(dataSource); configuration.setEnvironment(environmentBuilder.build; } } } }

创建Environment时使用了Environment内置的构造器Builder,在Environment内部,定义了静态内部Builder类

/** * Copyright 2009-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */package org.apache.ibatis.mapping;import javax.sql.DataSource;import org.apache.ibatis.transaction.TransactionFactory;/** * @author Clinton Begin */public final class Environment { private final String id; private final TransactionFactory transactionFactory; private final DataSource dataSource; public Environment(String id, TransactionFactory transactionFactory, DataSource dataSource) { if (id == null) { throw new IllegalArgumentException("Parameter 'id' must not be null"); } if (transactionFactory == null) { throw new IllegalArgumentException("Parameter 'transactionFactory' must not be null"); } this.id = id; if (dataSource == null) { throw new IllegalArgumentException("Parameter 'dataSource' must not be null"); } this.transactionFactory = transactionFactory; this.dataSource = dataSource; } public static class Builder { private String id; private TransactionFactory transactionFactory; private DataSource dataSource; public Builder(String id) { this.id = id; } public Builder transactionFactory(TransactionFactory transactionFactory) { this.transactionFactory = transactionFactory; return this; } public Builder dataSource(DataSource dataSource) { this.dataSource = dataSource; return this; } public String id() { return this.id; } public Environment build() { return new Environment(this.id, this.transactionFactory, this.dataSource); } } public String getId() { return this.id; } public TransactionFactory getTransactionFactory() { return this.transactionFactory; } public DataSource getDataSource() { return this.dataSource; }}

看完Mybatis源码中的建造者模式案例,我们来详细学习此模式。建造者模式(Builder
Pattern)使用多个简单的对象一步一步构建成一个复杂的对象,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。一个Builder类会一步一步构造最终的对象,该Builder类是独立于其他对象的

1.2 方案二

遵循JavaBean的规则,有一个默认的无参构造方法并且都有getter和setter方法。就像这样:

public class User {
    private String firstName; // required
    private String lastName; // required
    private int age; // optional
    private String phone; // optional
    private String address;  //optional

    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String getPhone() {
        return phone;
    }
    public void setPhone(String phone) {
        this.phone = phone;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
}

这种方法看起来很容易阅读和维护。在客户端里我可以只创建一个空对象,然后只设置那些我感兴趣的属性。那么,这种方法有什么问题?这种解决方案有两个主要的问题。

  • 第一个问题是该类的实例状态不固定。如果你想创建一个User对象,该对象的5个属性都要赋值,那么直到所有的setXX方法都被调用之前,该对象都没有一个完整的状态。这意味着在该对象状态还不完整的时候,一部分客户端程序可能看见这个对象并且以为该对象已经构造完成。
  • 第二个不足是User类是易变的。你将会失去不可变对象带来的所有优点。
意图

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

1.3 方案三

建造者模式实现

public class User {
    private final String firstName; // required
    private final String lastName; // required
    private final int age; // optional
    private final String phone; // optional
    private final String address; // optional

    private User(UserBuilder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.age = builder.age;
        this.phone = builder.phone;
        this.address = builder.address;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public int getAge() {
        return age;
    }

    public String getPhone() {
        return phone;
    }

    public String getAddress() {
        return address;
    }

    public static class UserBuilder {
        private final String firstName;
        private final String lastName;
        private int age;
        private String phone;
        private String address;

        public UserBuilder(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }

        public UserBuilder age(int age) {
            this.age = age;
            return this;
        }

        public UserBuilder phone(String phone) {
            this.phone = phone;
            return this;
        }

        public UserBuilder address(String address) {
            this.address = address;
            return this;
        }

        public User build() {
            return new User(this);
        }

    }
}

使用建造者模式有如下优势:

  • User构造方法是私有的,这意味着该类不能在客户端代码里直接实例化。
  • 该类是不可变的。所有属性都是final类型的,在构造方法里面被赋值。另外,我们只为它们提供了getter方法。
  • builder类使用流式接口风格,让客户端代码阅读起来更容易。
  • builder类构造方法只接收必须属性,为了确保这些属性在构造方法里赋值,只有这些属性被定义成final类型。
  • 现在,试图创建一个新的User对象的客户端代码看起来如何那?让我们来看一下:

public User getUser() {
    return new User.UserBuilder('Jhon', 'Doe')
    .age(30)
    .phone('1234567')
    .address('Fake address 1234')
    .build();
}

需要注意的是要在builder的参数拷贝到建造对象之后再验证参数,这样验证的就是建造对象的字段,而不是builder的字段。这么做的原因是builder类不是线程安全的,如果我们在创建真正的对象之前验证参数,参数值可能被另一个线程在参数验证完和参数被拷贝完成之间的某个时间修改。这段时间周期被称作“脆弱之窗”

正确姿势:

public User build() {
    User user = new user(this);
    if (user.getAge() > 120) {  // user的成员变量user.getAge()
        throw new IllegalStateException(“Age out of range”); // thread-safe
    }
    return user;
}

错误姿势:

public User build() {
    if (age > 120) {  // UserBuilder的成员变量age
        throw new IllegalStateException(“Age out of range”); // bad, not thread-safe
    }
    // This is the window of opportunity for a second thread to modify the value of age
    return new User(this);
}

除了上述使用建造者模式的优点之外,建造者模式还有的一个优点是builder可以作为参数传递给一个方法,让该方法拥有为客户端创建一个或者多个对象的能力,而不需要知道创建对象的任何细节。为了这么做你可能通常需要一个如下所示的简单接口:

public interface Builder<T> {
    T build();
}

借用之前的User例子,UserBuilder类可以实现Builder<User>。如此,我们可以有如下的代码:

UserCollection buildUserCollection(Builder<? extends User> userBuilder){...}

例如:

public List<User> buildUserList(Builder<User> userBuilder){
    List<User> mList = new ArrayList<>();
    for (int i = 0; i < 5; i ++) {
        mList.add(userBuilder.build());
    }

    return mList;
}
主要解决

在软件系统中,有时候面临着“一个复杂对象”的创建工作,其通常由各个部分的子对象用一定的算法构成,由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定

看完Mybatis的案例,再结合在简书中看到的一篇关于建造者模式的文章,虽然文章已经写得很详细了,但还是想自己重新梳理一下整个实现思路,参考的文章是最下方的第二篇,在这里十分感谢作者提供的思路有一个User类,里面的属性都是不可变的,其中有些属性是必要的,有些是不必要的,那我们该如何创建这个类的对象呢?

public class User { private final String firstName; // 必传参数 private final String lastName; // 必传参数 private final int age; // 可选参数 private final String phone; // 可选参数 private final String address; // 可选参数}

二、实践

发表评论

电子邮件地址不会被公开。 必填项已用*标注