建造者(Builder)模式

最近在學(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í)踐

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 前言 建造者模式即BUilder模式,看名字就可知是一種創(chuàng)建型模式。主要是解決 構(gòu)造方法中參數(shù)過(guò)多導(dǎo)致的可讀性較差...
    C調(diào)路過(guò)閱讀 645評(píng)論 0 0
  • 沒(méi)有人買車會(huì)只買一個(gè)輪胎或者方向盤,大家買的都是一輛包含輪胎、方向盤和發(fā)動(dòng)機(jī)等多個(gè)部件的完整汽車。如何將這些部件組...
    justCode_閱讀 1,887評(píng)論 1 6
  • 每天三件事 1、去香港,找到Airbnb 上訂的房間 2、下午去尖沙咀逛逛 3、晚上聽(tīng)得到語(yǔ)音 小確幸 獨(dú)自一人第...
    Katrina程閱讀 120評(píng)論 0 0
  • 加入007“不寫就出局”活動(dòng)后,值月生提出倡議,給七年后的自己寫點(diǎn)什么。既然加入組織,就要響應(yīng)組織號(hào)召呀,寫! 可...
    卡拉手記閱讀 358評(píng)論 4 8
  • 得到APP專欄作者李翔在年度書單中推薦了《鞋狗》這本書,所以對(duì)它發(fā)生了興趣。這本書與其它企業(yè)傳記的不同之處在于,本...
    卓海閱讀 366評(píng)論 0 0