建造者(Builder)模式

最近在学习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);
}

经典建造者模式

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

建造者模式类图

类图中

  • Product:产品抽象类,即要创建的复杂对象
  • Builder:抽象的Builder类,给出一个抽象接口,以规范产品对象的各个组成成分的建造。这个接口规定要实现复杂对象的哪些部分的创建,并不涉及具体的对象部件的创建
  • ConcretBuilder:具体的Builder类,实现Builder接口,针对不同的商业逻辑,具体化复杂对象的各部分的创建。 在建造过程完成后,提供产品的实例
  • Director:统一组装过程,调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建
    之前示例中的建造者模式,省略掉了Director,结构更加简单,所以在许多框架源码中,涉及到建造者模式时,不再是经典GOF的建造者模式,而是省略后的,比如一开始我们提到的Mybatis中建造者的使用
    经典建造者模式的案例可参考设计模式之建造者模式(Builder)

使用建造者模式的好处

  • 使用建造者模式可以使客户端不必知道产品内部组成的细节
  • 具体的建造者类之间是相互独立的,对系统的扩展非常有利
  • 由于具体的建造者是独立的,因此可以对建造过程逐步细化,而不对其他的模块产生任何影响

使用建造者模式的场合

  • 创建一些复杂对象时,这些对象的内部组成构件间的建造顺序是稳定的,但是对象的内部组成构件面临着复杂的变化
  • 要创建的复杂对象的算法,独立于该对象的组成部分,也独立于组成部分的装配方法时

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 前言 建造者模式即BUilder模式,看名字就可知是一种创建型模式。主要是解决 构造方法中参数过多导致的可读性较差...
    C调路过阅读 614评论 0 0
  • 没有人买车会只买一个轮胎或者方向盘,大家买的都是一辆包含轮胎、方向盘和发动机等多个部件的完整汽车。如何将这些部件组...
    justCode_阅读 1,828评论 1 6
  • 每天三件事 1、去香港,找到Airbnb 上订的房间 2、下午去尖沙咀逛逛 3、晚上听得到语音 小确幸 独自一人第...
    Katrina程阅读 115评论 0 0
  • 加入007“不写就出局”活动后,值月生提出倡议,给七年后的自己写点什么。既然加入组织,就要响应组织号召呀,写! 可...
    卡拉手记阅读 345评论 4 8
  • 得到APP专栏作者李翔在年度书单中推荐了《鞋狗》这本书,所以对它发生了兴趣。这本书与其它企业传记的不同之处在于,本...
    卓海阅读 351评论 0 0