建造者模式详解及其实战源码分析
保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/57/ )。
在软件开发过程中,经常需要新建对象并给对象的属性赋值。当对象的属性比较多时,创建对象的过程会变得比较烦琐。
例如,某个平台需要在开学季给Sunny School和Garden School两所学校的新同学注册账号。Sunny School是一所小学,新同学年龄大多为7岁;Garden School是一所中学,新同学年龄大多为13岁。在注册账号的过程中,如果同学没有电子邮箱的话还要为其注册电子邮箱。
先建造空对象,然后在不断调用set方法为对象属性赋值是一种常见的建造对象的方式,如下面代码所示。
User user01 = new User();
user01.setName("Candy");
user01.setEmail("[email protected]");
user01.setAge(7);
user01.setSex(1);
user01.setSchoolName("Sunny School");
这种方式需要了解对象的所有属性细节,是和对象的属性耦合的,而且这个过程中可能会导致属性的遗忘。
使用具有多个入参的构造方法直接建造对象也是一种常见的建造对象的方式,如下所示。
User user02 = new User("Candy", "[email protected]", 7, 1, "Sunny School");
这种情况下,为了能适应多种入参组合,通常需要重载出大量的构造方法。
建造者模式给我们提供了另一种建造对象的思路。使用建造者模式,对象的建造细节均交给建造者来完成,调用者只需要掌控总体流程即可,而不需要了解被建造对象的细节。
例如,我们编写一个UserBuilder接口作为建造User对象的接口,如下所示。
public interface UserBuilder {
public UserBuilder setEmail(String email);
public UserBuilder setAge(Integer age);
public UserBuilder setSex(Integer sex);
public User build();
}
然后我们继承UserBuilder接口编写一个SunnySchoolUserBuilder类,它用来建造Sunny School的用户,如下所示。
public class SunnySchoolUserBuilder implements UserBuilder {
private String name;
private String email;
private Integer age;
private Integer sex;
private String schoolName;
public SunnySchoolUserBuilder(String name) {
this.name = name;
}
public SunnySchoolUserBuilder setEmail(String email) {
this.email = email;
return this;
}
public SunnySchoolUserBuilder setAge(Integer age) {
this.age = age;
return this;
}
public SunnySchoolUserBuilder setSex(Integer sex) {
this.sex = sex;
return this;
}
public User build() {
if (this.name != null && this.email == null) {
this.email = this.name.toLowerCase().replace(" ", "").concat("@sunnyschool.com");
}
if (this.age == null) {
this.age = 7;
}
if (this.sex == null) {
this.sex = 0;
}
this.schoolName = "Sunny School";
return new User(name, email, age, sex, schoolName);
}
}
这样一来,我们可以灵活地建造对象。
// 用匿名建造者建造一个对象
User user03 = new SunnySchoolUserBuilder("Candy").setSex(1).build();
// 分步设置建造者属性,建造一个对象
UserBuilder userBuilder04 = new SunnySchoolUserBuilder("Eric");
userBuilder04.setEmail("[email protected]").build();
User user04 = userBuilder04.build();
基于建造者创建对象时,有以下几个优点:
- 使用建造者时十分灵活,可以一次也可以分多次地设置被被建造对象的属性。
- 调用者只需要调用建造者的主要流程而不需要关系建造对象的细节。
- 可以很方便地修改建造者的行为,从而建造出不同的对象。
下面是建造者模式的类图:
在学习了建造者模式后,我们可以为属性较多的类创建建造者类。建造者类一般包含两类方法:
- 一类是属性设置方法。这类方法一般有多个,可以接受不同类型参数来设置建造者的属性。
- 一类是目标对象生成方法。该类方法一般只有一个,即根据目前建造者中的属性创建出一个目标对象。
在需要创建复杂的对象时,建造者模式的优势将会体现地更为明显。因此建造者模式在一些大型的系统中非常常见。
以上示例和代码均参考自《通用源码阅读指导书——MyBatis源码详解》一书。
这是一本以MyBatis的源码为实例讲述源码阅读方法的书籍,并且附带有示例项目源码,MyBatis的全中文注解。书籍还总结了大量的编程知识和架构经验,对提升编程和架构能力十分有用。
我们接下来继续跟随这本书分析MyBatis中的建造者模式的使用。
MyBaits有一个专门的防止建造者的包,为builder
包。包中的BaseBuilder类是所有建造者的基类,下图是它与子类的类图。
BaseBuilder类虽然被声明成一个抽象类,但是本身不含有任何的抽象方法,因此它的子类无需实现它的任何方法。BaseBuilder类更像是一个工具类,为继承它的建造者类提供了众多实用的工具方法。当然,也确实有很多建造者类不需要BaseBuilder提供的工具方法,因此没有继承BaseBuilder,这些类有MapperAnnotationBuilder、SelectBuilder等。
BaseBuilder类提供的工具方法大致分为以下几类:
- *ValueOf:类型转化函数。负责将输入参数转换为指定的类型,并支持默认值设置。
- resolve*:字符串转枚举类型函数。根据字符串找出指定的枚举类型并返回
- createInstance:根据类型别名创建类型实例
- resolveTypeHandler:根据类型处理器别名返回类型处理器实例
在BaseBuilder类的子类中,MapperBuilderAssistant
类最为特殊,因为它本身不是建造者类而是一个建造者辅助类。它继承BaseBuilder类的原因仅仅是因为要使用BaseBuilder类中的方法。
其他的各个*Builder
类都是对应的建造者类,MyBatis正是通过它们来创建对应的对象。这种方式大大地提升了创建对象的遍历程度。
在需要创建参数众多的对象时,我们可以使用建造者模式,让创建对象变得更为灵活、简单。
可以访问个人知乎阅读更多文章:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。