一、什么是建造者模式
建造者模式,又称构建者模式 或者 生成器模式。所谓的"建造者"顾名思义也是一个用来创建对象的类,它可以将对象的构建逻辑和对象的行为逻辑独立开来。而且对建造者类进行多态可以得到一个同一类型不同状态的对象。
直接使用构造函数或者配合 setter 方法就能创建对象,为什么还需要建造者模式来创建呢?建造者模式和工厂模式都可以创建对象,那它们两个的区别在哪里呢?
下面我们通过一个例子来学习建造者模式的使用场景和用法。
二、建造者模式的用法和使用场景
现在假设我有个需求,需要定义一个资源池配置类 ResourcePoolConfig。资源池可以简单理解为线程池、连接池、对象池等。在这个资源池配置类中,有以下几个成员变量。
<?php
class ResourcePoolConfig {
const DEFAULT_MAX_TOTAL = 8;
const DEFAULT_MAX_IDLE = 8;
const DEFAULT_MIN_IDLE = 0;
private $name;
private $maxTotal;
private $maxIdle;
private $minIdle;
public function __construct($name, $maxTotal = self::DEFAULT_MAX_TOTAL, $maxIdle = self::DEFAULT_MAX_IDLE, $minIdle = self::DEFAULT_MIN_IDLE) {
if (empty($name)) {
throw new \Exception("name should not be empty.");
}
$this->name = $name;
$this->maxTotal = $maxTotal;
$this->maxIdle = $maxIdle;
$this->minIdle = $minIdle;
}
//...省略getter方法...
}
现在考虑一个问题:如果可配置项逐渐增多,变成了 8 个、10 个。沿用现在的设计思路,构造函数的参数会变得很长,代码在可读性和易用性上都会变差。在使用构造函数的时候,我们就容易搞错各参数的顺序,传递进错误的参数值,导致非常隐蔽的 bug。
这个问题可以通过为每个成员变量设置setter方法解决,将构造函数的参数缩减为少量的几个重要成员,其他成员可以通过setter方法设置。
可是如果成员属性赋值的逻辑必须在构造函数完成,而不能通过setter方法完成的话,那么还是会出现上面所说的构造函数参数列表变长的问题。此时该怎么解决呢?
可能有人会说,什么场景会出现这种不能定义或者使用setter方法来给属性赋值的情况呢?针对上面的示例,假如我们再提出下面3个额外的需求,此时就无法使用setter方法给属性赋值:
1、上面的成员只有name 是必填的,将它放到构造函数可以强制创建对象的时候就设置。如果我们把必填项通过 set() 方法设置,那我们根本不知道调用者或者开发者有没有去设置这个必填项。此时就必须将成员放到构造函数作为参数传入。
2、假设配置项之间有一定的依赖关系,比如如果用户设置了 maxTotal、maxIdle、minIdle 其中一个,就必须显式地设置另外两个;或者配置项之间有一定的约束条件,比如,maxIdle 和 minIdle 要小于等于 maxTotal。如果使用setter方法,那属性的依赖关系或者约束条件的校验逻辑就无处安放了(无法放到setter方法中)。
3、如果我们希望 ResourcePoolConfig 类对象是不可变对象,也就是说对象在创建好之后,就不能再修改内部的属性值。这样的话我们就不能在 ResourcePoolConfig 类中暴露 set() 方法。
此时建造者模式就可以解决这个 属性必须在构造函数赋值但又怕构造函数参数列表过多 的问题。
具体要怎么做呢?
先创建一个和目标对象有相同属性成员的建造者类,通过建造者的 setter 方法设置建造者的成员变量值。 build() 方法负责真正创建对象并对成员变量做校验工作。ResourcePoolConfig 不提供任何 setter 方法(setter方法都放到了建造者类中),这样我们创建出来的ResourcePoolConfig对象就是不可变对象了。
<?php
class ResourcePoolConfig {
const DEFAULT_MAX_TOTAL = 8;
const DEFAULT_MAX_IDLE = 8;
const DEFAULT_MIN_IDLE = 0;
private $name;
private $maxTotal = self::DEFAULT_MAX_TOTAL;
private $maxIdle = self::DEFAULT_MAX_IDLE;
private $minIdle = self::DEFAULT_MIN_IDLE;
public function __construct(PoolConfigBuilder $builder) {
$this->name = $builder->name;
$this->maxTotal = $builder->maxTotal;
$this->maxIdle = $builder->maxIdle;
$this->minIdle = $builder->minIdle;
}
//...省略getter方法...
}
class PoolConfigBuilder{
public $maxTotal;
public $maxIdle;
public $minIdle;
public $name;
public function build(){
// 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
if (empty($this->name)) {
throw new \Exception('...');
}
if ($this->maxIdle > $this->maxTotal) {
throw new \Exception('...');
}
if ($this->minIdle > $this->maxTotal || $this->minIdle > $this->maxIdle) {
throw new \Exception('...');
}
return new ResourcePoolConfig($this);
}
public function setName($name){
$this->name = $name;
return $this; // 返回 $this 是为了链式操作,好看些,不返回也可以
}
public function setMaxTotal($maxTotal){
$this->maxTotal = $maxTotal;
return $this;
}
// 省略调剩下两个setter方法
}
// 在上层调用中这样用
$builder = new PoolConfigBuilder();
$poolConfig = $builder->setName('test')
->setMaxTotal(20)
->setMaxIdle(20)
->setMinIdle(30)
->build();
使用建造者模式创建对象,还能避免对象因为漏设置成员变量或漏了调用校验逻辑而存在无效状态(这也是在对象中采用setter方法的缺陷,无法对属性做合法校验,对象容易形成无效状态)。
举个例子,一个长方形类,你只调用了 setWidth() 方法设置了长方形的宽,但是没有调用 setHeight() 方法设置长方形的高,此时这个长方形对象就是一个无效状态的对象,调用 getSquare() 方法求面积肯定求不到。用建造者模式一次性设置好对象所有该设置的属性以及初始化逻辑,就能保证得到的是一个有效状态的对象。
看完这个例子,我们就知道建造者的使用场景了:
当一个对象的实例化逻辑比较复杂,初始化逻辑需要根据场景变化而变化,且构建逻辑有必要从对象本身分离处理的情况下就可以使用到建造者模式。
如果不同业务场景,对对象初始化设置的成员变量不同,或者校验的逻辑不同,那么我们可以创建一个抽象建造者,并对建造者进行多态,在不同的建造者中实现不同的检验逻辑和设置成员变量逻辑。
三、建造者模式中的角色
下面我们在看看在书籍和网上能常搜到的对建造者模式的解释。
《设计模式之美》一书中写道:当我们需要实列化一个复杂的类,以得到不同结构类型和不同的内部状态的对象时,我们可以用不同的类对它们的实列化操作逻辑分别进行封装,这些类我们就称之为建造者。
建造者模式分成4个角色:
Product(产品类) : 我们具体需要生成的对象,对应上面例子中的 ResourcePoolConfig;
Builder(抽象建造者类) 和 ConcreteBuilder(具体建造者类):建造者类负责创建我们要生成的类对象。如果产品类只有一种创建场景,那么不需要抽象建造者,只需要具体建造者即可。但如果产品类会有多种不同状态,就需要抽象建造者作为多个具体建造者类的父类和模范。对应上面例子中的 PoolConfigBuilder;
Director(导演类):导演类作为建造者的上层调用。对应上面例子注释中提到的上层调用。实际上我认为在对象创建流程不复杂的情况下可以不需要导演类,而直接在更上层调用者(如业务逻辑)中直接操作 Builder 对象。
它们的关系如下所示:
四、建造者模式的优缺点
优点是做到了对象创建与使用的分离和解耦。
缺点是为创建不同场景下的对象,要写多个Builder类,类多了代码维护性就降低了。而且产品类中有的属性,建造者类里面也需要有,代码会有重复。
五、建造者模式和工厂模式区别
建造者模式和工厂模式都用来创建对象,其主要区别如下:
1、建造者模式更加注重方法(属性设置和校验)的调用顺序,工厂模式注重于创建完整对象。
2、建造者模式根据不同的setter方法和调用顺序可以创造出不同状态的产品对象,而工厂模式创建出来的产品对象只有一个状态的。
3、建造者模式使用者(上层调用)需要知道这个产品对象有哪些属性组成,要怎么调用建造者给对象设置哪些属性,而工厂模式的使用者不需要知道,直接调工厂类预设的创建逻辑创建就行。