程序猿阵线联盟-汇总各类技术干货技术干货Java设计模式

建造者(Builder)模式

2017-09-28  本文已影响0人  就没一个昵称能用

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

Mybatis源码案例

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().reset();
      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().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }
    
  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

}

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

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("id");  
          
            if (isSpecifiedEnvironment(id)) {  
                TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));  
                DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));  
                DataSource dataSource = dsFactory.getDataSource();  

                Environment.Builder environmentBuilder = new Environment.Builder(id)  
                .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类是独立于其他对象的

意图

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

主要解决

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

具体实现

看完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;       // 可选参数
}
使用构造方法

很自然的我们会想到去使用构造方法,并且我们需要使用不同的构造方法来满足传入不同参数来创建对象的需求。第一个构造方法只包含两个必须的参数,第二个构造方法中增加一个可选参数,第三个构造方法中再增加一个可选参数,以此类推,直到构造方法中包含了所有的参数

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;
}

这样做确实可以满足根据传入的不同参数组合来创建对象的需求,但是缺点也很明显,如何参数较少的时候还可以接受,一旦参数多了,代码的可读性就很差,并且代码也难以维护。另外对于调用者来说也很麻烦,如果我只想多传一个address参数,必须要给age、phone设置默认值,调用者也不知道第四个String类型的参数该传address还是phone

使用getters和setters方法

除了使用构造器,我们可以为每一个属性设置getters和setters方法

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

    public User() {
    }

    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;
    }
}

这个方法的可读性相对构造器而言好了许多,也相对易于维护,调用者创建一个空的对象,然后只需设置想要的参数。但这个方法同样存在缺点,首先创建的对象会产生不一致的状态,当调用者想要传入所有参数的时候,必须将所有的set方法调用完,可能有一部分的调用者看到这个对象之后,以为这个对象已经创建完毕就直接使用了,其实这个时候对象并没有创建完成。另外此时User类中的属性都是可变的了,不可变类的优点不再拥有

使用建造者模式

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

    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类的构造方法是私有的,调用者不能直接创建User对象,属性也都是不可变的,所有属性都添加了final修饰符,并且在构造方法中进行了赋值,对外只提供getters方法。Builder的内部类构造方法中只接受必传的参数,且该参数使用了final修饰。另外Builder模式使用了链式调用,可读性更佳
现在来创建一个User对象

new User.UserBuilder("赵","四").age(24).phone("123456789").address("南京大学").build();

代码相当的简洁易懂,用一行代码就可以完成对象的创建

线程安全问题

Builder类可以在构造方法参数上增加约束,build方法可以检查这些约束,如果不满足就抛出一个IllegalStateException异常。但要在Builder的参数拷贝到建造对象之后再验证参数,这样验证的就是建造对象的字段,而不是Builder的字段。这么做的原因是Builder类不是线程安全的,如果我们在创建真正的对象之前验证参数,参数值可能被另一个线程在参数验证完和参数被拷贝完成之间的时间内进行修改。

正确写法

这个是线程安全的因为我们首先创建user对象,然后在不可变对象上验证条件约束

public User build() {
  User user = new user(this);
  if (user.getAge() > 120) {
    throw new IllegalStateException(“Age out of range”); // 线程安全
  }
  return user;
}
非线程安全写法
public User build() {
  if (age > 120) {
    throw new IllegalStateException(“Age out of range”); // 非线程安全
  }
  return new User(this);
}

经典建造者模式

上述介绍的建造者模式的实现并不是经典的案例

建造者模式类图

类图中

使用建造者模式的好处

使用建造者模式的场合

参考1:终结篇:MyBatis原理深入解析(一)
参考2:设计模式之Builder模式
参考3:建造者模式
参考4:建造者模式实践

上一篇下一篇

猜你喜欢

热点阅读