개념적인 예시
이 예시는 빌더 디자인 패턴의 구조를 보여주고 다음 질문에 중점을 둡니다:
패턴은 어떤 클래스들로 구성되어 있나요?
이 클래스들은 어떤 역할을 하나요?
패턴의 요소들은 어떻게 서로 연관되어 있나요?
이 패턴의 구조를 배우면 실제 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;