
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;