Bu yazı Design Patterns/Tasarım Desenleri nedir? başlıklı yazı dizisinin bir parçasıdır.
Bu içerik ağırlıklı olarak refactoring.guru sitesindeki içeriğin tercümesi ve derlenmesinden oluşturulmuştur.
Tüm tasarım desenleri ya da diğer adıyla tasarım kalıplarına yönelik ayrıntılı içeriklere yazının sonundaki bağlantılardan ulaşabilirsiniz.
Builder Deseninin Amacı
Builder karmaşık nesnelerin adım adım oluşturulduğu yaratımsal bir tasarım desenidir. Bu desen aynı kurucu kod ile farklı tür ve gösterimdeki nesneleri oluşturmanıza olanak sağlar. Türkçe karlılığı kurucu, inşaatçı vb. tanımlardır.
Sorun
Adım adım ve zahmetli bir şekilde oluşturulması gereken bir çok alan ve iç içe gemiş nesnelerden oluşan karmaşık bir nesne hayal edin. Bu tür başlatma kodları genellikle çok sayıda parametre bekleyen devasa bir kurucunun için gömülür. Hatta daha da kötüsü böyle bir kurucu olmadan istemci kodun içerisine dağılmış olur.
Örneğin evlerinizi tanımlayan House
nesnesini nasıl tanımlayacağınızı düşünelim. Basit bir ev oluşturmak için dört duvar ve zemini oluşturmanız, bir kapı ve bir kaç pencere takmanız ve çatısını inşa etmeniz gerekir. Peki daha aydınlık, büyük, arkasında bir bahçe ve diğer gereksinimleri (ısıtma sistemi, tesisat, elektrik kablolama vb.) olan bir eve ihtiyacınız varsa?
En basit çözüm temel House
nesnenizi genişletmek ve bütün bu parametreler için gereken alt sınıfları oluşturmaktır. Fakat bunu yaptığınızda çok ciddi miktarda alt sınıfla karşılaşırsınız. Ekleyeceğiniz her yeni özellik (örneğin verandanın stili) bu hierarşiyi daha da büyütecektir.
Bir başka yaklaşım House
sınıfınız içerisinde bir ev nesnesini oluşturan tüm olası paremetreleri dikkate alan devasa bir kurucu oluşturarak alt sınıfların sayısını arttırmayı önlemektir. Bu yaklaşım alt sınıflar oluşturmayı engellese de bir başka problemi beraberinde getirir.
Çoğu zaman parametrelerin tamamı kullanılmaz ve kurucu fonksiyon çağrları çok çirkin görünür. Örneğin sadece bazı evlerde yüzme havuzu vardır ve havuzla ilgili parametreler 10 evin 9’unda gereksiz olacaktır.
Çözüm
Builder deseni nesnenin kurucu kodunu kendine has bir sınıfa dönüştürerek, builder olarak adlandırılan bağımsız nesneleri taşımanızı önerir.
Bu desenle nesne kurulum süreci bir takım adımlara (duvar örme, kapıyı monte etme vs. gibi) bölünüyor. Bir nesne oluşturmak için builder nesnesinde çeşitli adımları artarda çağırıyoruz. Buradaki en önemli husus yukarıdaki yaklaşımların aksine tüm adımları çağırmak zorunda olmadan sadece ihtiyacımız olan nesne için gereken adımları çalıştırıyoruz.
Öte yandan bazı kurulum aşamalarının oluşturulan ürüne göre değişiklik göstermesi gerekebilir. Örneğin bir kulübenin duvarları ahşaptan yapılırken, bir sarayın duvarları büyük taşlardan oluşur.
Bu durumda aynı yapım aşamalarını farklı bir şekilde oluşturaraj özel builder sınıfları oluşturabilirsniz. Böylece farklı nesneler oluşturmak için farklı builder’lar kullanabilirsiniz.
Örneğin her şeyi ahşap ve camdan imal eden bir builder hayal edin, bir diğeri ise her şeyi taş ve demirden, üçüncü bir başkası ise her şeyi altın ve elmastan imal ediyor olsun. Her 3 builder’da aynı aşamaları çalıştırıp, birinden standart bir ev, birinden küçük bir kale, sonuncusundan ise bir saray ele edebilirsiniz. Fakat builder’lara bu adımları gerçekleştirmelerini söyleyecek istemci, onlarla standart bir arayüz üzerinden etkileşime girebilmelidir.
Director
Bu konsepti bir aşama ileriye götürüp bir ürün oluşturmak için bir dizi builder adımlarını çalıştıran ve ‘director’ olarak tanımlanan ayrı bir sınıf oluşturabilirsiniz. Builder oluşturma aşamalarını nasıl gerçekleştireceğini bilirken, director bu aşamaların hangilerini ve hangi sırada çağıracağını bilir.
Programınızda bir director sınıfı bulunması kesin bir gereklilik değildir. Kurulum aşamalarını doğrudan istemci kod içerisinden de çağırabilirsiniz. Fakat director sınıfı belirli oluşturma rutinlerini bir arada toplamak ve programınızda tekrar tekrar kullanabilmek için faydalı olabilir.
Ayrıca director sınıfı ürün oluşturma sürecini istemci koddan tamamen saklar. İstemci sadece bir builder ile bir director’u eşleştirip directorun kurulum fonksiyonunu çağırır ve builder’dan sonucu bekler.
Uygulanabilirlik
Teleskopik desenden kurtulmak için builder deseni
10 farklı parametre gerektiren bir kurucu sınıfınız olduğunu düşünün. Böyle geniş bir çağrı yapmak çok da kolay değildir ve kurucu (constructor) metodu ezerek daha az parametre gerektiren daha küçük sürümlerini oluşturmak zorunda kalırsınız. Bu kurucu fonksiyonlar hala asıl fonksiyona bağlıdır ve kendilerine gönderilmeyen parametrelerin yerine ana fonksiyona bazı varsayılan değerler gönderirler. Bu kurulum desenine teleskopik kurulum deseni denir. Parametre sayısı arttıkça bu parametreleri ve özellikle de sıralamalarını hatırlamak ve hang durumda hangi kurucu şeklini kullanacağınızı standardize etmek zor olabilir.
class Pizza {
Pizza(int size) { ... }
Pizza(int size, boolean cheese) { ... }
Pizza(int size, boolean cheese, boolean pepperoni) { ... }
// ...
// Bu ezme yöntemi sadece bazı programlaa dillerinde kullanılabilir.
Builder deseni sadece ihtiyacınız olanları çağırarak nesneleri adım adım oluşturmanıza olanak sağlar. Bu deseni kodunuza entegre ettikten sonra kurucu fonksiyonlara onlarca parametre göndermek zorunda kalmazsınız.
Kodunuzun bir ürünün farklı türlerini oluşturabilmesini istiyorsanız Builder desenini kullanabilirsiniz.
Builder deseni oluşturmak için benzer adımlar izlenen farklı ürünlerin oluşturulması için kullanılır. Temel buidler arayüzü bir ürün oluşturmak için kullanılabilecek tüm adımları tanımlarken, director bunların çalıştırılma sırasını belirler.
Builder desenini kompozit yol ağaçları veya kompleks nesneler oluştururken kullanabilirsiniz.
Builder deseni ürünleri adım adım oluşturmanıza olanak sağlar. Nihai üründe sıkıntıya yol açmadan bazı adımların çalıştırılmasını erteleyebilirsiniz. Hatta adımları kendini tekrar edecek şekilde çağırabilirsiniz, bu özellik bir nesne ağacı oluşturacağınız durumlarda çok faydalı olacaktır.
Builder’lar oluşturma adımları devam ederken tamamlanmamış ürünü sunmazlar. Böylece istemci kodun eksik bir sonuç
Diğer tasarım kalıpları ile ilişkisi
- Bir çok tasarım Factory Method kullanılarak başlar (çok karmaşık olmadığı ve alt sınıflarla özelleştirilebildiği için), bunlardan bazıları Builder’a evrimleşebilir.
- Builder deseni kompleks nesnelerin adım adım oluşturulmasını sağlar. Abstract Factory ilgili nesne aileleri oluşturmak için idealdir. Abstract Factory ürünü hemen döndürürken Builder ürünü getirmeden önce bazı ön oluşturma aşamaları çalıştırmanıza izin verir.
- Tekrarlı (recursive) çalışmaya olanağ saladığı için karmaşık kompozit ağaçlar oluştururken builder kullanabilirsiniz.
- Builder’ları Singleton olarak uygulayabilirsiniz.
Builder Tasarım Deseni Kod Örnekleri
Örnek PHP Kodu
<?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);
Örnek Python Kodu
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any
class Builder(ABC):
"""
The Builder interface specifies methods for creating the different parts of
the Product objects.
"""
@property
@abstractmethod
def product(self) -> None:
pass
@abstractmethod
def produce_part_a(self) -> None:
pass
@abstractmethod
def produce_part_b(self) -> None:
pass
@abstractmethod
def produce_part_c(self) -> None:
pass
class ConcreteBuilder1(Builder):
"""
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.
"""
def __init__(self) -> None:
"""
A fresh builder instance should contain a blank product object, which is
used in further assembly.
"""
self.reset()
def reset(self) -> None:
self._product = Product1()
@property
def product(self) -> Product1:
"""
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).
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.
"""
product = self._product
self.reset()
return product
def produce_part_a(self) -> None:
self._product.add("PartA1")
def produce_part_b(self) -> None:
self._product.add("PartB1")
def produce_part_c(self) -> None:
self._product.add("PartC1")
class Product1():
"""
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.
"""
def __init__(self) -> None:
self.parts = []
def add(self, part: Any) -> None:
self.parts.append(part)
def list_parts(self) -> None:
print(f"Product parts: {', '.join(self.parts)}", end="")
class Director:
"""
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.
"""
def __init__(self) -> None:
self._builder = None
@property
def builder(self) -> Builder:
return self._builder
@builder.setter
def builder(self, builder: Builder) -> None:
"""
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.
"""
self._builder = builder
"""
The Director can construct several product variations using the same
building steps.
"""
def build_minimal_viable_product(self) -> None:
self.builder.produce_part_a()
def build_full_featured_product(self) -> None:
self.builder.produce_part_a()
self.builder.produce_part_b()
self.builder.produce_part_c()
if __name__ == "__main__":
"""
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.
"""
director = Director()
builder = ConcreteBuilder1()
director.builder = builder
print("Standard basic product: ")
director.build_minimal_viable_product()
builder.product.list_parts()
print("\n")
print("Standard full featured product: ")
director.build_full_featured_product()
builder.product.list_parts()
print("\n")
# Remember, the Builder pattern can be used without a Director class.
print("Custom product: ")
builder.produce_part_a()
builder.produce_part_b()
builder.product.list_parts()