“Dünya eskisinden kötü değil. Sadece iletişim çok gelişti.”

Strategy Design Pattern(Strateji Tasarım Şablonu)

Tarih: Ekim 29th, 2012 | Yazar: | Kategoriler: Tasarım Şablonları (Design Patterns) | Etiketler: , | Yorum Yok »

Eminim bu yazıyı okuyanlar en az bir tane Shooter(Silah kullanılan oyunlar. FPS’ler TPS’ler…) oyunu oynamıştır. Shooter oyunlarının temeli düşmanlar, silah ve bu silahı kullanan karakter üzerine kurulmuştur. Bu üç öge olmadan bir shooter oyunu düşünülemez. Gelin bizde bir shooter oyununu PHP tarafında modelleyelim.

Character sınıfımız

namespace Strategy;
require_once 'Weapon.php';
class Character
{
    public $name;

    public function attack($weapon, Character $target)
    {
        switch ($weapon) {
            case 'machineGun':
                printf("'%s' makinalı silah ile vuruldu", $character->name);
                break;
            case 'revolver':
                printf("'%s' tabanca ile vuruldu", $character->name);
                break;
        }
    }
}

Bir kişiye makinalı silah ve tabanca kullanarak ateş edelim.

require_once 'Character.php';

$hedef = new \Strategy\Character();
$hedef->name = 'Emma Richardson';

$katil = new \Strategy\Character();
$katil->name = 'Albert Fish';

$katil->attack('machineGun', $hedef);
$katil->attack('revolver', $hedef);

Ekranımızda aşağıdakiler yazı.

'Emma Richardson' makineli silah ile vuruldu
'Emma Richardson' tabanca ile vuruldu

Yukarıdaki örnekte iki tane Character sınıfından örnekleme yaptık bunlardan biri $hedef isimli değişkene, diğerini $katil isimli değişkene atadık. $katil içinde bulunan karakterimiz $hedef içinde bulunan karaktere attack metodu ile saldırıyor ve verdiğimiz silah bilgisi ile hedefe ateş ediyor.

Yukarıdaki örneği prensipler bakımından inceleyelim.

  • attack metoduna bakarsak, switch yapısını fark edebiliriz. Her bir silah için ayrı ayrı case tanımlaması yaptık ki silah türlerini ayırt edebilelim. Bu yüzden her yeni silah için, yeni bir case eklememiz gerekiyor. Bu da open-closed(geliştirmeye açık-değiştirmeye kapalı) prensibine uymayan bir durum. Sisteme ekleyeceğimiz her bir silah için kullanıcı sınıfı olan Character sınıfında değişiklikler yapmamızı gerektirir. Bu yapı da zaman içerisinde uygulamamızı kırılgan bir duruma sokacaktır. (Gerçek hayattaki örnekler çoğunlukla daha karmaşık olur. Bunu aklımızda tutmamızda fayda var.)
  • Character sınıfımız olması gerekenden daha fazla sorumluluğa sahip. Yine attack metoduna bakarsak, sınıfın silah türlerini ve silahlar hakkında bilgiler içerdiğini görebiliriz. Oysa single responsibility(tek sorumluluk) prensibi bize her sınıfım bir tane sorumluğu olması gerektiğini söyler. Ancak biz Character sınıfına silahlar hakkında bilgi ekleyerek sınıfın sorumluluklarına birden ikiye çıkardık. Bu hali ile Character sınıfı hem bir karakteri, hem de silahları temsil ediyor.

Peki, single responsibility ve open-closed prensiplerini bozmadan uygulamamızı nasıl geliştirebiliriz? Bunu cevabı yazımızın konusu olan Strateji tasarım şablonunda.

Strateji tasarım şablonunu uyguladıktan sonra sınıflarımızın son hali aşağıdaki şekilde olacaktır.

Weapon arayüzü

namespace Strategy;
interface Weapon
{
    public function fire(\Strategy\Character $character);
}

MachineGun sınıfı

namespace Strategy;
require_once 'Weapon.php';
use Strategy\Weapon;
class MachineGun implements Weapon
{
    public function fire(\Strategy\Character $character)
    {
        printf("'%s' makineli silah ile vuruldu", $character->name);
    }
}

Revolver sınıfı

namespace Strategy;
require_once 'Weapon.php';
use Strategy\Weapon;
class Revolver implements Weapon
{
    public function fire(\Strategy\Character $character)
    {
        printf("'%s' tabanca ile vuruldu", $character->name);
    }
}

Character sınıfı

namespace Strategy;
require_once 'Weapon.php';
class Character
{
    public $name;

    public function attack(\Strategy\Weapon $weapon, Character $target)
    {
        $weapon->fire($target);
    }
}

Character sınıfındaki değişikliklere gelmeden önce isterseniz yeni eklenen sınıflara ve arayüzlere bakalım. Öncelikle Weapon adında yeni bir arayüz(interface) ekledik. Bu arayüzün koşullarını yerine getiren(implement) her bir sınıf Weapon özelliği kazanacaktır. Örneğimizde is Weapon arayüzü kullanan sınıflardan fire metodunu mutlaka barındırmalarını istedik. MachineGun ve Revolver sınıfına bakarsanız bu isteğin karşılık bulduğunu görebilirsiniz.

Character sınıfına gelirsek, oradaki attack metodunda bazı değişikler yaptık. Öncelikle metodun $weapon argümanın başına \Strategy\Weapon eklendiğini fark etmişsinizdir. Bunu ekleyerek $weapon argümanı için gönderilen içeriğin mutlaka Weapon arayüzüne sahip olmasını istedik. (Gönderilen içerik Weapon arayüzüne sahip değilse PHP fatal error verecektir.) Diğer yandan yaptığımız tanımlama ile $weapon için gönderilen sınıfın hangi metotları ile iletişim kuracağımızı kabul etmiş sayıldık. Biz, bundan sonra sadece Weapon arayüzünde tanımlı olan metotları attack metodunda kullanacağız. Yani bu örnekte sadece fire metodunu kullanacağız. Çünkü Weapon arayüzünde sadece o tanımlı. Burada şunu belirtmekte fayda var. Arayüz dışındaki metotlara da erişebiliriz ama bu önerilen bir yöntem değildir. Çünkü arayüzde bulunmayan metotların olup olmadığı veya nasıl çalıştığının bir garantisi yok. Oysa arayüz içinde bulunan metotlarda, bu garanti arayüz tarafından sağlanıyor.

Ayrıcı attack metodun içinde bulunan switch yapısını tamamen kaldırdık. Bunun yerine Weapon arayüzünün fire metodunu kullanarak hedefe ateş ediyoruz.

Character sınıfında yaptığımız son değişiklikler sayesinde single responsibility prensibini artık ihlal etmiyoruz. Çünkü Character sınıfının silah sorumluğunu ondan alıp Weapon arayüzü üzerinden MachineGun ve Revolver sınıfına devrettik. Character sınıfın şu anda tek bir sorumluluğu var o da; karaktere ait bilgileri ihtiva etmek.

Son yaptığımız değişiklikler ile beraber kodumuzu çalıştıralım.

require_once 'Character.php';
require_once 'MachineGun.php';
require_once 'Revolver.php';

$hedef = new \Strategy\Character();
$hedef->name = 'Emma Richardson';

$katil = new \Strategy\Character();
$katil->name = 'Albert Fish';

$machineGun = new \Strategy\MachineGun();
$revolver = new \Strategy\Revolver();

$katil->attack($machineGun, $hedef);
$katil->attack($revolver, $hedef);

Çıktımız

'Emma Richardson' makineli silah ile vuruldu
'Emma Richardson' tabanca ile vuruldu

Her şey yolunda gözüküyor. Yukarıda bahsettiğim open-closed prensibine kodumuz uyuyor mu peki? Bunu yeni bir silah ekleyerek test edebiliriz. Bu silah da sniper olsun.

Sniper sınıfı

namespace Strategy;
require_once 'Weapon.php';
use Strategy\Weapon;
class Sniper implements Weapon
{
    public function fire(\Strategy\Character $character)
    {
        printf("'%s' sniper ile vuruldu", $character->name);
    }
}

Yeni eklediğimiz Sniper sınıfı ile beraber tüm silahları kullanarak hedefimize saldıralım.

require_once 'Character.php'; 
require_once 'MachineGun.php'; 
require_once 'Revolver.php'; 
require_once 'Sniper.php'; 
$hedef = new \Strategy\Character(); 
$hedef->name = 'Emma Richardson';

$katil = new \Strategy\Character();
$katil->name = 'Albert Fish';

$machineGun = new \Strategy\MachineGun();
$revolver = new \Strategy\Revolver();
$sniper = new \Strategy\Sniper();

$katil->attack($machineGun, $hedef);
$katil->attack($revolver, $hedef);
$katil->attack($sniper, $hedef);

Çıktı

'Emma Richardson' makineli silah ile vuruldu
'Emma Richardson' tabanca ile vuruldu
'Emma Richardson' sniper ile vuruldu

Evet! Strateji tasarım şablonu sayesinde kullanıcı(Character) sınıfımızda herhangi bir değişiklik yapmadan uygulamamıza kolayca yeni bir silah ekledik. (Değiştirme yapmadan geliştirme yaptık.) Open-closed prensip ihlalini de bu sayede ortadan kaldırdık.

Örneğimizi Strateji tasarım şablonu çerçevesinde incelersek;

  • Weapon arayüzüne sahip olan her bir sınıf birer stratejidir.
  • Her bir strateji kendine has algoritmalara sahiptir. Bu örnek için Revolver tek tek atış ederken, MachineGun arka arkaya ateş ettiğini düşünebiliriz.
  • Stratejimiz değiştiğinde, uygulamamızın davranışları da değişecektir. Bu da strateji tasarım şablonunu davranışsal tasarım şablonu(behavioral design pattern) yapmaktadır. Kullandığımız örnekte her bir silahın kendine has atışı olduğuna göre uygulamamız aynı amaç için birden fazla ve birbirinden farklı davranışlara sahip olmuş demektir.

Sonuç

Geliştirdiğimiz uygulamada bir amacı(yukarıdaki örnekte attack) birden fazla yöntem(MachineGun, Revolver…) kullanarak uygulamamıza farklı davranışlar kazandırmak istiyorsak veya sınıfların birbiri hakkında daha az bilgiye sahip olmasını istiyorsak Strateji Tasarım Şablonunu kullanabiliriz.

Bu yazımda kullandığım örneğe buradan ulaşabilirsiniz.