构建者模式(Builder)

来源:互联网 发布:伊朗革命卫队 知乎 编辑:程序博客网 时间:2024/04/28 17:55

WHAT

复杂应用:

当一个应用相关的类和对象参数逐步复杂化,数量逐步增加时,这个应用也会伴随着变得复杂。那么对于复杂对象的使用,为了尽可能降低后期维护成本,我们就不能按照传统的简易方式使用。
举个例子:如果我们的应用涉及到人,并且是国家级信息,需要记录一个人的所有信息,如全名、工作、薪资、家庭、教育等只要与该人相关的信息都需要记录。由于每个人的阅历背景都是不同的,正式由于这样的多样性,信息的广泛性,造就了人这个对象是极其的复杂。那么采用什么样的方式可以做到每种人能够各取所需呢?今天我们讲的就是提供一个可以解决这类复杂应用的解决方案。

动机:

1、传统构造器的缺点:

        ①不能很好地扩展大量的可选参数。

        ②当构造参数不断增加时,按照原来做法得采用众多重载构造函数,构造函数的数量将按照指数级增长,客户端代码会很    难编写,并且仍然难以阅读。

        ③一长串类型相同的参数会导致一些细微的错误,如果客户端不小心颠倒了两个参数的顺序,编译器不会报错,但是程序    运行时会出错,这给排查错误造成了潜在的成本。

2、为了保证一些特殊情况下,组件的可移植性,预防应用的硬编码行为,造成后期不必要的麻烦。


目的:

1、将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
2、通过使用Builder模式来减少构造函数所需要的参数个数及构造函数的数量。

作用:

1、它使得我们可以改变一个产品的内部表示。 比如原装电脑和组装电脑的区别。
2、它将构造代码和表示代码分开。 
3、它是一步一步的构造产品,并不是一下子就生产产品,它使得你可对构造过程进行更精细的控制。

WHY

解决上述问题方案分析:


1、采用JavaBeans模式,将多个参数转换成实体,通过setter方法来设置每个必要或可选的参数。
潜在问题:
            1、单线程环境下,此做法可行,但是在多线程环境下,在构造过程中JavaBean可能处于不一致的状态。类无法仅仅通过检验构造参数的有效性来保证一致性,因为调试起来也十分困难。
            2、JavaBeans模式阻止了把类做成不可变的可能,需要程序员付出额外的努力来确保它的线程安全。
            3、部分错误会在运行时导致错误,因为编译器无法确保程序员会再使用之前先在对象上调用freeze方法。
2、为了既能保证重叠构造器模式那样安全性,也能保证像JavaBeans模式那样好的阅读性,也就是Builder模式。不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器,得到一个builder对象,然后可以通过相关setter方法来设置每个相关的可选参数。最后,通过调用无参的build方法来生成不可变的对象。
        缺点:
            1、为了创建对象,必须先创建它的构造器,造成了一些额外的开销,如果在十分注重性能的情况下,可能会有一些影响。
2、代码会变得非常的冗长。

创建型设计模式间关系及区别:

往往设计是始于Factory Method(工厂模式)随着项目扩展会发展到引入Abstract Factory(抽象工厂),Prototype(原型模式),或Builder(建造者模式)来达到复杂代码更具灵活的需要。
Builder着重于一步步构造一个复杂对象,而Abstract Factory着重于多个系列的产品对象(简单的或复杂的)。
Builder在最后的一步返回产品,而Abstract Factory是立即返回产品。
Builder目的在于找到一个可伸缩构造的解决方案,而Abstract Factory目的是为了能够实现多态性。
正是由于方式的不同,在一些时候创建型设计模式本身是互补的,可以相互搭配使用来达到更佳的效果。


适用性:

1、当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。
2、当构造过程必须允许被构造的对象有不同的表示时。
3、当构造函数所需要太多参数,并且这些参数是可装配的。
4、如果类的构造器或者静态方法中具有多个参数
有符合上述条件的类,设计时,Builder模式就是种不错的选择。


HOW

Builder官方模式结构分析


(此结构图来自Wikipedia的Builder文章)
就按照官方的结构图进行概念性的讲诉Builder模式一些成员的作用。

Director: 使用Builder接口的对象。
Builder: 创建一个Product对象的各个部件指定的抽象接口。
ConcreteBuilder: 实现Builder的接口以构造和装配该产品的各个部件。
Product: 被构造的复杂对象。

从上述UML图可以发现,Builder本身是一个抽象,在真实的产品中,可能会有很多层级的构建器所以ConcreteBuilder看似是一个,本质是泛指实际的构造器,下面通过案例代码来实践比较一下。

案例背景

以下通过人的全名、地址、性别等一些人物信息的初始化作为案例来进行传统构造器和Builder模式的比较,让大家更佳接地气的了解Builder模式的优缺点。

案例代码结构说明



Builder设计模式下(仅builder包路径下代码)
first包路径下的代码: 传统的构造器+Builder模式(未进行代码重构)
second包路径下的代码:进行代码重构和优化后的代码
Builder.java: 抽象接口
testMain.java: 测试类

接下来通过最粗糙代码开始讲解,不断进行优化,并运用Builder进行代码的重构,最终大家会发现它们之间的优缺点。

传统构造器实现

Person.java: 没有采用Builder设计模式前的Person类。 具体那些字段什么用大家可以不用深入了解,只需要知道这个Person有以下那些属性,实际上人的信息数量下面是永不可及的。但目前这么多字段就可以进行这个知识点的讲解了。
public class Person {private final String lastName;private final String firstName;private final String middleName;private final String salutation;private final String suffix;private final String streetAddress;private final String city;private final String state;private final boolean isFemale;private final boolean isEmployed;private final boolean isHomewOwner;// 传统的构造器方法, 此时可以看出,参数个数非常的多,在构造时非常容易混乱public Person(final String newLastName, final String newFirstName,final String newMiddleName, final String newSalutation,final String newSuffix, final String newStreetAddress,final String newCity, final String newState,final boolean newIsFemale, final boolean newIsEmployed,final boolean newIsHomeOwner) {this.lastName = newLastName;this.firstName = newFirstName;this.middleName = newMiddleName;this.salutation = newSalutation;this.suffix = newSuffix;this.streetAddress = newStreetAddress;this.city = newCity;this.state = newState;this.isFemale = newIsFemale;this.isEmployed = newIsEmployed;this.isHomewOwner = newIsHomeOwner;}}
从上述使用可以看出存在几点问题:
1、一些不需要的属性,需要手动设置null,非常的麻烦。
2、当参数数量过多时,人脑很难直接正确的映射出属性与参数位置,存在数据不小心填到错误的位置,如果恰巧两个属性的类型相同,那么编译器还无法发觉,在运行时出错,这个问题是非常严重的。
那么采用Builder模式,来看看最终会变成什么样

Builder模式说明

两种方式:
1、可以将Builder内嵌在相关的实体类中。
2、采用以下的抽象Builder,通过泛型来实现每个Builder所构建的产品对象。
/** * * @ClassName: Builder * @Description: 通过泛型来满足所有的builder。 * @author YK.Lin* @date 2015年1月2日 下午3:06:11 * * @param <T> builder 期望的类型 */public interface Builder<T> {public T build();}
由于一般人都是采用内嵌的方式实现,比较简单,并且可以很好的因此实体的内部信息。
本例采用第二种方式进行说明。每一个构建器通过build()方法可以创建出在build()方法之前用户所赋予的属性。
PersonBuilder.java 用于构建Person类的构建器。
public class PersonBuilder implements Builder<Person> {private String newLastName;private String newFirstName;private String newMiddleName;private String newSalutation;private String newSuffix;private String newStreetAddress;private String newCity;private String newState;private boolean newIsFemale;private boolean newIsEmployed;private boolean newIsHomeOwner;public PersonBuilder() {}public PersonBuilder setNewLastName(String newLastName) {this.newLastName = newLastName;return this;}public PersonBuilder setNewFirstName(String newFirstName) {this.newFirstName = newFirstName;return this;}public PersonBuilder setNewMiddleName(String newMiddleName) {this.newMiddleName = newMiddleName;return this;}public PersonBuilder setNewSalutation(String newSalutation) {this.newSalutation = newSalutation;return this;}public PersonBuilder setNewSuffix(String newSuffix) {this.newSuffix = newSuffix;return this;}public PersonBuilder setNewStreetAddress(String newStreetAddress) {this.newStreetAddress = newStreetAddress;return this;}public PersonBuilder setNewCity(String newCity) {this.newCity = newCity;return this;}public PersonBuilder setNewState(String newState) {this.newState = newState;return this;}public PersonBuilder setNewIsFemale(boolean newIsFemale) {this.newIsFemale = newIsFemale;return this;}public PersonBuilder setNewIsEmployed(boolean newIsEmployed) {this.newIsEmployed = newIsEmployed;return this;}public PersonBuilder setNewIsHomeOwner(boolean newIsHomeOwner) {this.newIsHomeOwner = newIsHomeOwner;return this;}@Overridepublic Person build() {return new Person(newLastName, newFirstName, newMiddleName,newSalutation, newSuffix, newStreetAddress, newCity, newState,newIsFemale, newIsEmployed, newIsHomeOwner);}}

我们会发现,真正实例化的代码被移植到了PersonBuilder的build()方法中,所以之前说容易错误的问题还是存在,这个后面再进行解决方案说明。接下来看看引入了Builder模式之后,现在如何构建对象。
// 构建器构造对象方法. 没有设置的属性统一采用默认值PersonBuilder pb = new PersonBuilder();pb.setNewCity("北京").setNewFirstName("林大厨").setNewIsEmployed(false).setNewIsFemale(false);Person person2 = pb.build();
构造一个和之前一样的person对象,可以发现代码变得清晰很多,不是直接初始化Person对象,而且隐藏了Person的相关信息,通过PersonBuilder进行Person对象的构造。那些没有调用的set属性方法就默认采用默认的值,如middleName,那么就是默认为NULL。当设置完之后,通过build方法就可以构造出用户指定属性的Person对象。
分析到这里,回头看看Builder模式的目的和优缺点,是否会明朗很多。
当然代码这样,还是很粗糙的,绝对不能满足实际的需求。
这个时候需要借助重构的知识。如何不改变软件行为的前提下,改善其内部结构。
以上情况可以采用Introduce Parameter Object(引入参数对象)进行较多参数的重构。

重构后的Builder模式

构建器会进行细分:

为了区别之前的Person类,将重构后的Person类用新的Person2表示, PersonBuilder用PersonBuilder2表示。

public class Person2 {private final FullName name;private final Address address;private final Gender gender;private final EmploymentStatus employment;private final HomeownerStatus homeOwnerStatus;public Person2(final FullName newName, final Address newAddress,final Gender newGender, final EmploymentStatus newEmployment,final HomeownerStatus newHomeOwner) {this.name = newName;this.address = newAddress;this.gender = newGender;this.employment = newEmployment;this.homeOwnerStatus = newHomeOwner;}public FullName getName() {return this.name;}public Address getAddress() {return this.address;}public Gender getGender() {return this.gender;}public EmploymentStatus getEmployment() {return this.employment;}public HomeownerStatus getHomeOwnerStatus() {return this.homeOwnerStatus;}}

比对之前的Person构造函数:
public Person(final String newLastName, final String newFirstName,final String newMiddleName, final String newSalutation,final String newSuffix, final String newStreetAddress,final String newCity, final String newState,final boolean newIsFemale, final boolean newIsEmployed,final boolean newIsHomeOwner) {}
发现参数个数少了很多,并且清晰了很多,那些具有相同性质的属性都细分到了一个属性类去了。
比如lastName,firstName,middleName 等和姓名相关的属性被抽到了FullName对象中。

public class FullName {private final Name lastName;private final Name firstName;private final Name middleName;private final Salutation salutation;private final Suffix suffix;public FullName(final Name newLastName, final Name newFirstName,final Name newMiddleName, final Salutation newSalutation,final Suffix newSuffix) {this.lastName = newLastName;this.firstName = newFirstName;this.middleName = newMiddleName;this.salutation = newSalutation;this.suffix = newSuffix;}public Name getLastName() {return this.lastName;}public Name getFirstName() {return this.firstName;}public Name getMiddleName() {return this.middleName;}public Salutation getSalutation() {return this.salutation;}public Suffix getSuffix() {return this.suffix;}@Overridepublic String toString() {return this.salutation + " " + this.firstName + " " + this.middleName+ this.lastName + ", " + this.suffix;}}

public class Name {String name;public Name(String name) {this.name = name;}public String getName() {return name;}public void setName(String name) {this.name = name;}}

public class Salutation {// Salutation 的相关属性}

这就是引入参数对象的重构方式,那么就会引入新的FullNameBuilder来进行FullName对象的构建。
public class FullNameBuilder implements Builder<FullName> {private final Name nestedLastName;private final Name nestedFirstName;private Name nestedMiddleName;private Salutation nestedSalutation;private Suffix nestedSuffix;public FullNameBuilder(final Name newLastName, final Name newFirstName) {this.nestedLastName = newLastName;this.nestedFirstName = newFirstName;}public FullNameBuilder middleName(final Name newMiddleName) {this.nestedMiddleName = newMiddleName;return this;}public FullNameBuilder salutation(final Salutation newSalutation) {this.nestedSalutation = newSalutation;return this;}public FullNameBuilder suffix(final Suffix newSuffix) {this.nestedSuffix = newSuffix;return this;}@Overridepublic FullName build() {return new FullName(nestedLastName, nestedFirstName, nestedMiddleName,nestedSalutation, nestedSuffix);}}

地址信息构建器
public class AddressBuilder implements Builder<Address> {private StreetAddress nestedStreetAddress;private final City nestedCity;private final State nestedState;public AddressBuilder(final City newCity, final State newState) {this.nestedCity = newCity;this.nestedState = newState;}public AddressBuilder streetAddress(final StreetAddress newStreetAddress) {this.nestedStreetAddress = newStreetAddress;return this;}@Overridepublic Address build() {return new Address(nestedStreetAddress, nestedCity, nestedState);}}

通过这些抽取,尽量保证每次调用传统构造器的数量能保证在4-5个以下,保证普通人的阅读记忆范围内最佳。如果过多就要进行多级细分,其他的属性也是类似的,这里就不一一说明。考虑篇幅问题代码就不一一贴上来了。
那么重构之后的代码,我们要如何使用?
// 构建器构造对象方法. 没有设置的属性统一采用默认值PersonBuilder pb = new PersonBuilder();pb.setNewCity("北京").setNewFirstName("林大厨").setNewIsEmployed(false).setNewIsFemale(false);Person person2 = pb.build();// 优化后的构建器构造对象方法.FullNameBuilder fnb = new FullNameBuilder(null, new Name("林大厨"));FullName fullName = fnb.build();AddressBuilder ab = new AddressBuilder(new City("北京"), new State());ab.streetAddress(new StreetAddress());Address address = ab.build();PersonBuilder2 pb2 = new PersonBuilder2(fullName, address).gender(Gender.MALE).employment(EmploymentStatus.EMPLOYED);Person2 person3 = pb2.build();

可以发现,重构后的代码,使用时变得非常的繁琐,但是它也带来了很大的好处,就是构建的过程变得很清晰,无论以后你的项目多么复杂,也可以很简单的了解它们之间的关系,大大降低了后期的维护成本。也达到了代码即是注释,注释即是代码的最高编码境界!

总结

当对象有很多的参数时,我是真的非常喜欢用Builder模式来构造对象,尤其是这些参数很多都是null或者它们很多共享同一的数据类型时。很多开发人员也许感觉,在较少参数时,构造采用Builder模式并没有什么好处,尤其是很少的参数,并且它们需要不同的类型。 在一些情况下,它也许考虑传统的重叠构造器模式,如果不是要求不变性,还可以使用无参构造器搭配setter方法进行参数的配置也行。在项目设计时,将来可能要增加参数,如果一开始就使用构造器或者静态工厂,等到类需要多个参数时才添加构建器,就会无法控制,那些过时的构造器或者静态工厂显得十分不协调。因此,如果可以明确这些对象未来具有扩展的可能,通常最好一开始就使用构建器。

0 0
原创粉丝点击