Skip to content

Builder Pattern

Builder pattern veya builder method isimlendirmeleriyle kaynaklarlarda rastlayabileceğiniz konsept creational/yaratımsal bir tasarım kalıbıdır. Uygulamalarımızı geliştirirken zaman zaman ihtiyaçlar değişir ve uygulamanın buna ayak uydurması gerekir, bu durumunda complextyi artıracağı bir gerçektir. __init__ metodunda 10 farklı parametre alan bir class düşünün bu classı bir yerlerde çağırıp instance’ını almak biraz can sıkıcı olacaktır. Builder patternin çözüm sunduğu noktalardan birisi budur, bir nesneyi inşa etme işini builder nesnesine devreder, bu sayede sınıf özellikleri gruplanarak instance’ın oluşturulabilmesi daha okunabilir ve temiz şekilde sağlanır. Bir örnek üzerinden gidecek olursak;

İsim, doğum tarihi, pozisyon değerleriyle initialize edilen bir Employee classımız olsun.

class Employee:
    def __init__(self) -> None:
        self.name = None
        self.position = None
        self.date_of_birth = None

    def __str__(self) -> str:
        return f'{self.name} named employee, birth at {self.date_of_birth} and working as {self.position}'

Employee sınıfını initialize ederken name, position ve date_of_birth değişkenlerinin değerlerini None olarak atadık ve bu classtan bir instance oluşturmaya çalıştığımızda __init__ metodundanda bu değişkenlerin değerini almadık, ancak classımızı print ettiğimizde çalışacak __str__ methodu içinde bu değişkenleri kullanıyoruz, bu değişkenleri bir şekilde set etmemiz gerekecek işte tamda bu noktada devreye builder design el atıyor olacak.

class EmployeeBuilder:
    def __init__(self, employee=Employee()) -> None:
        self.employee = employee

    def build(self):
        return self.employee

EmployeeBuilder sınıfı initialize edilirken bir employee objesi alıyor ve build methodu çağırıldığında instance’ı geri dönüyor. İnitialization(ilk sırada çalışacak) ve build(son sırada çalışacak) methodlarının çalışmaları arasında istediğimiz özellikleri Employee instanceımıza ekliyor olacağız.

class EmployeeInfoBuilder(EmployeeBuilder):
    def called(self, name):
        self.employee.name = name
        return self

class EmployeePositionBuilder(EmployeeInfoBuilder):
    def work_as_a(self, position):
        self.employee.position = position
        return self

class EmployeeBirthDateBuilder(EmployeePositionBuilder):
    def born(self, date_of_birth):
        self.employee.date_of_birth = date_of_birth
        return self

Dikkat edeceğimiz nokta builder sınıfılarını özelleştirerek, istediğimiz özellikleri ayırdığımız bir yapı oluşturduk, yani çalışana isim eklemek istediğimde EmployeeInfoBuilder, pozisyon eklemek istediğimde ise EmployeePositionBuilder sınıfları üzerindeki metodlara gerekli parametreleri paslayarak instance’ımıza set etmiş oluyoruz.

employee_builder = EmployeeBirthDateBuilder()
me = employee_builder\
    .called('Kadir')\
    .work_as_a('Developer')\
    .born('1990-01-01')\
    .build()


print(me)

----------------------------------------------------------------------
Output:
Kadir named employee, birth at 1990-01-01 and working as Developer

PersonBirthDateBuilder üzerinden tüm bu build işleminin yürüdüğünü görüyoruz, burda hiyerarşik ve sıralı bir yapı olduğunu gözden kaçırmamakta yarar var en fazla özelleştirdiğimiz sınıf PersonBirthDateBuilder ve bu sınıf InfoBuilder ve PositionBuilder’e ait özellikleride içeriyor, bu yüzden builder instance’ını BirthDateBuilder sınıfımız üzerinden oluşturuyoruz. date_of_birth değişkenini set etmek amacımız olsaydı ve builder instance’ını PositionBuilder üzerinden oluştursaydık, date_of_birth değişkenini set eden born() metodunu kullanamayacaktık.

Örneği biraz daha geliştirelim ve bir e-ticaret uygulamasında elektronik ve yiyecek ürünleri sattığımızı düşünelim, elektronik ürünler ve yiyecek ürünleri arasında şu farklar mevcut olsun:

  • Elektronik ürünlerin vergisi %18’dir.
  • Elektronik ürünlerde belirlenen indirim tutarına ek 5 birim ekstra indirim yapılmaktadır.
  • Elektronik ürünlerin toplam stoğu her zaman 1 ürün eksik olacak şekilde gösterilmelidir.
  • Yiyecek ürünlerinin vergisi %8’dir.
  • Yiyecek ürünleri belirlenen net fiyat üzerinden 5 birim indirimli şekilde satışa sunulmaktadır.
  • Yiyecek ürünlerinde stok miktarının bir önemi yoktur.

bu kurallar doğrultusunda builder design ile geliştirme yaptığımızda kodumuz şöyle olacaktır.

from abc import ABC, abstractmethod

class Product(object):
    def __init__(self) -> None:
        self.name = None
        self.net_price = None
        self.total_price = None
        self.discount = None
        self.stock = None
        

class ProductBuilder(ABC):
    
    @property
    @abstractmethod
    def product(self) -> Product:
        pass
    
    @abstractmethod
    def set_name(self, name:str) -> None:
        pass
    
    @abstractmethod
    def add_net_price(self, price:float) -> None:
        pass
    
    @abstractmethod
    def add_stock(self, stock:int) -> None:
        pass
    
    @abstractmethod
    def add_discount_amount(self, discount:float) -> None:
        pass
    
    @abstractmethod
    def apply_discount(self) -> float:
        pass
    

class ConcreteProductBuilder(ProductBuilder):
    
    def __init__(self) -> None:
        self._product = Product()
    
    @property
    def product(self) -> None:
        return self._product
    
    def apply_discount(self):
        self.product.total_price = self.product.net_price - self.product.discount * (1+self.tax_ratio)


class FoodProductBuilder(ConcreteProductBuilder):
    
    def set_name(self, name):
        self.product.name = name
        return self
    
    def add_net_price(self, net_price):
        self.product.net_price = net_price - 5
        return self
    
    def add_discount_amount(self, discount):
        self.product.discount = discount
        return self
    
    def add_stock(self, stock):
        self.product.stock = stock
        return self
    

class ElectronicProductBuilder(ConcreteProductBuilder):
    
    def set_name(self, name):
        self.product.name = name
        return self
    
    def add_net_price(self, net_price):
        self.product.net_price = net_price
        return self
    
    def add_discount_amount(self, discount):
        self.product.discount = discount + 5
        return self
    
    def add_stock(self, stock):
        self.product.stock = stock - 1
        return self


class ProductDirector():
    
    def __init__(self, product_builder:ProductBuilder) -> None:
        self.product_builder = product_builder
    
    def build(self):
        if isinstance(self.product_builder, FoodProductBuilder):
            self.product_builder.tax_ratio = 0.08
        else:
            self.product_builder.tax_ratio = 0.18
            
        self.product_builder.apply_discount()
        return self.product_builder.product


food_builder = FoodProductBuilder()
food_builder.set_name("Bread")\
                .add_discount_amount(0)\
                .add_net_price(10)

fpd = ProductDirector(food_builder)
print(fpd.build().__dict__)


electronic_builder = ElectronicProductBuilder()
electronic_builder.set_name("Pc")\
                    .add_net_price(1000)\
                    .add_discount_amount(50)\
                    .add_stock(500)

epd = ProductDirector(electronic_builder)
print(epd.build().__dict__)

-----------------------------------------------------------------------
Output:
{'name': 'Bread', 'net_price': 5, 'total_price': 5.0, 'discount': 0, 'stock': None}
{'name': 'Pc', 'net_price': 1000, 'total_price': 935.1, 'discount': 55, 'stock': 499}

böylelikle farklı tiplerdeki ürünleri yaratırken vergi, stok, indirim gibi hesaplamarının özelleştirilmesini ve bu ürünlerin yaratılma süreçlerinin tek bir obje üzerinden yürütülmesini sağlamış oluyoruz.


Sources:

Published inCreational Design PatternsDesign Patterns

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *