模式说明
建造者模式旨在创建一个对象,而且是一个复杂对象,那么什么是复杂对象呢?我个人认为,该对象的属性比较多(5 个或更多),同时,某些属性之间还存在依赖关系。如果一个对象存在多个属性,但每个属性并不存在依赖关系,那这种对象就类似于 DTO(Data Transfer Object)了,仅仅只是数据的搬运工。
应用场景
只要一个对象的构造方法上的参数过多,或将来可能会过多的情况下就应该引起注意了,往往这些都可能引入建造者模式的信号或者先兆。接下来,我将通过一个例子的全过程来说明建造者模式解决的是程序上的哪些设计痛点。
现在,项目中的使用了原始的 JDBC 操作数据库,导致大量重复创建数据库连接对象的代码。为了解决这个问题,项目组决定将创建数据库连接对象的代码搬迁到 DataSource 类中,代码如下所示。
public class DataSource {
private String jdbcUrl;
private String username;
private String password;
public DataSource(String jdbcUrl, String username, String password) {
Objects.requireNonNull(jdbcUrl);
Objects.requireNonNull(username);
Objects.requireNonNull(password);
this.jdbcUrl = jdbcUrl;
this.username = username;
this.password = password;
}
public Connection getConnection() {
try {
return DriverManager.getConnection(jdbcUrl, username, password);
} catch (SQLException e) {
throw new GetConnectionException(e); // RuntimeException
}
}
public void closeConnection(Connection conn) {
try {
if (conn != null)
conn.close();
} catch (SQLException e) {
throw new CloseException(e); // RuntimeException
}
}
}
上述代码在低版本的 JDK(1.2) 上还是不能正常使用,或者没有按照 JDBC 2.0 规范来实现的 Driver 注册方式也不能正常使用,所以,为了兼容,还需要引入 DriverClass 的参数,具体的构造方法代码将如下所示。
public class DataSource {
public DataSource(String jdbcUrl, String username,
String password, String driverClass) {
// ... ...
this.driverClass = driverClass;
try {
Class.forName(driverClass);
} catch (ClassNotFoundException e) {
throw new DataSourceException("Driver class initialize error", e);
}
}
}
JDBC 2.0 规范中提到 JDBC 驱动实现程序,需要在 /META-INF/services/java.sql.Driver 文件内写入具体的 Driver Class 全称,DriverManager 会通过 SPI 技术将其自动注册到应用内部,更多说明可参考 DriverManager 的类注释。
针对 4 个参数,利用静态工厂就能很好的满足设计要求,代码如下。
public class DataSource {
public static DataSource fromMySql(String jdbcUrl,
String username, String password) {
return new DataSource(jdbcUrl, username, password,
"com.mysql.jdbc.Driver");
}
public static DataSource fromOracle(String jdbcUrl,
String username, String password) {
return new DataSource(jdbcUrl, username, password,
"oracle.jdbc.driver.OracleDriver");
}
}
后来,程序创建太多的数据库短连接,导致数据库拒绝服务,因此,我们考虑引入数据库连接池,但连接池里涉及非常多的配置,此时的 DataSource 构造方法的参数已经放不下了,于是可以尝试新增一些 setter 方法对连接池的属性进行设置。
public class DataSource {
private int initialSize;
private int maxTotal;
private int maxIdle;
private int minIdle;
private long maxWaitMillis;
public void setInitialSize(int initialSize) {
this.initialSize = initialSize;
}
// ... 其它 setter 方法 ...
}
那么,现在创建 DataSource 将会是如下代码所示。
DataSource source = DataSource.fromMySql(jdbcUrl, username, password);
source.setInitialSize(2);
source.setMaxTotal(16);
source.setMaxIdle(16);
source.setMinIdle(8);
不过,setter 方法难以处理以下问题
- initialSize 必须小于等于 maxTotal
- minIdle 必须小于等于 maxIdle
- maxIdle 必须小于等于 maxTotal
- setter 方法还可以被重复调用
幸运的是,我们可以使用建造者模式,即所谓的 Builder Design Pattern,在 代码实现 一节,我们就可以看到建造者模式的魅力所在。可以看到上面代码演变的过程中,我们遇到了构造方法参数不多的情况下可以使用静态工厂就能很好的满足设计要求,而且也不会造成过度设计。但后来因为引入了数据连接池,构造方法的参数一下子膨胀了很多,于是想要通过 setter 方法对连接池的属性进行设置,可惜的是 setter 方法无法校验属性之间的关系,这个就是建造者模式出场的时机了,也是该模式要解决的程序设计痛点。
那么 Java 中的哪些类库使用了建造者模式呢?在 Java 11 中正式投入使用的 HttpClient / HttpRequest 构建方式正是建造者模式,样例代码参考如下。
HttpClient client = HttpClient.newBuilder()
.version(Version.HTTP_2)
.followRedirects(Redirect.SAME_PROTOCOL)
.proxy(ProxySelector.of(new InetSocketAddress("www-proxy.com", 8080)))
.authenticator(Authenticator.getDefault())
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://openjdk.java.net/groups/net/httpclient/"))
.timeout(Duration.ofMinutes(1))
.header("Content-Type", "application/json")
.POST(BodyPublisher.fromFile(Paths.get("file.json")))
.build();
HttpResponse<String> response = client.send(request, BodyHandler.asString());
System.out.println(response.statusCode());
System.out.println(response.body());
那么,我们该如何判断是否需要建造者模式,可参考如下准则
- 构造方法的参数过多(等于或多于 5 个)
- 无法单纯通过 setter 来满足属性之间的校验逻辑(区别于 DTO 对象)
- 对象内部属性存在依赖关系,或部分属性属于必选项
代码实现
最后,根据建造者模式改造 DataSource,以下是具体代码,注意并未实现 ConnectionPool,这里的代码类似于脚手架,大部分的建造者模式实现都与下面代码大同小异。
public class DataSource implements AutoCloseable {
private final ConnectionPool pool;
private DataSource(Builder builder) {
pool = new ConnectionPool(builder.jdbcUrl,
builder.username,
builder.password);
pool.setInitialSize(builder.initialSize);
pool.setMaxTotal(builder.maxTotal);
pool.setMaxIdle(builder.maxIdle);
pool.setMinIdle(builder.minIdle);
pool.setMaxWaitMillis(builder.maxWaitMillis);
}
public Connection getConnection() {
return pool.borrowConnection();
}
public void closeConnection(Connection conn) {
pool.returnConnection(conn);
}
@Override
public void close() throws Exception {
pool.close();
}
public static class Builder {
private String jdbcUrl;
private String username;
private String password;
private int initialSize;
private int maxTotal;
private int maxIdle;
private int minIdle;
private long maxWaitMillis;
public Builder jdbcUrl(String jdbcUrl) {
this.jdbcUrl = jdbcUrl;
}
public Builder username(String username) {
this.username = username;
}
public Builder password(String password) {
this.password = password;
}
public Builder poolInitialSize(int initialSize) {
this.initialSize = initialSize;
}
public Builder poolMaxTotal(int maxTotal) {
this.maxTotal = maxTotal;
}
public Builder poolMaxIdle(int maxIdle) {
this.maxIdle = maxIdle;
}
public Builder poolMinIdle(int minIdle) {
this.minIdle = minIdle;
}
public Builder poolMaxWaitMillis(long maxWaitMillis) {
this.maxWaitMillis = maxWaitMillis;
}
public DataSource build() {
Objects.requireNonNull(jdbcUrl);
Objects.requireNonNull(username);
Objects.requireNonNull(password);
if (initialSize > maxTotal) throw new IllegalArgumentException();
if (maxIdle > maxTotal) throw new IllegalArgumentException();
if (minIdle > maxIdle) throw new IllegalArgumentException();
return new DataSource(this);
}
}
}
再来看如何在客户端代码中使用,如下所示。
DataSource source = new DataSource.Builder()
.jdbcUrl("jdbc:mysql:...").username("root").password("xxxx")
.poolMaxTotal(36)
.poolMaxIdle(24)
.poolMinIdle(0)
.poolInitialSize(0)
.poolMaxWaitMillis(5000L)
.build();
Connection conn = source.getConnection();
try {
... ...
} finally {
source.closeConnection(conn);
}