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.

Decorator tasarım deseninin amacı

Decorator nesneleri özel sarıcı nesneler içerisine yerleştirerek onlara yeni davranışlar kazandırmanızı sağlayan yapısal bir tasarım desenidir.

Sorun

Başka programların kullanıcılarına bildirimler göndermesini sağlayan bir bildirim kütüphanesi üzerinde çalıştığınızı düşünün. Kütüphanenin ilk sürümü bir kaç alan, bir oluşturucu (constructor) send (gönder) metodundan oluşan Notifier sınıfını esas alıyordu. Bu metod istemci koddan bir mesaj argümanı alıp bu mesajı oluşturucu metodunda kendisine verilmiş olan e-posta adreslerine gönderiyordu. Bu bildirim sınıfını kullanan üçüncü parti uygulamalar bu objenin bir örneğini oluştup bildirim göndermeleri gerektiğinde kullanıyorlardı.

Decorator tasarım deseni
Bir program notifier (bildirim) sınıfını kullanarak önceden belirlenmiş e-posta adreslerine bildirim gönderebiliyor.

Fakat bir yerden sonra kütüphanenin kullanıcılarının e-posta bildiriminden daha fazlasını istediklerini gördünüz. Bazıları kiritik durumları SMS’le bildirmek istersen, bazıları facebook, bazıları ise Slack’den bildirim gönderebilmek istiyordu.

Decorator tasarım kalıbı
Her bildirim sınıfı ana sınıfın alt sınıfı olarak uygulamaya geçirilmiş.

Bu ne kadar zor olabilir ki? Notifier sınıfını genişletir, tüm bildirim yöntemleri için alt sınıflar oluşturursunuz. Bunda sonra istemci ilgili bildirimin sınıfını oluşturur ve kullanır.

Fakat ardından gelen makul bir isteğe nasıl yanıt vereceksiniz? “Neden aynı anda birden fazla bildirim tipinde gönderi yapamıyoruz? Evin yanıyorsa mümkün olan tüm kanallardan bildirim almak istersin.”

Sorunu çözmek için özel çeşitli bildirim yöntemlerini kombinleyen alt sınıflar oluşturmayı deneyebilirsiniz. Fakat bu yöntem sadece kütüphane kodunuzu değil aynı zamanda istemci kodunuda şişrip karmaşık hale getirecektir.

Decorator design pattern
Alt sınıf kombinasyonlarının patlaması

Bu sorunu daha düzgün bir yöntemle çözmek için bildirim yapısını başka bir şekilde inşa etmelisiniz.

Çözüm

Bir nesnenin davranış şeklini değiştirmeniz gerektiğinde ilk akla gelen o sınıfı genişletmek (extend) oluyor. Fakat bunu yaparken bu inheritance (miras) yönteminin ciddi olabilecek bazı sonuçlarını farkında olmalısınız.

  • Inheritance statiktir. Mevcut bir objenin davranışını çalışma anında değiştiremezsiniz. Ancak başka bir sınıftan yeni bir nesne oluşturarak mevcut nesneyi tamamen değiştirebilirsiniz.
  • Alt sınıfların sadece bir ana sınıfı olabilir. Bir çok programlama dili bir sınıfın aynı anda birden fazla sınıftan miras almasına (inheritance) izin vermez.

Bu sorunu aşmanın bir yolu Inheritance yerine Aggregation veya Composition kullanmaktır. Her iki alternatifte benzer şekilde çalışır, bir nesne bir başka nesneye referans içerir ve işin bir kısmını o nesneye yaptırır, inheritance’da ise nesnenin kendisi ana sınıftan miras aldığı yöntemleri kullanarak işi kendisi yapar.

Bu yeni yaklaşımla bağlantılı yardımcı nesneyi başka bir nesne ile yer değiştirerek ana nesnenin davranış şeklini çalışma anında değiştirebilirsiniz. Bir nesne çeşitli sınıfların davranışlarını kullanabilir, birden fazla nesneye referanslar içerebilir ve bunlara her tür işi aktarabilir. Aggregation/composition, Decorator’da dahil bir çok tasarım deseninin ardında yatan temel prensiptir. Bu notu da verdikten sonra tasarım desenimizle ilgili tartışmaya geri dönelim.

Inheritance ve Aggregation
Inheritance ve Aggregation

Decorator tasarım kalbının bir diğer adı da Wrapper, yani sarıyıcıdır ve aslında bu isim desenin ana fikrini yansıtır. Sarıcı bir nesne bir hedef nesneyle ilişkilendirilir. Sarıcı, hedefle aynı metod setine sahiptir ve aldığı tüm istekleri ona delege eder. Fakat sarıcı bu isteği alıcıya göndermeden önce ya da sonra başka şeyler yaparak sonucu kısmen ya da tamamen değiştirebilir.

Basit bir sarıcı ne zaman bir decorator’e dönüşür? Sarıcı adını verdiğimiz nesne sardığı nesne ile aynı arayüzü paylaşır. Bu nedenle istemcinin bakış açısıyla bu iki nesne aynı tipte iki nesnedir. Sarıcının referans alanını da aynı arayüzde bir nesne alacak şekilde yapılandırırsak iç içe sarıcılar oluşturabiliriz. (Biraz karmaşık bir açıklama olduğunu biliyorum ama aşağıdaki örneği incelediğinizde anlaşılır olacaktır.)

Bildirim örneğimize geri dönersek, basit e-posta fonksiyonunu temel Notifier sınıfımız içerisinde bırakalım fakat diğer bütün bildirim metodlarını decoratorlere çevirelim.

Decorator deseni nerede işe yarar
Çeşitli bildirim yöntemleri decorator’e dönüştürüldü.

İstemci kodu, temel bildirim nesnemizi oluşturduktan sonra ihtiyacına göre bir dizi decoratorle sarmalıdır. Sonuçta oluşan nesneler bir yığın haline gelecektir.

decorator deseni hangi sorunu çözer
Uygulamalar kompleks bildirim decorator yığınları oluşturabilir.

Yukarıdaki kodda istemci kodu önce temel bildirim nesnenini oluşturuyor. Örneğin facebook bildirimleri yapılacaksa Facebook decorator’üne paramete olarak bu nesneyi vererek nesneyi güncelliyor, akabinde slack için aynısını yapıyor. Böylece adım adım bir önceki notifier tipini referans olarak (wrappe alanında) saklayan bir hiyerarşi oluşturmuş oluyoruz. Oluşturulan her yeni nesne, bir öncekini wrapper alanında saklamış oluyor.

Bu yığındaki son nesne istemcinin asıl çalıştığı nesne olacaktır. Tüm decorator’ler aynı ana bildirim sınıfı ile aynı arayüzü paylaştıkları için istemci kod ana sınıfla mı, yoksa decoratorlerden biriyle mi çalışıyor farketmeksizin aynı şekilde yoluna devam edebilecektir.

Uygulanabilirlik

Nesnelere istemci kodun çalışmasını bozmadan çalışma anında ekstra özellikler ekleyebilmek istiyorsanız Decorator tasarım desenini kullanabilirsiniz.

Bir nesnenin davranışını inheritance ile değiştirmek kullanışsız ve hatta imkansızsa bu tasarım kalıbı size yardımcı olabilir.

Diğer tasarım desenleri/kalıpları ile ilişkisi

  • Adapter mevcut bir nesnenin arayüzünü değiştirirken, Decorator nesnenin arayüzünü değiştirmeden yeni özellikler ekler. Ayrıca Adapter özyinelemeli (recursive) kompozisyonları desteklemezken, Decoratorle bu mümkündür.
  • Adapter sardığı nesne için farklı bir arayüz sunar, Proxy bunu aynı arayüzle yapar, Decorator ise aynı arayüzü geliştirir.
  • Chain of Responsibility (sorumluluk zinciri) ve Decorator sınıflarının yapıları çok benzerdir. Her iki desende işlemi bir dizi nesneye aktarmak için özyinelemeli kompozisyonlara ihtiyaç duysalar da bazı önemli farkları vardır;

    Chain Of Responsibility işlemleri birbirinden bağımsız olarak çalıştırabilir. Herhangi bir zamanda isteği aktarmayı da durdurabilirler. Öte yandan çeşitli Decoratorler nesnenin davranışını geliştirirken bunu temel arayüzle tutarlı olarak yaparlar ve isteğin akışını durdurabilmeleri mümkün değildir.
  • Composite ve Decorator desenleri ortak bir yapısal gösterime sahiptirler çünkü her ikiside ucu açık/sonsuz sayıda nesnenin öz yinelemeli (recursive) olarak organize edilmesini sağlar.

    Bir Decorator bir Composite‘e çok benzer fakat Decorator‘ün sadece bir alt bileşeni vardır. İkisi arasındaki önemli fark; Decorator kapsadığı nesneye ek sorumluluklar yüklerken, Composite sadece altındaki nesnelerin sonuçlarını toplar.

    Fakat bu iki desen birlikte de çalışabilir. Composite ağacındaki bir nesnenin davranışını Decorator kullanarak genişletebilirsiniz.
  • Decorator ve Proxy‘nin yapıları benzer, fakat amaçları bambaşkadır. Her iki desende, bir nesnenin işin bir kısmını başka bir nesneye delege ettiği composition prensibine dayanır. Aralarındaki en önemli fark; Proxy genellikle servis nesnesinin yaşam süresini kendisi belirlerken Decoratorlerde bu her zaman istemcinin kontrolündedir.

Decorator Deseni Kod Örnekleri

Örnek PHP Kodu

<?php

namespace RefactoringGuru\Decorator\Conceptual;

/**
 * The base Component interface defines operations that can be altered by
 * decorators.
 */
interface Component
{
    public function operation(): string;
}

/**
 * Concrete Components provide default implementations of the operations. There
 * might be several variations of these classes.
 */
class ConcreteComponent implements Component
{
    public function operation(): string
    {
        return "ConcreteComponent";
    }
}

/**
 * The base Decorator class follows the same interface as the other components.
 * The primary purpose of this class is to define the wrapping interface for all
 * concrete decorators. The default implementation of the wrapping code might
 * include a field for storing a wrapped component and the means to initialize
 * it.
 */
class Decorator implements Component
{
    /**
     * @var Component
     */
    protected $component;

    public function __construct(Component $component)
    {
        $this->component = $component;
    }

    /**
     * The Decorator delegates all work to the wrapped component.
     */
    public function operation(): string
    {
        return $this->component->operation();
    }
}

/**
 * Concrete Decorators call the wrapped object and alter its result in some way.
 */
class ConcreteDecoratorA extends Decorator
{
    /**
     * Decorators may call parent implementation of the operation, instead of
     * calling the wrapped object directly. This approach simplifies extension
     * of decorator classes.
     */
    public function operation(): string
    {
        return "ConcreteDecoratorA(" . parent::operation() . ")";
    }
}

/**
 * Decorators can execute their behavior either before or after the call to a
 * wrapped object.
 */
class ConcreteDecoratorB extends Decorator
{
    public function operation(): string
    {
        return "ConcreteDecoratorB(" . parent::operation() . ")";
    }
}

/**
 * The client code works with all objects using the Component interface. This
 * way it can stay independent of the concrete classes of components it works
 * with.
 */
function clientCode(Component $component)
{
    // ...

    echo "RESULT: " . $component->operation();

    // ...
}

/**
 * This way the client code can support both simple components...
 */
$simple = new ConcreteComponent();
echo "Client: I've got a simple component:\n";
clientCode($simple);
echo "\n\n";

/**
 * ...as well as decorated ones.
 *
 * Note how decorators can wrap not only simple components but the other
 * decorators as well.
 */
$decorator1 = new ConcreteDecoratorA($simple);
$decorator2 = new ConcreteDecoratorB($decorator1);
echo "Client: Now I've got a decorated component:\n";
clientCode($decorator2);

Örnek Python Kodu

class Component():
    """
    The base Component interface defines operations that can be altered by
    decorators.
    """

    def operation(self) -> str:
        pass


class ConcreteComponent(Component):
    """
    Concrete Components provide default implementations of the operations. There
    might be several variations of these classes.
    """

    def operation(self) -> str:
        return "ConcreteComponent"


class Decorator(Component):
    """
    The base Decorator class follows the same interface as the other components.
    The primary purpose of this class is to define the wrapping interface for
    all concrete decorators. The default implementation of the wrapping code
    might include a field for storing a wrapped component and the means to
    initialize it.
    """

    _component: Component = None

    def __init__(self, component: Component) -> None:
        self._component = component

    @property
    def component(self) -> str:
        """
        The Decorator delegates all work to the wrapped component.
        """

        return self._component

    def operation(self) -> str:
        return self._component.operation()


class ConcreteDecoratorA(Decorator):
    """
    Concrete Decorators call the wrapped object and alter its result in some
    way.
    """

    def operation(self) -> str:
        """
        Decorators may call parent implementation of the operation, instead of
        calling the wrapped object directly. This approach simplifies extension
        of decorator classes.
        """
        return f"ConcreteDecoratorA({self.component.operation()})"


class ConcreteDecoratorB(Decorator):
    """
    Decorators can execute their behavior either before or after the call to a
    wrapped object.
    """

    def operation(self) -> str:
        return f"ConcreteDecoratorB({self.component.operation()})"


def client_code(component: Component) -> None:
    """
    The client code works with all objects using the Component interface. This
    way it can stay independent of the concrete classes of components it works
    with.
    """

    # ...

    print(f"RESULT: {component.operation()}", end="")

    # ...


if __name__ == "__main__":
    # This way the client code can support both simple components...
    simple = ConcreteComponent()
    print("Client: I've got a simple component:")
    client_code(simple)
    print("\n")

    # ...as well as decorated ones.
    #
    # Note how decorators can wrap not only simple components but the other
    # decorators as well.
    decorator1 = ConcreteDecoratorA(simple)
    decorator2 = ConcreteDecoratorB(decorator1)
    print("Client: Now I've got a decorated component:")
    client_code(decorator2)

Diğer Tasarım Kalıpları/Design Patterns

Yaratımsal Kalıplar (Creational Patterns)

Yapısal Kalıplar (Structural Patterns)

Davranışsal Kalıplar (Behavioral Patterns)