
PHP 生成器模式讲解和代码示例
生成器是一种创建型设计模式, 使你能够分步骤创建复杂对象。
与其他创建型模式不同, 生成器不要求产品拥有通用接口。 这使得用相同的创建过程生成不同的产品成为可能。
复杂度:
流行度:
使用示例: 生成器模式是 PHP 世界中的一个著名模式。 当你需要创建一个可能有许多配置选项的对象时, 该模式会特别有用。
识别方法: 生成器模式可以通过类来识别, 它拥有一个构建方法和多个配置结果对象的方法。 生成器方法通常支持方法链 (例如 someBuilder->setValueA(1)->setValueB(2)->create()
)。
概念示例
本例说明了生成器设计模式的结构并重点回答了下面的问题:
- 它由哪些类组成?
- 这些类扮演了哪些角色?
- 模式中的各个元素会以何种方式相互关联?
了解该模式的结构后, 你可以更轻松地理解下面基于真实世界的 PHP 应用案例。
index.php: 概念示例
<?php namespace RefactoringGuru\Builder\Conceptual; /** * The Builder interface specifies methods for creating the different parts of * the Product objects. */ interface Builder { public function producePartA(): void; public function producePartB(): void; public function producePartC(): void; } /** * The Concrete Builder classes follow the Builder interface and provide * specific implementations of the building steps. Your program may have several * variations of Builders, implemented differently. */ class ConcreteBuilder1 implements Builder { private $product; /** * A fresh builder instance should contain a blank product object, which is * used in further assembly. */ public function __construct() { $this->reset(); } public function reset(): void { $this->product = new Product1(); } /** * All production steps work with the same product instance. */ public function producePartA(): void { $this->product->parts[] = "PartA1"; } public function producePartB(): void { $this->product->parts[] = "PartB1"; } public function producePartC(): void { $this->product->parts[] = "PartC1"; } /** * Concrete Builders are supposed to provide their own methods for * retrieving results. That's because various types of builders may create * entirely different products that don't follow the same interface. * Therefore, such methods cannot be declared in the base Builder interface * (at least in a statically typed programming language). Note that PHP is a * dynamically typed language and this method CAN be in the base interface. * However, we won't declare it there for the sake of clarity. * * Usually, after returning the end result to the client, a builder instance * is expected to be ready to start producing another product. That's why * it's a usual practice to call the reset method at the end of the * `getProduct` method body. However, this behavior is not mandatory, and * you can make your builders wait for an explicit reset call from the * client code before disposing of the previous result. */ public function getProduct(): Product1 { $result = $this->product; $this->reset(); return $result; } } /** * It makes sense to use the Builder pattern only when your products are quite * complex and require extensive configuration. * * Unlike in other creational patterns, different concrete builders can produce * unrelated products. In other words, results of various builders may not * always follow the same interface. */ class Product1 { public $parts = []; public function listParts(): void { echo "Product parts: " . implode(', ', $this->parts) . "\n\n"; } } /** * The Director is only responsible for executing the building steps in a * particular sequence. It is helpful when producing products according to a * specific order or configuration. Strictly speaking, the Director class is * optional, since the client can control builders directly. */ class Director { /** * @var Builder */ private $builder; /** * The Director works with any builder instance that the client code passes * to it. This way, the client code may alter the final type of the newly * assembled product. */ public function setBuilder(Builder $builder): void { $this->builder = $builder; } /** * The Director can construct several product variations using the same * building steps. */ public function buildMinimalViableProduct(): void { $this->builder->producePartA(); } public function buildFullFeaturedProduct(): void { $this->builder->producePartA(); $this->builder->producePartB(); $this->builder->producePartC(); } } /** * The client code creates a builder object, passes it to the director and then * initiates the construction process. The end result is retrieved from the * builder object. */ function clientCode(Director $director) { $builder = new ConcreteBuilder1(); $director->setBuilder($builder); echo "Standard basic product:\n"; $director->buildMinimalViableProduct(); $builder->getProduct()->listParts(); echo "Standard full featured product:\n"; $director->buildFullFeaturedProduct(); $builder->getProduct()->listParts(); // Remember, the Builder pattern can be used without a Director class. echo "Custom product:\n"; $builder->producePartA(); $builder->producePartC(); $builder->getProduct()->listParts(); } $director = new Director(); clientCode($director);
Output.txt: 执行结果
Standard basic product: Product parts: PartA1 Standard full featured product: Product parts: PartA1, PartB1, PartC1 Custom product: Product parts: PartA1, PartC1
真实世界示例
生成器模式最好的应用方式之一是 SQL 查询生成器。 生成器接口定义了生成一般 SQL 查询所需的通用步骤。 另一方面, 对应不同 SQL 语言的具体生成器会去实现这些步骤, 返回能在特定数据库引擎中执行的 SQL 查询语句。
index.php: 真实世界示例
<?php namespace RefactoringGuru\Builder\RealWorld; /** * The Builder interface declares a set of methods to assemble an SQL query. * * All of the construction steps are returning the current builder object to * allow chaining: $builder->select(...)->where(...) */ interface SQLQueryBuilder { public function select(string $table, array $fields): SQLQueryBuilder; public function where(string $field, string $value, string $operator = '='): SQLQueryBuilder; public function limit(int $start, int $offset): SQLQueryBuilder; // +100 other SQL syntax methods... public function getSQL(): string; } /** * Each Concrete Builder corresponds to a specific SQL dialect and may implement * the builder steps a little bit differently from the others. * * This Concrete Builder can build SQL queries compatible with MySQL. */ class MysqlQueryBuilder implements SQLQueryBuilder { protected $query; protected function reset(): void { $this->query = new \stdClass(); } /** * Build a base SELECT query. */ public function select(string $table, array $fields): SQLQueryBuilder { $this->reset(); $this->query->base = "SELECT " . implode(", ", $fields) . " FROM " . $table; $this->query->type = 'select'; return $this; } /** * Add a WHERE condition. */ public function where(string $field, string $value, string $operator = '='): 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; } /** * Add a LIMIT constraint. */ 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; } /** * Get the final query string. */ 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; } } /** * This Concrete Builder is compatible with PostgreSQL. While Postgres is very * similar to Mysql, it still has several differences. To reuse the common code, * we extend it from the MySQL builder, while overriding some of the building * steps. */ class PostgresQueryBuilder extends MysqlQueryBuilder { /** * Among other things, PostgreSQL has slightly different LIMIT syntax. */ public function limit(int $start, int $offset): SQLQueryBuilder { parent::limit($start, $offset); $this->query->limit = " LIMIT " . $start . " OFFSET " . $offset; return $this; } // + tons of other overrides... } /** * Note that the client code uses the builder object directly. A designated * Director class is not necessary in this case, because the client code needs * different queries almost every time, so the sequence of the construction * steps cannot be easily reused. * * Since all our query builders create products of the same type (which is a * string), we can interact with all builders using their common interface. * Later, if we implement a new Builder class, we will be able to pass its * instance to the existing client code without breaking it thanks to the * SQLQueryBuilder interface. */ function clientCode(SQLQueryBuilder $queryBuilder) { // ... $query = $queryBuilder ->select("users", ["name", "email", "password"]) ->where("age", 18, ">") ->where("age", 30, "<") ->limit(10, 20) ->getSQL(); echo $query; // ... } /** * The application selects the proper query builder type depending on a current * configuration or the environment settings. */ // if ($_ENV['database_type'] == 'postgres') { // $builder = new PostgresQueryBuilder(); } else { // $builder = new MysqlQueryBuilder(); } // // clientCode($builder); echo "Testing MySQL query builder:\n"; clientCode(new MysqlQueryBuilder()); echo "\n\n"; echo "Testing PostgresSQL query builder:\n"; clientCode(new PostgresQueryBuilder());
Output.txt: 执行结果
Testing MySQL query builder: SELECT name, email, password FROM users WHERE age > '18' AND age < '30' LIMIT 10, 20; Testing PostgresSQL query builder: SELECT name, email, password FROM users WHERE age > '18' AND age < '30' LIMIT 10 OFFSET 20;