最近在學(xué)習(xí)Mybatis原理的時(shí)候,發(fā)現(xiàn)其初始化的過(guò)程中涉及到創(chuàng)建各種對(duì)象,運(yùn)用了一些創(chuàng)建型的設(shè)計(jì)模式,其中建造者模式的運(yùn)用還比較多,應(yīng)該是比較常用的設(shè)計(jì)模式,所以來(lái)深入了解一下
Mybatis源碼案例
SqlSessionFactory的創(chuàng)建
既然是在學(xué)習(xí)Mybatis原理時(shí)發(fā)現(xiàn)的建造者模式,就先來(lái)看看它是如何用代碼實(shí)現(xiàn)的。Mybatis創(chuàng)建SqlSessionFactory時(shí),會(huì)根據(jù)情況提供不同的參數(shù),參數(shù)組合也會(huì)有好幾種,由于構(gòu)造時(shí)參數(shù)的不確定,可以為其創(chuàng)建一個(gè)構(gòu)造器Builder,將SqlSessionFactory的構(gòu)建過(guò)程和表示分開(kāi)
/**
* 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);
}
}
上述代碼中,通過(guò)傳遞不同組合的參數(shù),返回了最終所要?jiǎng)?chuàng)建的對(duì)象DefaultSessionFactory
Environment的創(chuàng)建
Mybatis在構(gòu)建Configuration對(duì)象的過(guò)程中,XMLConfigBuilder解析Mybatis的XML配置文件<environment>節(jié)點(diǎn)時(shí),可以看到如何代碼
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());
}
}
}
}
創(chuàng)建Environment時(shí)使用了Environment內(nèi)置的構(gòu)造器Builder,在Environment內(nèi)部,定義了靜態(tài)內(nèi)部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源碼中的建造者模式案例,我們來(lái)詳細(xì)學(xué)習(xí)此模式。建造者模式(Builder Pattern)使用多個(gè)簡(jiǎn)單的對(duì)象一步一步構(gòu)建成一個(gè)復(fù)雜的對(duì)象,這種類型的設(shè)計(jì)模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對(duì)象的最佳方式。一個(gè)Builder類會(huì)一步一步構(gòu)造最終的對(duì)象,該Builder類是獨(dú)立于其他對(duì)象的
意圖
將一個(gè)復(fù)雜的構(gòu)建與其表示相分離,使得同樣的構(gòu)建過(guò)程可以創(chuàng)建不同的表示
主要解決
在軟件系統(tǒng)中,有時(shí)候面臨著“一個(gè)復(fù)雜對(duì)象”的創(chuàng)建工作,其通常由各個(gè)部分的子對(duì)象用一定的算法構(gòu)成,由于需求的變化,這個(gè)復(fù)雜對(duì)象的各個(gè)部分經(jīng)常面臨著劇烈的變化,但是將它們組合在一起的算法卻相對(duì)穩(wěn)定
具體實(shí)現(xiàn)
看完Mybatis的案例,再結(jié)合在簡(jiǎn)書中看到的一篇關(guān)于建造者模式的文章,雖然文章已經(jīng)寫得很詳細(xì)了,但還是想自己重新梳理一下整個(gè)實(shí)現(xiàn)思路,參考的文章是最下方的第二篇,在這里十分感謝作者提供的思路
有一個(gè)User類,里面的屬性都是不可變的,其中有些屬性是必要的,有些是不必要的,那我們?cè)撊绾蝿?chuàng)建這個(gè)類的對(duì)象呢?
public class User {
private final String firstName; // 必傳參數(shù)
private final String lastName; // 必傳參數(shù)
private final int age; // 可選參數(shù)
private final String phone; // 可選參數(shù)
private final String address; // 可選參數(shù)
}
使用構(gòu)造方法
很自然的我們會(huì)想到去使用構(gòu)造方法,并且我們需要使用不同的構(gòu)造方法來(lái)滿足傳入不同參數(shù)來(lái)創(chuàng)建對(duì)象的需求。第一個(gè)構(gòu)造方法只包含兩個(gè)必須的參數(shù),第二個(gè)構(gòu)造方法中增加一個(gè)可選參數(shù),第三個(gè)構(gòu)造方法中再增加一個(gè)可選參數(shù),以此類推,直到構(gòu)造方法中包含了所有的參數(shù)
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;
}
這樣做確實(shí)可以滿足根據(jù)傳入的不同參數(shù)組合來(lái)創(chuàng)建對(duì)象的需求,但是缺點(diǎn)也很明顯,如何參數(shù)較少的時(shí)候還可以接受,一旦參數(shù)多了,代碼的可讀性就很差,并且代碼也難以維護(hù)。另外對(duì)于調(diào)用者來(lái)說(shuō)也很麻煩,如果我只想多傳一個(gè)address參數(shù),必須要給age、phone設(shè)置默認(rèn)值,調(diào)用者也不知道第四個(gè)String類型的參數(shù)該傳address還是phone
使用getters和setters方法
除了使用構(gòu)造器,我們可以為每一個(gè)屬性設(shè)置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;
}
}
這個(gè)方法的可讀性相對(duì)構(gòu)造器而言好了許多,也相對(duì)易于維護(hù),調(diào)用者創(chuàng)建一個(gè)空的對(duì)象,然后只需設(shè)置想要的參數(shù)。但這個(gè)方法同樣存在缺點(diǎn),首先創(chuàng)建的對(duì)象會(huì)產(chǎn)生不一致的狀態(tài),當(dāng)調(diào)用者想要傳入所有參數(shù)的時(shí)候,必須將所有的set方法調(diào)用完,可能有一部分的調(diào)用者看到這個(gè)對(duì)象之后,以為這個(gè)對(duì)象已經(jīng)創(chuàng)建完畢就直接使用了,其實(shí)這個(gè)時(shí)候?qū)ο蟛](méi)有創(chuàng)建完成。另外此時(shí)User類中的屬性都是可變的了,不可變類的優(yōu)點(diǎn)不再擁有
使用建造者模式
public class User {
private final String firstName; // 必傳參數(shù)
private final String lastName; // 必傳參數(shù)
private final int age; // 可選參數(shù)
private final String phone; // 可選參數(shù)
private final String address; // 可選參數(shù)
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類的構(gòu)造方法是私有的,調(diào)用者不能直接創(chuàng)建User對(duì)象,屬性也都是不可變的,所有屬性都添加了final修飾符,并且在構(gòu)造方法中進(jìn)行了賦值,對(duì)外只提供getters方法。Builder的內(nèi)部類構(gòu)造方法中只接受必傳的參數(shù),且該參數(shù)使用了final修飾。另外Builder模式使用了鏈?zhǔn)秸{(diào)用,可讀性更佳
現(xiàn)在來(lái)創(chuàng)建一個(gè)User對(duì)象
new User.UserBuilder("趙","四").age(24).phone("123456789").address("南京大學(xué)").build();
代碼相當(dāng)?shù)暮?jiǎn)潔易懂,用一行代碼就可以完成對(duì)象的創(chuàng)建
線程安全問(wèn)題
Builder類可以在構(gòu)造方法參數(shù)上增加約束,build方法可以檢查這些約束,如果不滿足就拋出一個(gè)IllegalStateException異常。但要在Builder的參數(shù)拷貝到建造對(duì)象之后再驗(yàn)證參數(shù),這樣驗(yàn)證的就是建造對(duì)象的字段,而不是Builder的字段。這么做的原因是Builder類不是線程安全的,如果我們?cè)趧?chuàng)建真正的對(duì)象之前驗(yàn)證參數(shù),參數(shù)值可能被另一個(gè)線程在參數(shù)驗(yàn)證完和參數(shù)被拷貝完成之間的時(shí)間內(nèi)進(jìn)行修改。
正確寫法
這個(gè)是線程安全的因?yàn)槲覀兪紫葎?chuàng)建user對(duì)象,然后在不可變對(duì)象上驗(yàn)證條件約束
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);
}
經(jīng)典建造者模式
上述介紹的建造者模式的實(shí)現(xiàn)并不是經(jīng)典的案例
類圖中
- Product:產(chǎn)品抽象類,即要?jiǎng)?chuàng)建的復(fù)雜對(duì)象
- Builder:抽象的Builder類,給出一個(gè)抽象接口,以規(guī)范產(chǎn)品對(duì)象的各個(gè)組成成分的建造。這個(gè)接口規(guī)定要實(shí)現(xiàn)復(fù)雜對(duì)象的哪些部分的創(chuàng)建,并不涉及具體的對(duì)象部件的創(chuàng)建
- ConcretBuilder:具體的Builder類,實(shí)現(xiàn)Builder接口,針對(duì)不同的商業(yè)邏輯,具體化復(fù)雜對(duì)象的各部分的創(chuàng)建。 在建造過(guò)程完成后,提供產(chǎn)品的實(shí)例
- Director:統(tǒng)一組裝過(guò)程,調(diào)用具體建造者來(lái)創(chuàng)建復(fù)雜對(duì)象的各個(gè)部分,在指導(dǎo)者中不涉及具體產(chǎn)品的信息,只負(fù)責(zé)保證對(duì)象各部分完整創(chuàng)建或按某種順序創(chuàng)建
之前示例中的建造者模式,省略掉了Director,結(jié)構(gòu)更加簡(jiǎn)單,所以在許多框架源碼中,涉及到建造者模式時(shí),不再是經(jīng)典GOF的建造者模式,而是省略后的,比如一開(kāi)始我們提到的Mybatis中建造者的使用
經(jīng)典建造者模式的案例可參考設(shè)計(jì)模式之建造者模式(Builder)
使用建造者模式的好處
- 使用建造者模式可以使客戶端不必知道產(chǎn)品內(nèi)部組成的細(xì)節(jié)
- 具體的建造者類之間是相互獨(dú)立的,對(duì)系統(tǒng)的擴(kuò)展非常有利
- 由于具體的建造者是獨(dú)立的,因此可以對(duì)建造過(guò)程逐步細(xì)化,而不對(duì)其他的模塊產(chǎn)生任何影響
使用建造者模式的場(chǎng)合
- 創(chuàng)建一些復(fù)雜對(duì)象時(shí),這些對(duì)象的內(nèi)部組成構(gòu)件間的建造順序是穩(wěn)定的,但是對(duì)象的內(nèi)部組成構(gòu)件面臨著復(fù)雜的變化
- 要?jiǎng)?chuàng)建的復(fù)雜對(duì)象的算法,獨(dú)立于該對(duì)象的組成部分,也獨(dú)立于組成部分的裝配方法時(shí)
參考1:終結(jié)篇:MyBatis原理深入解析(一)
參考2:設(shè)計(jì)模式之Builder模式
參考3:建造者模式
參考4:建造者模式實(shí)踐