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.

Strategy Tasarım Deseninin Amacı

Strategy deseni bir algoritma ailesi oluşturup her birini farklı sınıfa yerleştirerek nesnelerini birbiri arasında değişebilir hale getirmeyi sağlayan bir tasarım desenidir.

Sorun

Gezginler için bir navigasyon uygulaması oluşturmaya karar verdiniz. Uygulamayı herhangi bir şehirde, güzel bir harita üzerinde, kullanıcıların yönlerini hızlıca belirlemelerini sağlayacak şekilde inşa ettiniz.

Uygulama için en çok istenen özelliklerden biri de otomatik rota oluşturabilmesi oldu. Bunun için kullanıcı varmak istediği adresi girmeli ve o adrese gitmek için en hızlı rotayı görebilmeli.

Uygulamanın ilk sürümü, yalnızca yollar üzerinde rotalar oluşturabiliyordu. Arabayla seyahat edenler sevinçten havalara uçtu. Ama görünüşe göre, herkes tatilde araba kullanmayı tercih etmiyor. Bir sonraki güncelleme ile yürüyüş rotaları oluşturmak için bir seçenek eklediniz. Hemen ardından insanların güzergahlarında toplu taşımayı kullanmalarına izin veren bir seçenek daha eklediniz.

Ancak, bu sadece başlangıçtı. Daha sonra bisikletçiler için rota oluşturmayı eklemeyi planladınız. Hatta daha sonra, şehrin tüm turistik mekanları arasında rota oluşturmak için başka bir seçenek eklemeyi düşündünüz.

Strategy tasarım deseni nedir
Navigasyon kodu gittikçe karmaşık bir hal aldı.

Uygulama ticari olarak bakıldığında başarılı olsa da teknik kısım bir çok baş ağrısına neden oldu. Her yeni rota belirleme algoritması ana navigasyon kodunun büyüklüğü iki katına çıktı. Bir yerden sonra bu canavarla baş etmek zor olmaya başladı.

Algoritmalardan birinde yapılacak herhangi bir değişiklik, ister basit bir hata düzeltmesi isterse sokak puanında küçük bir değişiklik olsun, tüm sınıfı etkileyerek halihazırda çalışan kodda bir hata oluşturma şansını artırdı.

Ayrıca, ekip çalışması verimsiz hale geldi. Başarılı sürümden hemen sonra işe alınan takım arkadaşlarınız, birleştirme çatışmalarını (merge conflict) çözmek için çok fazla zaman harcadıklarından şikayet ediyorlar. Yeni bir özelliğin eklenmesi, diğerleri tarafından üretilen kodlarla çelişecek şekilde aynı büyük sınıf üzerinde çalışılmasını gerektiriyor.

Çözüm

Strategy deseni, spesifik bir işi bir çok farklı yolla yapan bir sınıfı alıp bütün bu algoritmaları strategy adı verilen ayrı sınfılara ayırmanızı öneriyor.

Bu modelde, bağlam (context) adını vereceğimiz orijinal sınıfta, oluşturacağımız bu stratejilere referansları içeren alanlar oluşturulması gerekiyor.

Bağlam adı verilen orijinal sınıf tüm davranışları kendisi uygulamak yerine, her biri kendi durumunu tutan durum nesnelerinin referansını saklar ve durumla alakalı işleri gerektiğinde onlara delege eder.

Yapılacak iş için uygun algoritmayı seçmek bağlamın sorumluluğu değildir. İstemci kod bağlama tercih ettiği stratejiyi gönderir. Aslına bakarsanız bağlam nesnesinin stratejilerin ne yaptığı ile ilgili bir fikri yoktur. Tüm stratejilerle ortak bir arayüz üzerinden tek bir metot ile etkileşime girer ve algoritmanın içeriğini sadece seçilen strateji bilir.

strateji tasarım deseni örnekleri
Rota planlama stratejileri

Navigasyon uygulamamızda, her rota algoritması sadece buildRoute (rota oluştur) sınıfı içeren bağımsız bir sınıfa dönüştürülebilir. Metot bir başlangıç ve bitiş konumu alır ve rotayı geri döndürür.

Aynı parametreler ile çağrılsalar bile her rota sınıfı başka bir rota döndürebilir, ana navigasyon uygulaması hangi algoritmanın seçildiği ile ilgilenmez, onun tek görevi geri döndürülen rotayı harita üzerinde çizmektir. Bı sınıfın aktif rota stratejisini değiştirmek için bir metodu vardır, böylece kullanıcı arabirimindeki düğmeler gibi istemciler rota stratejisini bir başkasıyla değiştirebilirler.

Uygulanabilirlik

Bir nesne içinde bir algoritmanın farklı türevlerini kullanmak ve çalışma esnasında bir algoritmadan diğerine geçebilmek istediğinizde strateji tasarım kalıbın kullanabilirsiniz.

Strateji deseni, belirli alt görevleri farklı şekillerde gerçekleştirebilen farklı alt nesnelerle ilişkilendirerek, nesnenin davranışını çalışma zamanında dolaylı olarak değiştirmenize olanak tanır.

Yalnızca bazı davranışları yürütme biçimleri bakımından farklılık gösteren çok sayıda benzer sınıfınız olduğunda strategy tasarım kalıbını kullanabilirsiniz.

Strateji deseni, değişen davranışı ayrı bir sınıf hiyerarşisine çıkarmanıza ve orijinal sınıfları tek bir sınıfta birleştirmenize, böylece tekrarlanan kodu azaltmanıza olanak tanır.

Bir sınıfın iş mantığı için çok önemli olmayan algoritma uygulama ayrıntılarını sınıfın kendisinden yalıtmak için strategy tasarım kalıbını kullanabilirsiniz.

Strateji modeli, kodu, dahili verileri ve çeşitli algoritmaların bağımlılıklarını kodun geri kalanından ayırmanıza olanak tanır. Çeşitli istemciler, algoritmaları çalıştırmak ve bunları çalışma esnasında değiştirmek için basit bir arayüze sahip olurlar.

Sınıfınız, aynı algoritmanın farklı türevleri arasında geçiş yapan çok büyük bir koşullu operatöre sahip olduğunda strategy tasarım kalıbını kullanabilirsiniz.

Strateji tasarım deseni, tüm algoritmaları hepsi aynı arabirimi esas alan farklı sınıflara ayıklayarak bu büyük koşullu durumdan kurtulmanıza olanak tanır. Orijinal nesne, algoritmanın tüm varyasyonlarını kendi uygulamak yerine yürütmeyi bu nesnelerden birine devreder.

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.
  • Her ikisi de bir nesneyi parametre olarak kullanmayı sağladığı için Command ve Strategy benzer desenler olarak görülebilir. Fakat aslında çok farklı amaçları vardır;
    • Command herhangi bir işlemin bir nesneye dönüştürülmesi için kullanılır. Bu işlemin parametreleri nesnenin alanları haline gelir. Bu dönüşüm işlemin çalışmasını ertelemenizi, sıraya almanızı, geçmişini saklamanızı ve uzak servislere gönderebilmenizi sağlar.
    • Öte yandan Strategy aynı şeyi yapmak için farklı yöntemler sunar ve tek bir bağlam içerisinde algoritmaları değiştirebilmenize olanak tanır.
  • Decorator bir nesnenin görünümünü değiştirmenize izin verirken Strateji özünü değiştirmenize olanak tanır.
  • Template Method deseni ‘inheritance’ temellidir, bir algoritmanın belirli bölümlerini alt sınıflara genişleterek değiştirmenizi sağlar. Strateji ise kompozisyon temellidir, nesnenin bazı davranışlarını ona o davranışlarla ilgili başka stratejiler vererek değiştirirsiniz. Template Method deseni sınıf seviyesinde çalışır, statiktir. Strategy deseni ise nesne seviyesinde çalışır ve nesnenin davranışlarını çalışma zamanında değiştirmenize olanak tanır.
  • 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.

Observer Deseni Kod Örnekleri

Örnek PHP Kodu

<?php

namespace RefactoringGuru\Strategy\Conceptual;

/**
 * The Context defines the interface of interest to clients.
 */
class Context
{
    /**
     * @var Strategy The Context maintains a reference to one of the Strategy
     * objects. The Context does not know the concrete class of a strategy. It
     * should work with all strategies via the Strategy interface.
     */
    private $strategy;

    /**
     * Usually, the Context accepts a strategy through the constructor, but also
     * provides a setter to change it at runtime.
     */
    public function __construct(Strategy $strategy)
    {
        $this->strategy = $strategy;
    }

    /**
     * Usually, the Context allows replacing a Strategy object at runtime.
     */
    public function setStrategy(Strategy $strategy)
    {
        $this->strategy = $strategy;
    }

    /**
     * The Context delegates some work to the Strategy object instead of
     * implementing multiple versions of the algorithm on its own.
     */
    public function doSomeBusinessLogic(): void
    {
        // ...

        echo "Context: Sorting data using the strategy (not sure how it'll do it)\n";
        $result = $this->strategy->doAlgorithm(["a", "b", "c", "d", "e"]);
        echo implode(",", $result) . "\n";

        // ...
    }
}

/**
 * The Strategy interface declares operations common to all supported versions
 * of some algorithm.
 *
 * The Context uses this interface to call the algorithm defined by Concrete
 * Strategies.
 */
interface Strategy
{
    public function doAlgorithm(array $data): array;
}

/**
 * Concrete Strategies implement the algorithm while following the base Strategy
 * interface. The interface makes them interchangeable in the Context.
 */
class ConcreteStrategyA implements Strategy
{
    public function doAlgorithm(array $data): array
    {
        sort($data);

        return $data;
    }
}

class ConcreteStrategyB implements Strategy
{
    public function doAlgorithm(array $data): array
    {
        rsort($data);

        return $data;
    }
}

/**
 * The client code picks a concrete strategy and passes it to the context. The
 * client should be aware of the differences between strategies in order to make
 * the right choice.
 */
$context = new Context(new ConcreteStrategyA());
echo "Client: Strategy is set to normal sorting.\n";
$context->doSomeBusinessLogic();

echo "\n";

echo "Client: Strategy is set to reverse sorting.\n";
$context->setStrategy(new ConcreteStrategyB());
$context->doSomeBusinessLogic();

Örnek Python Kodu

rom __future__ import annotations
from abc import ABC, abstractmethod
from typing import List


class Context():
    """
    The Context defines the interface of interest to clients.
    """

    def __init__(self, strategy: Strategy) -> None:
        """
        Usually, the Context accepts a strategy through the constructor, but
        also provides a setter to change it at runtime.
        """

        self._strategy = strategy

    @property
    def strategy(self) -> Strategy:
        """
        The Context maintains a reference to one of the Strategy objects. The
        Context does not know the concrete class of a strategy. It should work
        with all strategies via the Strategy interface.
        """

        return self._strategy

    @strategy.setter
    def strategy(self, strategy: Strategy) -> None:
        """
        Usually, the Context allows replacing a Strategy object at runtime.
        """

        self._strategy = strategy

    def do_some_business_logic(self) -> None:
        """
        The Context delegates some work to the Strategy object instead of
        implementing multiple versions of the algorithm on its own.
        """

        # ...

        print("Context: Sorting data using the strategy (not sure how it'll do it)")
        result = self._strategy.do_algorithm(["a", "b", "c", "d", "e"])
        print(",".join(result))

        # ...


class Strategy(ABC):
    """
    The Strategy interface declares operations common to all supported versions
    of some algorithm.

    The Context uses this interface to call the algorithm defined by Concrete
    Strategies.
    """

    @abstractmethod
    def do_algorithm(self, data: List):
        pass


"""
Concrete Strategies implement the algorithm while following the base Strategy
interface. The interface makes them interchangeable in the Context.
"""


class ConcreteStrategyA(Strategy):
    def do_algorithm(self, data: List) -> List:
        return sorted(data)


class ConcreteStrategyB(Strategy):
    def do_algorithm(self, data: List) -> List:
        return reversed(sorted(data))


if __name__ == "__main__":
    # The client code picks a concrete strategy and passes it to the context.
    # The client should be aware of the differences between strategies in order
    # to make the right choice.

    context = Context(ConcreteStrategyA())
    print("Client: Strategy is set to normal sorting.")
    context.do_some_business_logic()
    print()

    print("Client: Strategy is set to reverse sorting.")
    context.strategy = ConcreteStrategyB()
    context.do_some_business_logic()
 Output.txt: Execution result

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)