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.

State Tasarım Deseninin Amacı

State bir nesnenin iç durumu değiştiğinde davranışını da değiştirmesini sağlayan bir tasarım desenidir. Sanki nesne sınıfını (class) değiştirmiş gibi görünür.

Sorun

State deseni sonlu durum makineleri konsepti ile yakından ilişkilidir.

Sonlu Durum Makinesi

Ana fikir şu; herhangi bir zamanda, bir programın olabileceği sınırlı sayıda durum vardır. Herhangi bir benzersiz durumda, program farklı şekilde hareket eder ve program durumu birinden diğerine anlık olarak değişebilir. Fakat, programın mevcut duruma bağlı olarak, durumu bir başka duruma değiştirilebilir veya değiştirilemeyebilir. Bu değiştirme kurallarına geçişler denir ve bunlar önceden belirlidir ve bir sonlu sayıdadır.

Bu yaklaşımı nesnelere de uygulayabilirsiniz. Bir Document sınıfınız olduğunu düşünün. Bir doküman şu 3 durumdan birinde olabilir: Draft (Taslak), Moderation (Moderasyonda) ve Published (Yayımlanmış). Dokümanın publish (yayınla) metodu her durumda farklı çalışır:

  • Taslak durumundayken yayımlanırsa, moderasyon durumuna geçer
  • Moderasyon durumundayken dokümanı genele açık hale getirir, fakat bunu sadece mevcut kullanıcı bir yöneticiyse yapabilir.
  • Daha önceden yayımlanmış bir doküman için durumda bir değişiklik olmaz.
Bir doküman nesnesi için mümkün olan durumlar ve geçişler

State machines (durum makineleri) genel olarak, mevcut duruma göre uygun davranışı seçen koşullu ifadelerle ( if veya switch gibi) uygulanırlar. Genelde durum nesnenin alanlarından bazılarının değerlerinden oluşur. Sonlu-durum-makinelerini daha önce duymadıysanız bile, muhtemelen en az bir defa bir durum uygulaması yapmışsınızdır. Aşağıdaki kod yapısı tanıdık geldi mi?

Koşullu ifadelere dayalı bir durum makinesinin zayıf noktası yeni durumlar ve duruma bağlı davranışlar eklemek istediğinizde ortaya çıkar. Çoğu metot, mevcut duruma göre doğru davranışı seçen devasa koşul ifadeleri içerecektir. Geçiş mantığında yapılacak herhangi bir değişiklik tüm metodlardaki koşullu ifadelerin değişmesini gerektireceğinden bu tarz kodları yönetmek zordur.

Proje geliştikçe sorun daha da büyür. Bütün olası durumları ve geçişleri tasarım aşamasında planlamak hiç de kolay değildir. Böyle olunca da başta çok yalın gözüken bir durum makinesi kodu, zamanla şişerek karmaşık bir hale dönüşebilir.

Çözüm

State deseni bir nesnenin tüm olası durumları için bir sınıf oluşturmanızı ve bu duruma özel davranışların hepsini bu özel sınıf içerisine aktarmanızı tavsiye eder.

Bağlam adı verilen orijinal nesne tüm davranışları kendisi uygulayacağına, her biri kendi durumunu tutan durum nesnelerinin referansını saklayarak, durumla alakalı işleri gerektiğinde onlara delege eder.

Doküman işi bir durum nesnesine delege ediyor.

Bağlamın bir başka duruma geçiş yapması için, aktif durum nesnesi yeni durumu temsil eden bir başka nesne ile değiştirilir. Bunun mümkün olması için tüm durum nesnelerinin aynı arayüzü esas alması şarttır. Böylece bağlam nesnesi durum nesneleri ile bu arayüz üzerinden iletişim kurabilir.

Bu yapı Strategy desenine benzer görünebilir, fakat ikisi arasında önemli bir fark var. State deseninde, durumlar birbirlerinden haberdar olabilir ve bir durumdan diğerine geçişi başlatabilir

Uygulanabilirlik

Mevcut durumuna bağlı olarak farklı hareket etmesi gereken nesneleriniz varsa, olası durum sayıları çok fazlaysa ve bu durumlara özel kodlar çok sık değişiyorsa State desenini kullanabilirsiniz.

Bu desen duruma özel tüm kodları ayrı sınıflara almanızı tavsiye eder. Böylece, diğerlerinden bağımsız olarak yeni durumlar ekleyebilir veya mevcut durumları değiştirebilirsiniz. Böylelikle kodu yönetme maliyetiniz azalır.

Sınıfın belirli alanlarının değerlerine göre hareketlerini değiştirmek için kullanılan aşırı miktarda koşullu ifadelerle dolu, kirli bir sınıfınız varsa bu deseni kullanabilirsiniz.

State deseni bu koşullu ifadeleri ilgili durum sınıflarına dönüştürmenizi sağlar. Bunu yaparken ana sınıfınızda durum yönetimi için kullandığınız geçici alanları ve yardımcı metotları temizleyebilirsiniz.

Koşullu ifadeler esaslı bir durum makinesinde, farklı durumlar ve geçişler için tekrarlanarak kullanılan çok fazla kodunuz varsa bu deseni kullanabilirsiniz.

State deseni durum, sınıfları için bir hiyerarşi oluşturmanızı ve ortak kodları temel bir abstract sınıfı içine alarak tekrarları azaltmanızı sağlar.

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

  • Bridge, State, Strategy (ve bir dereceye kadar Adapter) çok benzer yapıya sahiptir. Aslında, bu desenlerin hepsi kompozisyon, yani işi başka nesnelere delege etme esasına dayanır. Fakat, her biri farklı bir soruna çözüm üretirler. Desenler kodunuzu belirli bir yolla oluşturmak için kesin tarifler değildir.
  • State Strategy ‘nin bir uzantısı olarak düşünülebilir. Her iki desende kompozisyon esaslıdır: ikisi de işin bir bölümünü yardımcı nesnelere delege ederek bağlamın davranışını değiştirirler. Strategy nesneleri birbirinden tamamen bağımsız ve hatta bi haber hale getirir. Öte yandan State durumlar arasında bağımlılıklar olmasına sınır getirmez ve istedikleri zaman bağlam nesnesinin durumunu değiştirmelerine olanak verir.

State Deseni Kod Örnekleri

Örnek PHP Kodu

<?php

namespace RefactoringGuru\State\Conceptual;

/**
 * The Context defines the interface of interest to clients. It also maintains a
 * reference to an instance of a State subclass, which represents the current
 * state of the Context.
 */
class Context
{
    /**
     * @var State A reference to the current state of the Context.
     */
    private $state;

    public function __construct(State $state)
    {
        $this->transitionTo($state);
    }

    /**
     * The Context allows changing the State object at runtime.
     */
    public function transitionTo(State $state): void
    {
        echo "Context: Transition to " . get_class($state) . ".\n";
        $this->state = $state;
        $this->state->setContext($this);
    }

    /**
     * The Context delegates part of its behavior to the current State object.
     */
    public function request1(): void
    {
        $this->state->handle1();
    }

    public function request2(): void
    {
        $this->state->handle2();
    }
}

/**
 * The base State class declares methods that all Concrete State should
 * implement and also provides a backreference to the Context object, associated
 * with the State. This backreference can be used by States to transition the
 * Context to another State.
 */
abstract class State
{
    /**
     * @var Context
     */
    protected $context;

    public function setContext(Context $context)
    {
        $this->context = $context;
    }

    abstract public function handle1(): void;

    abstract public function handle2(): void;
}

/**
 * Concrete States implement various behaviors, associated with a state of the
 * Context.
 */
class ConcreteStateA extends State
{
    public function handle1(): void
    {
        echo "ConcreteStateA handles request1.\n";
        echo "ConcreteStateA wants to change the state of the context.\n";
        $this->context->transitionTo(new ConcreteStateB());
    }

    public function handle2(): void
    {
        echo "ConcreteStateA handles request2.\n";
    }
}

class ConcreteStateB extends State
{
    public function handle1(): void
    {
        echo "ConcreteStateB handles request1.\n";
    }

    public function handle2(): void
    {
        echo "ConcreteStateB handles request2.\n";
        echo "ConcreteStateB wants to change the state of the context.\n";
        $this->context->transitionTo(new ConcreteStateA());
    }
}

/**
 * The client code.
 */
$context = new Context(new ConcreteStateA());
$context->request1();
$context->request2();

Örnek Python Kodu

from __future__ import annotations
from abc import ABC, abstractmethod


class Context:
    """
    The Context defines the interface of interest to clients. It also maintains
    a reference to an instance of a State subclass, which represents the current
    state of the Context.
    """

    _state = None
    """
    A reference to the current state of the Context.
    """

    def __init__(self, state: State) -> None:
        self.transition_to(state)

    def transition_to(self, state: State):
        """
        The Context allows changing the State object at runtime.
        """

        print(f"Context: Transition to {type(state).__name__}")
        self._state = state
        self._state.context = self

    """
    The Context delegates part of its behavior to the current State object.
    """

    def request1(self):
        self._state.handle1()

    def request2(self):
        self._state.handle2()


class State(ABC):
    """
    The base State class declares methods that all Concrete State should
    implement and also provides a backreference to the Context object,
    associated with the State. This backreference can be used by States to
    transition the Context to another State.
    """

    @property
    def context(self) -> Context:
        return self._context

    @context.setter
    def context(self, context: Context) -> None:
        self._context = context

    @abstractmethod
    def handle1(self) -> None:
        pass

    @abstractmethod
    def handle2(self) -> None:
        pass


"""
Concrete States implement various behaviors, associated with a state of the
Context.
"""


class ConcreteStateA(State):
    def handle1(self) -> None:
        print("ConcreteStateA handles request1.")
        print("ConcreteStateA wants to change the state of the context.")
        self.context.transition_to(ConcreteStateB())

    def handle2(self) -> None:
        print("ConcreteStateA handles request2.")


class ConcreteStateB(State):
    def handle1(self) -> None:
        print("ConcreteStateB handles request1.")

    def handle2(self) -> None:
        print("ConcreteStateB handles request2.")
        print("ConcreteStateB wants to change the state of the context.")
        self.context.transition_to(ConcreteStateA())


if __name__ == "__main__":
    # The client code.

    context = Context(ConcreteStateA())
    context.request1()
    context.request2()

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)