Home > Tasarım Desenleri > Strateji Tasarım Deseni

Strateji Tasarım Deseni

Uzun zamandır yazmak istediğim konulardan birisi de tasarım desenleri. Strateji deseni çok sık kullanılan ve çok yararlı olduğunu düşündüğüm bir tasarım deseni olduğu için bununla başlamak istedim. Bu desenin kitaplardaki tanımını verip kafa karıştırmaktansa bir örnekle başlamak istiyorum. Bir şirketimiz olduğunu ve bu şirkette çalışan teknisyen, mühendis ve yönetici türünde elemanlarımız olduğunu varsayalım. Her bir eleman türü için de ayrı ayrı maaş hesaplama algoritmalarımız olsun. Bu durumu koda döktüğümüzde aşağıdaki gibi bir sonuç karşımıza çıkabilir.

public class MaasHesap {	

	public int maasHesapla(String elemanTuru){

		if(elemanTuru.equals("teknisyen")){
			return teknisyenMaasiHesapla();
		}

		else if(elemanTuru.equals("muhendis")){
			return muhendisMaasiHesapla();
		}

		else if(elemanTuru.equals("yonetici")){
			return yoneticiMaasiHesapla();
		}

		return 0;
	}

	private int teknisyenMaasiHesapla() {
		// teknisyen maasi hesaplama algoritmasi
	}

	private int muhendisMaasiHesapla() {
		// muhendis maasi hesaplama algoritmasi
	}	

	private int yoneticiMaasiHesapla() {
		// yonetici maasi hesaplama algoritmasi
	}
}

Bu sınıf basit olarak maasHesapla yordamı içerisinde gelen parametreye göre bir algoritma seçmekte ve algoritmanın ürettiği sonucu istemciye döndürmektedir. Bu kodun çalışması için yazılabilecek istemciye bakalım.

public class MaasHesapClient {

	public static void main(String[] args) {

		MaasHesap maasHesapClient = new MaasHesap();
		int yoneticiMaas = maasHesapClient.maasHesapla("yonetici");
		int muhendisMaas = maasHesapClient.maasHesapla("muhendis");
		int teknisyenMaas = maasHesapClient.maasHesapla("teknisyen");

		System.out.println("Yonetici maasi: " + yoneticiMaas);
		System.out.println("Muhendis maasi: " + muhendisMaas);
		System.out.println("Teknisyen maasi: " + teknisyenMaas);
	}
}

İstemci MaasHesap sınıfından bir nesne üreterek yönetici, mühendis ve teknisyen eleman türleri için maasHesapla metodunu çağırmaktadır. Bu tasarımdaki problemi nesneye yönelik tasarım tecrübesi olan arkadaşlar görüyor olabilirler. MaasHesap sınıfındaki maasHesapla metoduna bakacak olursak, if-else blokları içerisinde gelen parametre değerine göre farklı algoritmalar çalıştırmaktayız. Yani hangi algoritmanın işletileceği gelen parametreye göre belirleniyor. Bu parametreyi ise istemci sınıfımız gönderiyor. Dolayısıyla istemci sınıfın hangi parametreyi göndereceğini bilmesi gibi bir zorunluluk vardır. Örneğin, istemci sınıf yönetici maaşı hesaplamak istediğinde maasHesapla metoduna “yonetici” parametresini geçmek zorundadır. Bu durum bu iki sınıf arasında bir bağımlılığa yol açmaktadır. Başka bir deyişle işletilecek algoritmanın seçilmesi ile bu algoritmanın çalıştırılması işlemleri iç içe geçmiş durumdadır.

Peki bu durum ne gibi sorunlara yol açar? Sisteme yeni bir eleman türü eklendiğini düşünürsek, MaasHesap sınıfının tekrar elden geçirilmesi gerekecektir. Yeni ekleyeceğimiz maaş hesaplama algoritması için gereken kodlar sınıfa eklenmeli ve maasHesapla metodu içerisinde yeni bir if bloğu ile seçim işleminin yapılması gerekmektedir. Bu kadar küçük bir kodda bir sorunmuş gibi gözükmese de, gerçek sistemlerde test edilmiş, çalıştığı bilinen sınıfların modifiye edilmesi istenen bir durum değildir. Yapılması gereken şey, sistemde var olan sınıfları değiştirmek yerine, sisteme yeni sınıflar ekleyerek problemin çözülmesidir. Böylece var olan koda hiç dokunulmamış olacak, dolayısıyla sistemin çalışan tarafında hasara yol açma ihtimali ortadan kaldırılmış olacaktır. Buna yazılımda “open-closed principle” adı verilmektedir. Bunu sağlamak için de yukarıda bahsettiğim bağımlılığın ortadan kaldırılması gerekmektedir.

Kalıtım Çözümü

Bu sorunu kalıtım yoluyla çözebilir miyiz önce ona bir bakalım. Farklı eleman türlerimizi bir ata sınıftan türeterek bir kalıtım hiyerarşisi kurabiliriz. Ata sınıfımız içerisine abstract maasHesapla metodunu yazıp, Yonetici, Muhendis ve Teknisyen sınıflarında da gerçekleştirimlerimizi yapabiliriz. Koda geçmeden önce söylediklerimizi basit olarak şekile dökelim.

Yandaki şekilde görüldüğü üzere Eleman ata sınıfımızdan Yonetici, Muhendis ve Teknisyen olmak üzere 3 sınıf türetiyoruz.

MaasHesapClient sınıfı içerisinde Eleman türünden referanslar kullanarak, maasHesapla metoduna herhangi bir parametre geçmeye gerek kalmadan çalışma zamanında doğru maaş hesaplama algoritmasını işletmeye çalışacağız.

Şimdi koda geçebiliriz.

public abstract class Eleman {

	public abstract int maasHesapla();
}

public class Yonetici extends Eleman {

	public int maasHesapla() {
		// Yonetici maasi hesaplama algoritmasi
	}
}

public class Muhendis extends Eleman {

	public int maasHesapla() {
		// Muhendis maasi hesaplama algoritmasi
	}
}

public class Teknisyen extends Eleman {

	public int maasHesapla() {
		// Teknisyen maasi hesaplama algoritmasi
	}
}

Görüldüğü üzere Eleman isimli ata sınıfımız ve bu sınıftaki soyut maasHesapla metodunu override eden 3 çocuk sınıfımız var. Bu şekilde maaş hesaplama algoritmalarını birbirinden ayırıp farklı sınıflara koyduk ve her üçü de aynı sınıftan türediği için ata sınıf referansları ile çocuk sınıflardaki maasHesapla metotlarını çalıştırabileceğiz. Daha güzeli, tek bir metod içerisinden if-else blokları ile dallanma yapmadığımız için, istemcinin maasHesapla metoduna parametre geçmesine gerek kalmamıştır. Bu durumda istemcimiz nasıl olur bir de ona bakalım.

public class MaasHesapClient {

	public static void main(String[] args) {

		Eleman eleman = new Yonetici();
		int yoneticiMaas = eleman.maasHesapla();

		eleman = new Muhendis();
		int muhendisMaas = eleman.maasHesapla();

		eleman = new Teknisyen();
		int teknisyenMaas = eleman.maasHesapla();

		System.out.println("Yonetici maasi: " + yoneticiMaas);
		System.out.println("Muhendis maasi: " + muhendisMaas);
		System.out.println("Teknisyen maasi: " + teknisyenMaas);
	}
}

Yeni istemci sınıfımız artık maasHesapla metodunu parametre geçmeden çağırıyor. Dolayısıyla istemci bir maasHesapla metodunu çağıracağı zaman, hangi algoritmanın çalışacağı ile ilgilenmiyor. Eleman referansı bir Muhendis nesnesini gösteriyorsa Muhendis sınıfındaki algoritma, Yonetici sınıfını gösteriyorsa Yonetici sınıfındaki algoritma çalışacaktır. Biz daha önceki örnekte hangi algoritmanın çalışacağını istemci tarafında belirlerken, artık bu işi Java Sanal Makinası’na bırakmış oluyoruz. İstemci olarak arka tarafta yapılan seçim işlemi ile artık ilgilenmiyoruz. Ve böylece de önceki örnekte var olan sınıflar arasındaki bağımlılıktan kurtulmuş oluyoruz.

Sisteme yeni bir eleman türü eklendiğinde ise yapmamız gereken tek şey Eleman sınıfından yeni bir sınıf türetmek ve maasHesapla metodunu override etmek olacaktır. Böylece daha önce yazdığımız sınıflara hiç dokunmadan sisteme yeni bir sınıf ekleyerek problemimizi çözmüş oluyoruz. O zaman bu tasarım bizim için gayet ideal görünüyor. Peki, sadece kalıtım ile böyle bir sorunu kolayca çözebiliyorsak strateji desenini niye kullanıyoruz?

Kalıtım Çözümü Neden Yetersiz?

Şimdi yeni bir gereksinim geldiğini ve çalışanların maaşlarının yanısıra izin günlerinin de hesaplanması gerektiğini düşünelim. Ancak izin günleri eleman türüne göre değil, elemanın bağlı olduğu departmana göre yapılıyor olsun. Yönetici türünün A departmanına, Mühendis ve Teknisyen türünün de B departmanına bağlı olduğunu düşünelim. Bu gereksinimi sistemimize nasıl entegre edebiliriz?

Çözüm 1

1. Eleman sınıfına izinHesapla() isimli soyut bir metot daha ekleriz. Alt sınıflarda da bu metodu override ederek izin günlerini hesaplarız.

Ancak, Mühendis ve Teknisyen aynı departmana bağlı olduğu için aynı kodu iki yere kopyalamak zorunda kalırız.  Kod kopyalamak hem yazılımın bakımını zorlaştıran hem de hata yapma riskini çok artıran bir durumdur. Üstelik bu çözümde A ve B departmanlarına yer vermedik. Hangi tür elemanın hangi departmana bağlı olduğunu nereden bileceğiz?

Bu çözümde Eleman diye bir sınıf yaratıp diğer sınıflarımızı bu sınıftan kalıttık. Eleman sınıfını bir arayüze (interface) dönüştürebiliriz ancak yine de aynı metotları farklı sınıflara kopyalama sorununu aşamayız.

Çözüm 2

2. Departmanlar için ayrı bir hiyerarşi oluştururuz. Departman isimli bir ata sınıf ve DepartmanA ve DepartmanB olmak üzere 2 çocuk sınıf yazarız. Gerekli izin hesaplama metotlarını DepartmanA ve DepartmanB içerisinde yapıp eleman sınıflarımızı bu sınıflardan kalıtırız. 

Burada da şöyle bir sorun var. Java ve C# gibi diller çoklu kalıtımı desteklemezler. Biz yukarıda zaten Yonetici, Muhendis ve Teknisyen sınıflarını Eleman sınıfından kalıtmıştık. Dolayısıyla ikinci bir kalıtım yapamayız.

Ata sınıflarımızdan birisini arayüze dönüştürüp çoklu kalıtım kısıtlamasından kurtulsak, sisteme sonradan eklememiz gereken özellikleri nasıl destekleyeceğiz? Üstelik bu çözümde Muhendis, Yonetici ve Teknisyen sınıflarını DepartmanA ve DepartmanB sınıflarından kalıttık. Peki Muhendis bir Departman mıdır? Tabi ki değildir. O zaman kurduğumuz kalıtım ilişkisi baştan yanlıştır. Departman sınıfını kalıtma sebebimiz sadece izinHesapla() davranışını ata sınıftan almak içindir.

Bu senaryolardan da göreceğimiz gibi kalıtımla yaptığımız çözüm ilk başta yeterli gibi görünse de bize yeterli esnekliği sağlamıyor ve bizi yanlış tasarımlara götürüyor. Biz burada küçücük bir örnekle bile bu çözümü zorlayabiliyorsak, gerçek sistemlerde karşılaşabileceğimiz sorunları siz düşünün. Kalıtım çözümünün neden yeterli olmadığını anladıysak strateji deseni ile yapacağımız çözüme geçebiliriz.

Strateji Çözümü

Sanırım problem üzerinde yeterince kafa yorduk. Yapmaya çalıştığımız şey, aynı işi farklı şekillerde yapan algoritmalar olduğunda (bizim örneğimizde maaş ve izin hesaplama) bu algoritmalardan hangisinin seçileceğini çalışma zamanında belirleyerek istemci ile diğer sınıflarımız arasındaki bağımsızlığı sağlamak. Strateji deseni de tam olarak bu probleme çözüm getirmek için vardır.

Şekilde görüldüğü gibi MaasHesap ve IzinHesap isimli iki tane arayüzümüz var ve bu arayüzleri uygulayan sınıflarımız var. Burada önemli olan nokta Eleman sınıfı. Daha önceki örneklerimizde Eleman sınıfı diğer sınıflar için ata sınıf olma özelliği taşırken, burada MaasHesap ve IzinHesap referansları tutan ve istemci tarafından kullanılan bir sınıfa dönüştü. Maaş hesaplama algoritması Munendis, Yonetici ve Teknisyen için farklı olduğu için, 3 farklı sınıfta bunu uyguladık. İzin hesaplama algoritması da departmanlara göre değiştiği için IzinHesap arayüzünü uygulayan iki farklı departman sınıfımız var. Koda bakarak daha iyi anlayabiliriz.

public interface MaasHesap {	

	public int maasHesapla();
}

public class MuhendisMaasHesap implements MaasHesap {

	public int maasHesapla() {		
		return 2500;
	}
}

public class TeknisyenMaasHesap implements MaasHesap {

	public int maasHesapla() {		
		return 1500;
	}
}

public class YoneticiMaasHesap implements MaasHesap {

	public int maasHesapla() {		
		return 3500;
	}
}

public interface IzinHesap {

	public int izinHesapla();
}

public class DepartmanAIzinHesap implements IzinHesap {

	public int izinHesapla() {
		return 15;
	}
}

public class DepartmanBIzinHesap implements IzinHesap {

	public int izinHesapla() {
		return 12;
	}
}

Yukarıda sadece arayüzleri ve bunları uygulayan sınıfları verdim. Kolaylık olması açısından izinHesapla() ve maasHesapla() algoritmalarının sadece bir integer değer döndürdüğünü düşünelim. Burada artık kalıtım ilişkisi kullanmıyoruz. Onun yerine izinHesapla() ve maasHesapla() davranışlarını birer arayüze koyup farklı sınıflarda bu arayüzleri uyguluyoruz. Şimdi Eleman sınıfımız nasıl yazılmalı ona bakalım.

public class Eleman {

	public IzinHesap izinHesap;
	public MaasHesap maasHesap;

	public Eleman(IzinHesap izinHesap, MaasHesap maasHesap) {
		this.izinHesap = izinHesap;
		this.maasHesap = maasHesap;
	}

	public int izinHesapla(){		
		return izinHesap.izinHesapla();
	}

	public int maasHesapla(){		
		return maasHesap.maasHesapla();
	}
}

Kalıtım çözümünde eleman sınıfı soyut bir sınıftı ve Yonetici, Muhendis ve Teknisyen sınıfları bu sınıftan kalıtılıyordu. Şimdi ise Eleman sınıfının içerisine kullandığımız MaasHesap ve IzinHesap arayüzlerinin birer referansını koyduk. Bu referansların göstereceği nesneleri de yapıcı metot içerisinde alarak referanslara atama yapıyoruz. İstemci tarafından Eleman sınıfına yapılan maasHesapla() ve izinHesapla() çağrılarını kullandığımız referanslar aracılığıyla, bu metotları gerçekte uygulamış olan alt sınıfları aktarıyoruz. Örneğin elimizdeki MaasHesap referansı bir TeknisyenMaasHesap nesnesini referans ediyor olabilir. Bu durumda Eleman sınıfındaki maasHesap() metodu TeknisyenMaasHesap sınıfındaki maasHesap() metodunu çağırıp oradan dönen sonucu istemciye döndürecektir. Aynı durum izin hesaplama algoritması için de geçerlidir. Bu şekilde istemci arka tarafta hangi algoritmanın çalıştığını bilmesine gerek kalmadan hareket edebiliyor. Son olarak istemci koduna da bakalım.

public class Istemci {

	public static void main(String[] args) {

		Eleman eleman = new Eleman(new DepartmanAIzinHesap(), new YoneticiMaasHesap());
		int yoneticiMaas = eleman.maasHesapla();
		int yoneticiIzin = eleman.izinHesapla();

		eleman = new Eleman(new DepartmanBIzinHesap(), new MuhendisMaasHesap());
		int muhendisMaas = eleman.maasHesapla();
		int muhendisIzin = eleman.izinHesapla();

		eleman = new Eleman(new DepartmanBIzinHesap(), new TeknisyenMaasHesap());
		int teknisyenMaas = eleman.maasHesapla();
		int teknisyenIzin = eleman.izinHesapla();

		System.out.println("Yonetici maasi: " + yoneticiMaas + " Yonetici Izini: " + yoneticiIzin);
		System.out.println("Muhendis maasi: " + muhendisMaas + " Muhendis Izini: " + muhendisIzin);
		System.out.println("Teknisyen maasi: " + teknisyenMaas + " Teknisyen Izini: " + teknisyenIzin);
	}
}

İstemci koduna bakarsak, eleman nesnelerini oluştururken gerekli nesneleri parametre olarak yapıcı metoda geçmemiz lazım. Eleman nesnesini oluşturduktan sonra yapacağımız tek şey, bu referans üzerinden maasHesap() ve izinHesap() metotlarını çağırmak olacak. Böylece istemci arka tarafta hangi algoritmaların çalıştığından haberi olmadan işini yapabiliyor. Dolayısıyla arada bir bağımlılık da oluşmamış oluyor. Kodun çıktısına bakalım.

Yonetici maasi: 3500 Yonetici Izini: 15
Muhendis maasi: 2500 Muhendis Izini: 12
Teknisyen maasi: 1500 Teknisyen Izini: 12

Gördüğünüz gibi Yönetici, Mühendis ve Teknisyen türündeki elemanların maaşları ve izin günleri farklı algoritmalarda belirlenmesine rağmen, biz istemci tarafında bu durumdan habersiziz. Gerekli Eleman referansı üzerinden metot çağrılarımızı yaptığımız zaman arka planda çalışması gereken metotlar çalışıp bize gerekli değerleri döndürüyorlar.

Burada yaptığımız tasarım ile kalıtımda karşılaştığımız sorunlardan kurtulmuş oluyoruz. Arayüzler kullandığımız için kalıtımın getirdiği kısıtlar yok ve yeni değişiklikler söz konusu olduğunda tasarım bize gerekli esnekliği sağlıyor.

Strateji desenini şu cümle ile özetleyebiliriz: “Değişeni bul ve sarmala”. Örneğimiz üzerinden gidecek olursak, her eleman türü için maas ve izin hesaplama davranışları vardır. Ancak bu davranışlar eleman türüne göre değişmektedir. Dolayısıyla bu davranışları sarmalamamız gerekir. Bunu yapmak için de kodda yaptığımız gibi değişen davranışı bir arayüze alıp, o arayüzün referansını kullanmak gerekir.

Elimizde bir işlem için farklı algoritmalar olduğunda strateji desenini kullanarak bu algoritmalardan birinin seçilmesi işlemi ile algoritmaların gerçekleştirimi işlemini birbirinden ayırabiliriz. Strateji deseninin kitaptaki tanımı da budur. Tabi bu arada bu algoritmaların imzalarının aynı olması gerektiğini de unutmayalım.

  1. Temmuz 31st, 2012 at 17:22 | #1

    Ayrıntılı ve anlaşılır bir anlatım, çok teşekkürler.

  2. Ali Aksoy
    Kasım 9th, 2012 at 17:20 | #2

    Gerçekten çok anlaşılır anlatmışsınız. Strategy Design Pattern i bir türlü anlayamıyordum.Şimdi anladım.Teşekkürler.

  3. Selman
    Mart 5th, 2013 at 15:22 | #3

    Güzel anlatmışsın dostum, teşekkürler

  4. Cansoy SOYDAN
    Mart 27th, 2013 at 11:11 | #4

    Çok güzel bir anlatım olmuş, teşekkürler.

  5. Mustafa
    Nisan 10th, 2013 at 16:55 | #5

    güzel anlatım, emeginize sağlık…

  6. ahmet
    Nisan 19th, 2013 at 11:34 | #6

    Güzel anlatım olmuş, teşekkürler.

  7. ayhan
    Aralık 13th, 2013 at 22:54 | #7

    Teşekkürler çok iyi bir anlatım

  8. selami
    Şubat 11th, 2014 at 13:10 | #8

    Türkçe içerik içerisindeki en anlaşılır strategy anlatımı.

  9. Mert
    Mayıs 23rd, 2014 at 09:10 | #9

    Dostum güzel anlatmışsın ama gözünden kaçmış,

    Eleman nesnesini oluştururken, iki tipte tür dayatması yapılmış. Hesaplama araçları arttıkça Eleman nesnesi başka farklı değerler alması gerekecek. Bu durumda sistemde kullanılan nesne toptan değiştirilmek gerekecek buda mümkün olmayacak tabi ki.

  10. Bulut Paradise
    Aralık 11th, 2014 at 18:52 | #10

    Buraya İkramiye Hesaplamayı nasıl ekleyeceğiz?

  11. Aralık 11th, 2014 at 21:58 | #11

    @Bulut Paradise
    Ikramiye hesaplama icin IkramiyeHesap isimli bir arayuz olusturup bu arayuzu uygulayan gercek siniflar yazmak lazim. Daha sonra Eleman sinifinda bu yeni arayuzu ekleyip kullanabilirsiniz.

  12. Zörkem
    Kasım 17th, 2015 at 03:03 | #12

    Emeğe sağlık +rep

  13. Zörkem
    Kasım 17th, 2015 at 03:04 | #13

    selami :
    Türkçe içerik içerisindeki en anlaşılır strategy anlatımı.

    Kesinlikle ben böylesini görmedim

  14. Kadir
    Temmuz 21st, 2016 at 16:21 | #14

    Elinize saglik guzel anlatim olmus.
    Ama burada istemcide element nesnesini olustururken calisanin hangi departmanda oldugunu bilme zorunlulugu olmus oluyor ve birde mert arkadasimin dedigi gibi baska metotlar eklendikce istemci tarafinda yine degisiklik yapmak gerekiyor.burada istemci sinifi bagimliligi cok fazla olmus oluyor.Bunun yerine ek olarak bide factory design pattern kullanirsak daha mantikli olabilir.

  15. Ağustos 7th, 2016 at 03:06 | #15

    Çok güzel bir anlatım olmuş, teşekkürler.

  16. M
    Ekim 30th, 2016 at 09:53 | #16

    Anlatım açıklayıcı ve güzel olmuş teşekkürler. Ancak bu desen her ne kadar bagimliligi azaltsa da söyle bir boşluk görüyorum. Yukarıda arkadaş ikramiye hesaplama sormuş. Eğer ikramiye hesaplama -bu örnek icin konuşuyorum- sadece mühendis ve teknisyen icin varsa (çok mantıklı değil ancak öyle varsayalım) eleman nesnesinde ya ikramiye hesabı dahil olmayan ikinci bir metot yazmamız lazım ya da ikramiye almadığı halde yönetici icin de bu nesneyi parametre olarak gönderip bu senaryoyu da ayrı bir metotla ele almamız lazım. Biz de maas hesabı yapıyoruz ve benzer istisnalarla çok sık karşılaştığımız icin bu durumu belirtmek istedim.

  17. Ali PIÇAKCI
    Şubat 15th, 2017 at 02:22 | #17

    Elinize sağlık güzel anlatmışsınız Bazı arkadaşların dediği gibi tip dayatması var bu modelde bence bu sorunuda bazı arkadaşların dediği gibi factory desing ile değilde zaten Eleman nesnesi herhangi bir kaılıtım alan nesne değil. Direk Eleman nesnesinin kendi içinde statik olarak kendini döndüren methotlar yazılsa daha mantıklı olacak gibi geliyor.

  1. No trackbacks yet.

Please leave these two fields as-is:

Protected by Invisible Defender. Showed 403 to 653.790 bad guys.

%d blogcu bunu beğendi: