实战生成器模式(Builder Pattern In Practice)

原文链接:http://www.javacodegeeks.com/2013/01/the-builder-pattern-in-practice.html

我不会详细介绍这个模式,因为已经有大量的文章或者书籍对该模式进行过详细的解析。我将告诉你的是为什么以及什么时候你应该考虑使用它。值得一提的是,我所介绍的这个模式和设计模式四人帮的书(《设计模式:可复用面向对象软件的基础》)里面的有些许区别。四人帮书里面介绍的生成器模式重点在抽象出对象创建的步骤,并通过调用不同的具体实现从而得到不同的结果,本文介绍的生成器模式目的在于移除因多个重载构造函数,可选的参数和setters的过度使用引入的不必要的复杂性。

如果你的代码中定义了类似下面的拥有大量属性的User类,假设你想把该类定义为不可变的(顺便说一句,除非有足够的理由,否者你应该尽量将变量定义为不可变,我们会在另一篇文章中谈及。)

public class User {
    private final String firstName;    //required
    private final String lastName;    //required
    private final int age;    //optional
    private final String phone;    //optional
    private final String address;    //optional
...
}

现在假设User类的某些属性是必须的,某些属性是可选的,那么你会如何构建这样一个类的实例呢?由于所有属性都声明为final,所以你需要在构造函数中把它们都赋值,但你又想让该类的使用者在实例化时可以忽略可选的参数。

最简单有效的方法是定义一个只接收必选参数的构造函数,一个接收所有必选参数和一个可选参数的构造函数,一个接收所有必选参数和两个可选参数的构造函数,依次类推。这样的代码看起来是怎样的呢?大概如下所示:

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;
}

好消息是这样创建类实例的方式是可行的。然而这种方法存在的问题是相当明显的。当类只有少量几个属性时,这种方式也没什么大碍,但随着类属性个数的增加,代码将变得越来越难以阅读和维护。更严重的是,对调用者而言,代码也变得更加难以使用。作为调用者,我应该使用哪个构造函数呢?是两个参数的构造函数还是三个参数的呢?如果我不显示指定可选参数的值,那它的默认值是多少呢?如果我只要设置地址而不想设置年龄和手机号码呢?在这种情况下,我需要调用具有指定参数的构造函数,并传递一个默认值给那些我不感兴趣的参数。另外,当几个参数具有相同类型时,也很容易混淆调用者的使用,第一个String类型的参数是指的手机号码还是地址呢?

那么对以上这些情况我们有其他的选择吗?我们可以遵循JavaBeans规范,定义一个默认无参构造函数,并对每个属性提供setters和getters函数,如下面所示:

public class User {
	private String firstName; // required
	private String lastName; // required
	private int age; // optional
	private String phone; // optional
	private String address;  //optional

	public String getFirstName() {
		return firstName;
	}
	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}
	public String getLastName() {
		return lastName;
	}
	public void setLastName(String lastName) {
		this.lastName = lastName;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	public String getPhone() {
		return phone;
	}
	public void setPhone(String phone) {
		this.phone = phone;
	}
	public String getAddress() {
		return address;
	}
	public void setAddress(String address) {
		this.address = address;
	}
}

上面这种方法貌似易于阅读和维护。作为使用者,我可以创建一个空实例,并只设置我感兴趣的属性值。哪里出错了吗?这种解决方案有两个主要的问题。第一个问题是使得该类的实例具有不连续的状态。如果你想创建一个同时具有五个属性值的类实例,那么直到所有属性值的设置函数setX被调用了,该实例才具有完整的状态。这意味着调用者应用程序的某些模块可能会看到这个实例的不完整的状态。第二个缺点是这种方案使得User类是可变的,你会因此而失去很多不可变对象的好处。

幸运的是,我们有第三种选择:生成器模式。这种解决方案类似下面代码所示:

public class User {
	private final String firstName; // required
	private final String lastName; // required
	private final int age; // optional
	private final String phone; // optional
	private final String address; // optional

	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);
		}

	}
}

有几个关键点需要注意一下:

1)User类的构造函数是私有的,这意味着调用者不能直接实例化这个类;

2)这个类再次成为不可变的,所有必选的属性值都是final的并且在构造函数中设置。另外,对属性值我们只提供getters函数;

3)该模式使用Fluent Interface惯用法(参见http://martinfowler.com/bliki/FluentInterface.html)使得调用者的代码更加可读(一会儿我们会看到这样的例子);

4)该模式的构造函数只接收必选的属性值作为参数,也只有这些必选的属性值被设置为final,以此保证它们在构造函数中设置;

生成器模式具有上面介绍过的其他两个方案的优点,同时又没有它们的缺点。调用者的代码更容易编写,更重要的是,更容易阅读。我听过的对该模式唯一的批评是你必须在builder内部类中重复外部类的属性定义。然而,鉴于builder类通常是它所构建的类的静态内部类,它们可以很容易同步的修改。

现在,试图创建User实例的调用者代码看起来是怎样的呢?如下所示:

public User getUser() {
	return new
			User.UserBuilder('Jhon', 'Doe')
			.age(30)
			.phone('1234567')
			.address('Fake address 1234')
			.build();
}

相当简洁,不是吗?你可以使用一行代码就创建User实例,更重要的是,代码易于阅读。而且你可以保证任何时候获取的User实例都处于完整的状态。这个模式相当灵活,一个builder可以在调用build函数之前通过设置不同的属性值来创建不同的类实例。builder甚至可以在每次调用之间自动补全生成的字段,例如id值或者序列号等。重要的一点是,类似构造函数,builder能够使得其参数为不可变的。build函数能够检查这些不可变参数并在参数无效时抛出IllegalStateException异常。

参数是从builder类拷贝到外部类,并在外部类而不是builder类中进行有效性校验的,这一点至关重要。原因在于builder类不是线程安全的,如果我们在创建外部类对象之前检查参数有效性,那么在参数校验和参数被拷贝到外部类的时间段之间,这些参数的值可能被另一个线程所更改。这个时间段就是著名的“脆弱之时”(“window of vulnerability”,脆弱之时,尤指冷战时期,美国的陆基导弹很容易成为苏联首次攻击目标的论点)。在我们的User例子中,代码类似下面所示:

public User build() {
    User user = new user(this);
    if (user.getAge() < 120) {
        throw new IllegalStateException(“Age out of range”); // thread-safe
    }
    return user;
}

上面的版本是线程安全的,因为我们先创建了User的实例,然后才对User不可变实例中不可变量进行校验。而下面的代码看起来实现相同的功能,但却是非线程安全的,因此我们要避免使用这样的方式:

public User build() {
    if (age < 120) {
        throw new IllegalStateException(“Age out of range”); // bad, not thread-safe
    }
    // This is the window of opportunity for a second thread to modify the value of age
    return new User(this);
}

该模式最后一个好处是builder可以传递给另外一个函数,使得该函数可以为调用者创建一个或者多个对象实例,而不需要知道对象实例的具体创建细节。为了达到这个目的,我们通常会定义一个简单的接口如下所示:

public interface Builder<T extends User> {
    T build();
}

在前面的User例子中,UserBuilder类需要改为实现Builder<User>接口,这样一来,build函数类似如下所示:

UserCollection buildUserCollection(Builder<? extends User> userBuilder){...}

好吧,这是我写过的第一篇长博客,总结一下,生成器模式对于具有多于几个参数(并不精准,通常对于具有4个或者以上属性的类我会使用这个模式)的类的构造是个很好的选择,特别是当这些属性多数是可选的时候。应用这个模式可以使得调用者代码易于阅读,编写和维护。此外,由于你的类是不可变的你的代码将更加安全。

更新:如果你使用Eclipse作为你的IDE,那么你可以使用不少插件来简化该模式引入的样板代码的编写。我知道的三个插件如下:

1)http://code.google.com/p/bpep/

2)http://code.google.com/a/eclipselabs.org/p/bob-the-builder/

3)http://code.google.com/p/fluent-builders-generator-eclipse-plugin/

我自己没有试用过这些插件,所以不能下结论说哪一个更好用。我想其他IDE也存在类似的插件。

时间: 2024-10-03 20:48:16

实战生成器模式(Builder Pattern In Practice)的相关文章

生成器模式(Builder Pattern)

一. 建造者(Builder)模式 建造者模式可以将一个产品的内部表象与产品的生成过程分割开来,从而可以使一个建造过程生成具有不同的内部表象的产品对象. 对象性质的建造 有些情况下,一个对象会有一些重要的性质,在它们没有恰当的值之前,对象不能作为一个完整的产品使用.比如,一个电子邮件有发件人地址.收件人地址.主题.内容.附录等部分,而在最起码的收件人地址未被赋值之前,这个电子邮件不能发出. 有些情况下,一个对象的一些性质必须按照某个顺序赋值才有意义.在某个性质没有赋值之前,另一个性质则无法赋值.

[设计模式]&lt;2&gt;. C++与生成器模式(Builder pattern)

原文地址: http://www.cnblogs.com/hebaichuanyeah/p/5585957.html 当构建一个复杂对象时,将构建过程与表示分离.使得同样的过程创建不同的对象. 简单例子,构建produce类,需要构建三个部分part1,part2,part3.通过build类去构建它们,并返回.通过director 类调用build对象进行配置. C++代码 #include <iostream> using namespace std; class Produce { pu

建造者模式(Builder Pattern)

模式定义 造者模式(Builder Pattern):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示. UML类图 Builder:抽象建造者 ConcreteBuilder:具体建造者 Director:指挥者 Product:产品角色 代码结构 public static class BuilderApp { public static void Run() { Director director = new Director(); Builder b1 = new

设计模式之七:建造模式(Builder Pattern)

建造者模式就是将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示. 适用范围: 当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时. 当构造过程必须允许被构造的对象有不同表示时. 建造者模式里面有四个角色: Builder: 给出一个抽象接口,以规范产品对象的各个组成部分的建造.一般而言,此接口独立于应用程序的业务逻辑.模式中直接创建产品对象的具体创建者角色.具体创建者角色必须实现这个接口的所有方法:一个是建造方法,另一个是结果返还方法. ConcreteBu

模式02 生成器模式(Builder)

1. 意图 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示. 2. 结构 此模式的结构如下图所示. 3. 参与者 Builder-- 为创建一个Product对象的各个部件指定抽象接口. ConcreteBuilder-- 实现Builder的接口以构造和装配该产品的各个部件.-- 定义并明确它所创建的表示.-- 提供一个检索产品的接口. Director-- 构造一个使用Builder接口的对象. Product-- 表示被构造的复杂对象.ConcreteBuilde

23种设计模式--建造者模式-Builder Pattern

一.建造模式的介绍       建造者模式就是将零件组装成一个整体,用官方一点的话来讲就是将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示.生活中比如说组装电脑,汽车等等这些都是建造者模式的具体体现,组装电脑反应到软件上就是一个复杂的对象,然后我们使用建造者模式的时候需要抽象一个建造类,抽象一个指挥者指挥具体实现的那个类,然后就是具体实现这个对象的类,这样就避免了每新创建一个不同的复杂对象就需要重新写一下这个类,这样就只要重写建造者就可以了,我们接下来就用组装电脑这个来说明一

【java设计模式】【创建模式Creational Pattern】建造模式Builder Pattern

1 package com.tn.pattern; 2 3 public class Client { 4 public static void main(String[] args) { 5 Director director=Director.getInstance(); 6 director.construct(new ConcreteBuilder1()); 7 director.construct(new ConcreteBuilder2()); 8 } 9 } 10 11 class

5.建造者模式(Builder Pattern)

using System; using System.Collections.Generic; namespace ConsoleApplication4 { class Program { /// <summary> /// 以组装电脑为例子 /// 每台电脑的组成过程都是一致的,但是使用同样的构建过程可以创建不同的表示(即可以组装成不一样的电脑,配置不一样) /// 组装电脑的这个场景就可以应用建造者模式来设计 /// </summary> /// <param name

设计模式(创建型)之建造者模式(Builder Pattern)

PS一句:最终还是选择CSDN来整理发表这几年的知识点,该文章平行迁移到CSDN.因为CSDN也支持MarkDown语法了,牛逼啊! 概述 建造者模式将客户端与包含多个组成部分的复杂对象的创建过程分离,客户端压根不用知道复杂对象的内部组成部分与装配方式,只需要知道所需建造者的类型即可.它关注如何一步一步创建一个的复杂对象,不同的具体建造者定义了不同的创建过程,且具体建造者相互独立,增加新的建造者非常方便,无须修改已有代码,系统具有较好的扩展性. 问题来了... 你可能会有疑惑,建造者模式和抽象工