建造者(Builder)模式是创建型设计模式的一种。也叫生成器模式。建造者模式可以将一个产品的内部表象与产品的生成过程分割开来。从而可以使一个建造过程生成具有不同的内部表象的产品对象。如果我们使用了建造者模式,那么用户就只需指定需要建造的类型就可以得到他们而具体建造的过程和细节就不需要知道了。

问题

例如, 我们现在要创建一个 房屋House对象。 建造一栋简单的房屋, 首先你需要建造四面墙和地板, 安装房门和一套窗户, 然后再建造一个屋顶。 但是如果你想要一栋更宽敞更明亮的房屋, 还要有院子和其他设施(例如暖气、 排水和供电设备), 那又该怎么办呢?
最简单的方法是扩展 房屋基类, 然后创建一系列涵盖所有参数组合的子类。 但最终你将面对相当数量的子类。 任何新增的参数 (例如门廊类型) 都会让这个层次结构更加复杂。
另一种方法则无需生成子类。 你可以在 房屋基类中创建一个包括所有可能参数的超级构造函数, 并用它来控制房屋对象。 这种方法确实可以避免生成子类, 但它却会造成另外一个问题。就是在大多数情况下有些参数我们不一定会用得上。例如,只有很少的房子有游泳池,因此游泳池类的参数十之八九是毫无用处的。

解决方案

这个时候我们就可使用建造者模式,使用建造者模式将对象构造代码从产品类中抽取出来, 并将其放在一个名为建造者的独立对象中。

该模式会将对象构造过程划分为一组步骤, 比如 buildWalls创建墙壁和 buildDoor创建房门创建房门等。 每次创建对象时, 你都需要通过建造者对象执行一系列步骤。 重点在于你无需调用所有步骤, 而只需调用创建特定对象配置所需的那些步骤即可。
当你需要创建不同形式的产品时, 其中的一些构造步骤可能需要不同的实现。 例如, 木屋的房门可能需要使用木头制造, 而城堡的房门则必须使用石头制造。
在这种情况下, 你可以创建多个不同的建造者, 用不同方式实现一组相同的创建步骤。 然后你就可以在创建过程中使用这些建造者 (例如按顺序调用多个构造步骤) 来生成不同类型的对象

例如, 假设第一个建造者使用木头和玻璃制造房屋, 第二个建造者使用石头和钢铁, 而第三个建造者使用黄金和钻石。 在调用同一组步骤后, 第一个建造者会给你一栋普通房屋, 第二个会给你一座小城堡, 而第三个则会给你一座宫殿。 但是, 只有在调用构造步骤的客户端代码可以通过通用接口与建造者进行交互时, 这样的调用才能返回需要的房屋。

主管

你可以进一步将用于创建产品的一系列建造步骤调用抽取成为单独的主管类。 主管类可定义创建步骤的执行顺序, 而建造者则提供这些步骤的实现。

严格来说, 你的程序中并不一定需要主管类。 客户端代码可直接以特定顺序调用创建步骤。 不过, 主管类中非常适合放入各种例行构造流程, 以便在程序中反复使用。此外, 对于客户端代码来说, 主管类完全隐藏了产品构造细节。 客户端只需要将一个建造者与主管类关联, 然后使用主管类来构造产品, 就能从建造者处获得构造结果了。

建造者模式的结构


建造者模式主要由以下几个角色组成:

  1. 抽象建造者 (Builder) 接口或抽象声明在所有类型建造者中通用的产品构造步骤。
  2. 具体建造者 (Concrete Builders) 提供构造过程的不同实现。 具体建造者也可以构造不遵循通用接口的产品。
  3. 产品 (Products) 是最终生成的对象。 由不同建造者构造的产品无需属于同一类层次结构或接口
  4. 主管 (Director) 类定义调用构造步骤的顺序, 这样你就可以创建和复用特定的产品配置。

代码示例:
抽象建造者角色 Builder.php

<?php
namespace DesignPatterns\Builder\House;

/**
 * 抽象建造者角色
 *
 * Interface Builder
 * @package DesignPatterns\Builder\House
 */

interface Builder
{
    /**
     * 创建墙壁
     * @return mixed
     */
    public function buildWalls();

    /**
     * 创建房门
     * @return mixed
     */
    public function buildDoors();

    /**
     * 创建窗户
     * @return mixed
     */
    public function buildWindows();

    /**
     * 创建房顶
     * @return mixed
     */
    public function buildRoof();

    /**
     * 创建车库
     * @return mixed
     */
    public function buildGarage();

}

具体建造者 HouseBuilder.php

<?php
namespace DesignPatterns\Builder\House;

/**
 * 具体建造者角色
 * 普通房屋
 * Class HouseBuilder
 * @package DesignPatterns\Builder\House
 */
class HouseBuilder implements Builder{
    private $house;

    /**
     * 新的建造者实例应包含一个空的产品对象,即用于进一步组装。
     */
    public function __construct()
    {
        $this->reset();
    }
    public function reset():void
    {
        $this->house = new House;
    }

    /**
     * @return mixed|void
     */
    public function buildWalls():void
    {
        $this->house->parts[] = "创建墙壁";
    }

    /**
     * @return mixed|void
     */
    public function buildDoors()
    {
        $this->house->parts[] = "创建房门";
    }

    /**
     * @return mixed|void
     */
    public function buildWindows()
    {
        $this->house->parts[] = "创建窗户";
    }

    /**
     * @return mixed|void
     */
    public function buildRoof()
    {
        $this->house->parts[] = "创建房顶";
    }

    /**
     * @return mixed|void
     */
    public function buildGarage()
    {
        $this->house->parts[] = "创建车库";
    }

    /**
     * @return mixed
     */
    public function getHouse(){
        $result = $this->house;
        $this->reset();
        return $result;
    }
}

产品 House.php

<?php
namespace DesignPatterns\Builder\House;
/**
 * 产品类
 * Class House
 * @package DesignPatterns\Builder\House
 */
class House{
    public $parts = [];

    public function listParts(): void
    {
        echo "Product parts: " . implode(', ', $this->parts) . "\n\n";
    }
}

主管 Director.php

<?php
namespace DesignPatterns\Builder\House;
/**
 * 主管 (Director) 类
 * 定义调用构造步骤的顺序,你可以创建和复用特定的产品配置。
 * Class Director
 * @package DesignPatterns\Builder\House
 */
class Director
{
    private $builder;

    /**
     * @param mixed $builder
     */
    public function setBuilder(Builder $builder): void
    {
        $this->builder = $builder;
    }
    public function buildHouse()
    {
        $this->builder->buildWalls();
        $this->builder->buildDoors();
        $this->builder->buildWindows();
        $this->builder->buildRoof();
        $this->builder->buildGarage();
    }
}

运行 Client.php

<?php
namespace DesignPatterns\Builder\House;
require dirname(__DIR__).'/../vendor/autoload.php';
//client
class Client{
    /**
     * @param Director $director
     */
    public function run(Director $director){
        //示例一 使用将建造步骤托管至主管
        $builder = new HouseBuilder();
        $director->setBuilder($builder);
        $director->buildHouse();
        $builder->getHouse()->listParts();
        //示例二 建造模式可以不依赖主管director类使用
        echo "<br>";
        $builder->buildWalls();
        $builder->buildDoors();
        $builder->buildWindows();
        $builder->buildRoof();
        $builder->getHouse()->listParts();

    }
}

$client = new Client();
$client->run(new Director());

执行结果:

建造者模式优缺点

优点:

  • 你可以分步创建对象, 暂缓创建步骤或递归运行创建步骤。
  • 生成不同形式的产品时, 你可以复用相同的制造代码。
  • 单一职责原则。 你可以将复杂构造代码从产品的业务逻辑中分离出来。

缺点:
由于该模式需要新增多个类, 因此代码整体复杂程度会有所增加。随着产品的增加,我们会增加产品类,相应的具体建造者类,以及相应的建造步骤等。

建造者模式适合的应用场景

  • 使用建造者模式可避免 “重叠构造函数 (telescopic constructor)” 的出现。
    假设你的构造函数中有十个可选参数, 那么调用该函数会非常不方便; 因此, 你需要重载这个构造函数, 新建几个只有较少参数的简化版。 但这些构造函数仍需调用主构造函数, 传递一些默认数值来替代省略掉的参数。
class Pizza {
    Pizza(int size) { ... }
    Pizza(int size, boolean cheese) { ... }
    Pizza(int size, boolean cheese, boolean pepperoni) { ... }
    // ...

只有在 C# 或 Java 等支持方法重载的编程语言中才能写出如此复杂的构造函数。
建造者模式让你可以分步骤生成对象, 而且允许你仅使用必须的步骤。 应用该模式后, 你再也不需要将几十个参数塞进构造函数里了。

  • 当你希望使用代码创建不同形式的产品 (例如石头或木头房屋) 时, 可使用建造者模式。
    如果你需要创建的各种形式的产品, 它们的制造过程相似且仅有细节上的差异, 此时可使用建造者模式。

基本建造者接口中定义了所有可能的制造步骤, 具体建造者将实现这些步骤来制造特定形式的产品。 同时, 主管类将负责管理制造步骤的顺序。

  • 使用建造者构造组合树或其他复杂对象。
    建造者模式让你能分步骤构造产品。 你可以延迟执行某些步骤而不会影响最终产品。 你甚至可以递归调用这些步骤, 这在创建对象树时非常方便。建造者在执行制造步骤时, 不能对外发布未完成的产品。 这可以避免客户端代码获取到不完整结果对象的情况。

建造者模式在PHP中的使用:

建造者模式在PHP最常用的就是sql查询构造器。建造者接口定义了生成一般 SQL 查询所需的通用步骤。 另一方面, 对应不同 SQL 语言的具体建造者会去实现这些步骤, 返回能在特定数据库引擎中执行的 SQL 查询语句。像thinkphp,laravel等PHP框架在sql查询构造器上都使用的是建造者模式。感兴趣的可以看一下。
下面是一个简单的sql查询构造器示例。

SqlQueryBuilder.php 抽象建造者

<?php
namespace DesignPatterns\Builder\php;

/**
 * Builder接口声明一组方法来组装SQL查询。
 * 所有的构造步骤都返回当前的builder对象以允许链接:$builder->select(…)->where(…)
 * Interface SqlQueryBuilder
 * @package DesignPatterns\Builder\php
 */
interface SqlQueryBuilder
{
    /**
     * select
     * @param string $table
     * @param array $fields
     * @return SqlQueryBuilder
     */
    public function select(string $table ,array $fields):SqlqueryBuilder;

    /**
     * where
     * @param string $field
     * @param string $value
     * @param string $operator
     * @return SqlQueryBuilder
     */
    public function where(string $field,  string $operator = '=', string $value): SQLQueryBuilder;

    /**
     * limit
     * @param int $start
     * @param int $offset
     * @return SqlQueryBuilder
     */
    public function limit(int $start, int $offset): SQLQueryBuilder;


    /**
     * @return string
     */
    public function getSQL(): string;
}

MysqlQueryBuilder.php 具体建造者 mysql

<?php
namespace DesignPatterns\Builder\php;

class MysqlQueryBuilder implements SqlQueryBuilder
{
    protected $query;
    public function reset():void
    {
        $this->query = new \stdClass();
    }

    public function select(string $table, array $fields): SqlqueryBuilder
    {
        $this->reset();
        $this->query->base = "SELECT " . implode(", ", $fields) . " FROM " . $table;
        $this->query->type = 'select';

        return $this;
    }

    public function where(string $field,  string $operator = '=', string $value): SQLQueryBuilder
    {
        if (!in_array($this->query->type, ['select', 'update', 'delete'])) {
            throw new \Exception("WHERE can only be added to SELECT, UPDATE OR DELETE");
        }
        $this->query->where[] = "$field $operator '$value'";

        return $this;
    }

    public function limit(int $start, int $offset): SQLQueryBuilder
    {
        if (!in_array($this->query->type, ['select'])) {
            throw new \Exception("LIMIT can only be added to SELECT");
        }
        $this->query->limit = " LIMIT " . $start . ", " . $offset;

        return $this;
    }

    public function getSQL(): string
    {
        $query = $this->query;
        $sql = $query->base;
        if (!empty($query->where)) {
            $sql .= " WHERE " . implode(' AND ', $query->where);
        }
        if (isset($query->limit)) {
            $sql .= $query->limit;
        }
        $sql .= ";";
        return $sql;
    }
}

运行 Client.php

<?php
namespace DesignPatterns\Builder\php;
require dirname(__DIR__).'/../vendor/autoload.php';
/**
 * 客户端调用
 * Class Client
 * @package DesignPatterns\Builder\php
 */
class Client{
    public function run(SqlQueryBuilder $sqlQueryBuilder)
    {
        $query = $sqlQueryBuilder->select('user',['name','email','password'])
                ->where('age','>','18')
                ->where('age','<','30')
                ->limit(10,20)
                ->getSQL();
        return $query;

    }
}
$Client = new Client();
echo $Client->run(new MysqlQueryBuilder());

执行结果:

建造者模式与工厂模式有何区别?

建造者模式是让建造者类来负责对象的创建工作。工厂模式,是由工厂类来负责对象创建的工作。那它们之间有什么区别呢?
实际上,工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。
建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象。网上有一个经典的例子很好地解释了两者的区别。顾客走进一家餐馆点餐,我们利用工厂模式,根据用户不同的选择,来制作不同的食物,比如披萨、汉堡、沙拉。对于披萨来说,用户又有各种配料可以定制,比如奶酪、西红柿、起司,我们通过建造者模式根据用户选择的不同配料来制作披萨。
实际上,我们也不要太学院派,非得把工厂模式、建造者模式分得那么清楚,我们需要知道的是,每个模式为什么这么设计,能解决什么问题。只有了解了这些最本质的东西,我们才能不生搬硬套,才能灵活应用,甚至可以混用各种模式创造出新的模式,来解决特定场景的问题。

Last modification:June 14th, 2020 at 02:58 pm