1. 概述
在软件开发的过程中,当需要创建一个复杂对象时,该对象由多个子部件按一定算法组合而成,例如计算机(对象)是由 CPU、主板、内存、硬盘、显卡、机箱、显示器、键盘、鼠标等部件组装而成的,采购员不可能自己去组装计算机,而是将计算机的配置要求告诉计算机销售公司,计算机销售公司安排技术人员去组装计算机,然后再交给要买计算机的采购员。由于需求的变化,复杂对象的各个部分经常面临剧烈的变化,但将它们组合在一起的算法相对稳定。
生活中这样的例子很多:
-
如游戏中的不同角色,其性别、个性、能力、脸型、体型、服装、发型等特性都有所差异;
-
汽车由方向盘、发动机、车架、轮胎构成,但这些部件也多种多样;
-
每封电子邮件的发件人、收件人、主题、内容、附件等内容也各不相同。
以上所有这些产品都是由多个部件构成的,各个部件可以灵活选择,但其创建步骤都大同小异。这类产品的创建无法用前面介绍的工厂模式描述,只有建造者模式可以很好地描述该类产品的创建。‘
例子1:买肯德基
典型的儿童餐包括一个主食,一个辅食,一杯饮料和一个玩具(例如汉堡、炸鸡、可乐和玩具车)。这些在不同的儿童餐中可以是不同的,但是组合成儿童餐的过程是相同的。
客户端:顾客,想去买一套套餐(这里面包括汉堡,可乐,薯条),可以有1号和2号两种套餐供顾客选择。
指导者角色:收银员。知道顾客想要买什么样的套餐,并告诉餐馆员工去准备套餐。
建造者角色:餐馆员工。按照收银员的要求去准备具体的套餐,分别放入汉堡,可乐,薯条等。
产品角色:最后的套餐,所有的东西放在同一个盘子里面。
例子2**:计算工资**:工资的计算一般是:底薪+奖金-税。但底薪分为一级8000、二级6000、三级4000三个等级。根据岗位不同奖金的发放也不一样,管理及日常事务处理岗位(A类)每月根据领导及同事间的评议得分计算奖金,销售岗位(B类)则根据销售额发放提成。税金则根据奖金和底薪的数额进行计算。由此看出该工资的计算方式是比较稳定的构建算法,但对工资的每一部分都会根据不同的情况产生不同的算法,如何将客户端与变化巨烈的底薪、奖金和税金计算方式分离呢,这也比较适合用建造者模式。
2 . 问题
我们如何应对这种变化,如何提供一种“封装机制”来隔离“复杂对象的各个部”的变化,从而保持系统中的“稳定构建算法”而不随需求的变化而变化?
3. 解决方案
建造者模式: 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
4. 适用性
在以下情况使用Builder模式
•当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。
•当构造过程必须允许被构造的对象有不同的表示时。
5. 结 构
建造者模式分为两种,一种为经典建造者模式,另一种为变种建造者模式(建一个内部类映射到主类)。
经典建造者模式结构如下页上图所示。
6. 经典构建模式的组成
• 抽象建造者角色(Builder):为创建一个Product对象的各个部件指定抽象接口,以规范产品对象的各个组成成分的建造。一般而言,此角色规定要实现复杂对象的哪些部分的创建,并不涉及具体的对象部件的创建。
• 具体建造者(ConcreteBuilder)
-
实现Builder的接口以构造和装配该产品的各个部件。即实现抽象建造者角色Builder的方法。
-
定义并明确它所创建的表示,即针对不同的商业逻辑,具体化复杂对象的各部分的创建
-
提供一个检索产品的接口
-
构造一个使用Builder接口的对象即在指导者的调用下创建产品实例
指导者(Director):调用具体建造者角色以创建产品对象的各个部分。指导者并没有涉及具体产品类的信息,真正拥有具体产品的信息是具体建造者对象。它只负责保证对象各部分完整创建或按某种顺序创建。
产品角色(Product):建造中的复杂对象。它要包含那些定义组件的类,包括将这些组件装配成产品的接口。
7. 效果
Builder模式的主要效果:
-
它使你可以改变一个产品的内部表示
Builder对象提供给导向器一个构造产品的抽象接口。该接口使得生成器可以隐藏这个产品的表示和内部结构。它同时也隐藏了该产品是如何装配的。因为产品是通过抽象接口构造的,你在改变该产品的内部表示时所要做的只是定义一个新的生成器。 -
它将构造代码和表示代码分开
Builder模式通过封装一个复杂对象的创建和表示方式提高了对象的模块性。客户不需要知道定义产品内部结构的类的所有信息;这些类是不出现在Builder接口中的。每个Concrete
Builder包含了创建和装配一个特定产品的所有代码。这些代码只需要写一次;然后不同的Director可以复用它以在相同部件集合的基础上构作不同的Product。 -
它使你可对构造过程进行更精细的控制
Builder模式与一下子就生成产品的创建型模式不同,它是在导向者的控制下一步一步构造产品的。仅当该产品完成时导向者才从生成器中取回它。因此Builder接口相比其他创建型模式能更好的反映产品的构造过程。这使你可以更精细的控制构建过程,从而能更精细的控制所得产品的内部结构。
8. 经典构建模式实现实例:
指导者:收银员
<?php
/**
* 指导者:收银员
*
*/
class DirectorCashier
{
/**
* 收银餐馆员工返回的食物
*
*/
public function buildFood(Builder $builder) {
$builder->buildPart1();
$builder->buildPart2();
}
}
抽象建造者:
/**
* 抽象建造者
*
*/
abstract class Builder
{
/**
* 创建产品的第一部分
*/
public abstract function buildPart1();
/**
*
* 创建产品的第二部分
*/
public abstract function buildPart2();
/**
*
* 返回产品
*/
public abstract function getProduct();
}
具体建造者类:
/**
* 具体建造者类:餐馆员工,返回的套餐是:汉堡两个+饮料一个
*
*/
class ConcreteBuilder1 extends Builder
{
protected $_product = null;//产品对象
function __construct(){
$this->_product = new Product();
}
/**
* 创建产品的第一部分::汉堡=2
*/
public function buildPart1()
{
$this->_product->add('Hamburger',2);
}
/**
*
* 创建产品的第二部分:
*/
public function buildPart2()
{
$this->_product->add('Drink', 1);
}
/**
* 返回产品对象 :
*
*/
public function getProduct() {
return $this->_product;
}
}
/**
* 具体建造者类:餐馆员工,汉堡1个+饮料2个
*
*/
class ConcreteBuilder2 extends Builder
{
protected $_product = null;//产品对象
function __construct(){
$this->_product = new Product();
}
/**
* 创建产品的第一部分:汉堡
*/
public function buildPart1()
{
$this->_product->add('Hamburger', 1);
}
/**
*
* 创建产品的第二部分:drink=2
*/
public function buildPart2()
{
$this->_product->add('Drink', 2);
}
/**
* 返回产品对象 :
*
*/
public function getProduct() {
return $this->_product;
}
}
产品类:
/**
* 产品类
*/
class Product
{
public $products = array();
/**
* 添加具体产品
*/
public function add($name, $value) {
$this->products[$name] = $value;
}
/**
* 给顾客查看产品
*/
public function showToClient()
{
foreach ($this->products as $key => $v) {
echo $key , '=' , $v ,'<br>';
}
}
}
客户程序:
//客户程序
class Client
{
/**
* 顾客购买套餐
*
*/
public function buy($type) {
//指导者,收银员
$director = new DirectorCashier();
//餐馆员工,收银员
$class = new ReflectionClass('ConcreteBuilder' .$type );
$concreteBuilder = $class->newInstanceArgs();
//收银员组合员工返回的食物
$director->buildFood($concreteBuilder);
//返回给顾客
$concreteBuilder->getProduct()->showToClient();
}
}
//测试
ini_set('display_errors', 'On');
$c = new Client();
$c->buy(1);//购买套餐1
$c->buy(2);//购买套餐1
不过在我们日常开发中经典Builder模式一般不常用,用的比较多的还是变种的Builder模式,接下来我们来看下变种的Builder模式。
9、变种Builder模式:链式builder
汽车由发动机、方向盘、车身、轮胎构成,但这些部件也多种多样:
public class Car {
private String engine;
private String rack;
private String tire;
private String steering;
public Car(CarBuilder carBuilder) {
this.engine = carBuilder.engine;
this.rack = carBuilder.rack;
this.tire = carBuilder.tire;
this.steering = carBuilder.steering;
}
public static class CarBuilder {
private String engine;
private String rack;
private String tire;
private String steering;
public CarBuilder engine(String engine) {
this.engine = engine;
return this;
}
public CarBuilder rack(String rack) {
this.rack = rack;
return this;
}
public CarBuilder tire(String steering) {
this.tire = tire;
return this;
}
public CarBuilder steering(String steering) {
this.steering = steering;
return this;
}
public Car build() {
return new Car(this);
}
}
}
客户端不直接调用所需的对象,而是调用构造方法 (或静态工厂),并使用所有必需的参数,并获得一个 builder 对象。然后,客户端调用 builder 对象的 setter 相似方法来设置每个可选参数。最后,客户端调用一个无参的 build 方法来生成对象,该对象通常是不可变的。Builder 通常是它所构建的类的一个静态成员类。以下是它在实践中的示例:
Car car = new CarBuilder()
.engine('v12')
.rack('镁合金')
.steering('方向盘')
.tire("米其林")
.build();
9. 建造者模式的优点
首先,建造者模式的封装性很好。使用建造者模式可以有效的封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在导演类中对整体而言可以取得比较好的稳定性。
其次,建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险。
10. 建造者模式与工厂模式的区别
我们可以看到,建造者模式与工厂模式是极为相似的,总体上,建造者模式仅仅只比工厂模式多了一个“导演类”的角色。在建造者模式的类图中,假如把这个导演类看做是最终调用的客户端,那么图中剩余的部分就可以看作是一个简单的工厂模式了。
与工厂模式相比,建造者模式一般用来创建更为复杂的对象,因为对象的创建过程更为复杂,因此将对象的创建过程独立出来组成一个新的类——导演类。也就是说,工厂模式是将对象的全部创建过程封装在工厂类中,由工厂类向客户端提供最终的产品;而建造者模式中,建造者类一般只提供产品类中各个组件的建造,而将具体建造过程交付给导演类。由导演类负责将各个组件按照特定的规则组建为产品,然后将组建好的产品交付给客户端。
11. 总结
建造者模式与工厂模式类似,他们都是建造者模式,适用的场景也很相似。一般来说,如果产品的建造很复杂,那么请用工厂模式;如果产品的建造更复杂,那么请用建造者模式。