C# State Design Pattern(State Tasarım Deseni)
Merhaba,
Bu içeriğimizde Davranışsal Tasarım Kalıplarından(Behavioral Patterns) olan State Tasarım Desenini(State Design Pattern) tam teferruatlı inceliyor olacağız. State pattern nedir? Genellikle hangi tarz senaryolarda ne amaçla kullanılmaktadır? Hangi tasarım desenleriyle benzerlik göstermektedir? vs. gibi sorular eşliğinde konumuzu irdeleyecek ve hem teorik hem de pratik örnekler üzerinden pekiştirmelerimizi gerçekleştirip içeriğimizi zenginleştireceğiz. O halde fazla vakit kaybetmeksizin buyrun başlayalım 🙂
State Design Pattern Nedir?
State Design Pattern, bir nesnenin o anki durumuna bağlı olarak çalışma zamanında(runtime) davranışını tamamen değiştirmesini sağlayan bir tasarım desenidir. Bu desen sayesinde herhangi bir davranışı herhangi bir duruma bağımlı hale getirip yönetilebilir kıvama getirebilmekteyiz. Bunu daha net anlayabilmek için şöyle bir örnek üzerinden metafor yapabiliriz: Misal, televizyon kumandasındaki açılış ve kapanış tuşunun aynı tuş olmasına rağmen televizyonun açık yahut kapalı olması durumlarına göre farklı davranışlar sergilemesini düşünebiliriz. Bu tuşa basıldığında televizyon açıksa eğer kapatılacak yok eğer kapalıysa açılacaktır. Haliyle buradaki açılma yahut kapatılma davranışları televizyonun açık ya da kapalı olma durumlarına bağımlılık sergilemektedirler.
Bir başka örnek vermemiz gerekirse eğer şu gençliğimizde kullandığımız MP3 çalarlardır? Hani tek bir oynatma tuşu olan ve o tuşu o anda müziğin çalıp çalmaması durumuna göre müziği ya başlatan ya da durduran… Heh işte, o tuştaki müziği başlatma ya da durdurma davranışları MP3’te ki müziğin çalma ya da çalmama durumlarına bağımlılık göstermekte değil midir? Öyledir dimi…
Konuyu teoride daha da zenginleştirebilmek için günlük hayattan bir misal daha vermek istiyorum. Bu misal de her ay düzenli bir şekilde koşarak uğradığımız ATM’lerdeki kart durumudur. ATM, o anda aktif bir kart takılı değilse kart alabilmekte yok eğer kart takılı değilse başka bir kart alamamaktadır. İşte burada da ATM’ye kartın takılı olup olmama durumlarına istinaden kart alabilme ya da alamama davranışları bağımlılık göstergesidir.
Uzun lafın kısası, bir nesnenin herhangi bir anda uygulama içerisinde sınırlı sayıda farklı durumları olabilmektedir. Bu durumlar yukarıda verilen örneklerde olduğu gibi; bir televizyonun açık ya da kapalı olması, bir MP3’ün müzik çalıyor ya da çalmıyor olması ya da ATM’ye bir kartın takılı olup olmaması gibi o uygulamanın da verdiği hizmete ve işlevselliğine göre özel durumlar olabilir. Ve bu durumlar herhangi bir t anında değişkenlik gösterdiği taktirde(ki gösterebilir) değişiklik gösterilen duruma bağlı olarak uygulama davranışı değiştirmek istenebilir.
İşte bu isteği tekrar kullanılabilirlik ve okunabilirlik açısından en verimli şekilde State Design Pattern ile gerçekleştirebiliriz.
Neden State Design Pattern Kullanılmalıdır?
Yazılımlardaki durumlara göre davranış değişiklikleri genellikle if-else
ya da switch case
gibi kontrol blokları eşliğinde gerçekleştirilmektedir. Evet, bu hızlı ve kolay bir çözümdür lakin uzun vadede gelen ihtiyaçlar doğrultusunda ne kadar kullanışsız ve bağlayıcı bir yöntem olduğu görülebilmektedir. Nihayetinde kontrol blokları kod içerisinde öngörülebilir koşullar sunmakta lakin irticalen gelişen ihtiyaçlar doğrultusunda tekrar kodun geliştirilmesini gerektirerek koda bağımlı bir yaklaşım gerektirmektedirler. Bu konuyu yazımızın sonraki satırlarında örneklendirmeyle daha da açıyor olacağız. Şimdilik State Design Pattern’ı, kodu yoğun if-else
/switch case
yapılarına boğmaksızın davranışları yönetebilmek ve yeni durumlarla birlikte olabilecek davranışları/ihtiyaçları hızlıca akışa/algoritmaya/işlevlere ekleyip yeri geldiğinde de çıkarabilmek için kullanılan bir stratejik tasarım olduğunu bilmemiz yeterlidir.
Buna yukarıda bahsettiğimiz MP3 örneğini kod üzerinden örnek olarak if-else
ya da switch case
yapıları ile vermemiz gayet açıklayıcı olacağı kanaatindeyim. MP3 örneğinin durumlarına istinaden davranış detayları neydi hatırlayalım…
Bu senaryonun koda aktarılmış hali izafi olarak aşağıdaki gibi olacaktır;
static string mp3State = "Playing"; public static void PlayButton() { if (mp3State == "Playing") { Console.WriteLine("Müzik durduruldu!"); mp3State = "Stoping"; } else if (mp3State == "Stoping") { Console.WriteLine("Müzik başlatılıyor!"); mp3State = "Playing"; } }
Ya hoca if-else
ile ne güzel hallediyoruz işte ne gerek var kurcalamaya! dediğinizi duyar gibiyim 🙂 Kısmen haklısınız. Lakin yukarılarda bahsettiğim gibi burada condition’lar arttıkça if-else
kontrolündeki yönetilebilirlik bir yerden sonra arap saçından hallice olmaya başlamaktadır 🙂 Misal elimizdeki MP3’ü daha da geliştirdiğimizi varsayalım ve o tuş üzerinde durumları bir nebze daha arttıralım.
static string mp3State = "Playing"; public static void PlayButton() { if (mp3State == "Playing") { Console.WriteLine("Müzik durduruldu!"); mp3State = "Stoping"; } else if (mp3State == "Stoping") { Console.WriteLine("Müzik değiştiriliyor!"); mp3State = "Changing"; } else if(mp3State == "Changing") { Console.WriteLine("Müzik çalınıyor!"); mp3State = "Playing"; } }
Dikkat ederseniz bu seferde MP3’ün tuşuna basıldığında ‘Stoping’ durumuna istinaden müzik değiştirme ve ardından ‘Changing’ durumunda müziği çaldırma/başlatma durumu eklenmiştir. Dikkat ederseniz artık buradaki kod haddinden fazla şişmekte ve daha komplike aksiyonlarda yapılacak operasyonlar neticesinde yönetilebilirlik neredeyse sıfıra doğru meyletmektedir. Bu örneği ayrıyeten sizler switch case
kombinasyonuyla da deneyip gözlemleyebilir ve hatta daha fazla durum karşılığında ne gibi bir zorlanışın söz konusu olduğunu inceleyebilirsiniz.
Ben deniz if-else
ve switch case
bloklarıyla geliştirilen yapılanmaların ne kadar komplike durumlara meydan verdiğini gösterebilmek için bu sefer de switch case
ile farklı bir örnek gerçekleştiriyor olacağım. Bu örneğimizde bir belge uygulaması üzerinden seyrediyor olacak. Bir belgenin(document) ‘Taslak’, ‘Denetleme’ ve ‘Yayınlanma’ olmak üzere üç durumu mevcuttur. Bu belge uygulamasının her bir durumda farklı davranış sergilemesi söz konusu olacaktır.
- Taslak durumunda belge denetlenir.
- Denetleme durumunda belge yayınlanır.
- Yayınlanma durumunda ise herhangi bir işlem gerçekleştirilmez.
Hadi gelin bu senaryoyu da koda aktaralım…
class Document { string state; void Publish() { switch (state) { case "draft": //...belge denetleniyor... state = "moderation"; break; case "moderation": //...belge yayınlanıyor... state = "published"; break; case "published": //...işlem yok... break; } } }
İşte böyle…. Yukarıdaki gibi koşullara dayalı bir durum kontrolünün en büyük zayıflığı belge sınıfına daha fazla durum ve bu durumlara bağlı davranışlar eklemeye başladığımızda kendini gösterecektir. Zaten bunu önceki örneğimizde de görmüştük… Öyle değil mi?
Yani anlayacağınız bu şekilde if-else
ya da switch case
yapılanması durumlara göre korkunç koşullar içerecektir ve bu tarz bir kodun bakımı oldukça zor ve maliyetli olacaktır. Uygulama geliştikçe bu problemler daha da büyüyecek ve tüm olası durumları ve davranışları tasarım aşamasında tahmin etmek oldukça zor olacağı için bu zorluklar kaçınılmaz olarak bizi bekliyor olacaktır.
Velhasıl, bizler artık bu ve buna benzer senaryolarda if-else
ya da switch case
kalıplarına danışmaktansa daha tasarımsal mantığa yatkın olan geliştirilebilir bir stratejiyi benimsiyor olacağız. İşte bu strateji State Design Pattern’ın ta kendisidir…
State Design Pattern Stratejisi
State Design Pattern, bir nesnenin tüm olası durumları için yeni sınıflar oluşturmamızı ve duruma özgü bir şekilde tüm davranışları bu sınıflara çıkarmamızı(yani davranışları bu sınıflarda inşa etmenizi) önerir. Tabi ki de tüm davranışları kendi başına uygulamak yerine, context adında bir nesne üzerinden gerçekleştirerek süreci daha da efektif hale getirir.
Yukarıdaki UML diyagramına göz atarsanız eğer State Design Pattern’ın genel stratejisini görebilirsiniz. Dikkat ederseniz bu stratejide Context
, State
ve Concrete State
olmak üzere üç aktör mevcuttur. Bu aktörlerin kimler olduğunu açıklamamız gerekirse eğer;
- Context
State Design Pattern’ı kullanacak client’lar tarafından kullanılacak olan sınıftır. Duruma özel tüm çalışmaları ve davranışları delege eder. Context sınıfı,Concrete State
nesneleri ileState
arayüzünün referansı aracılığıyla iletişim kurmaktadır. Dolayısıyla mevcut duruma göre davranışı sağlayanConcrete State
nesnesini tuttuğundan dolayı client’lar state nesnelerine doğrudan erişemezler. - State
Senaryoya uygun tüm durumlara özgü davranışların tanımlandığı arayüzdür. Abstract class ya da interface olarak tasarlanır. TümConcrete State
sınıfları için temel(base) olduğu için implemente edilir. Değiştirilebilir işlevselliğe erişebilmek içinContext
nesnesi tarafından kullanılır. - Concrete State
State
arayüzünü uygulayan somut nesnelerdir.Context
nesnesi tarafından kullanılacak olan gerçek işlevsellikleri sağlar. Her Concrete State sınıfı esasında bir durumu ifade edecek şekilde oluşturulur ve o durumu uygulanabilen davranışı sağlar. Ayrıca o anki durumun değişmesine neden olan talimatları da içerebilir. Concrete State nesneleri,Context
nesnesine referans vererek ulaşabilir. Burada önemli olan hemContext
‘in hem deConcrete State
‘in Context’in sonraki durumunu ayarlayabiliyor olmasıdır.
Şimdi ilgili pattern’ın stratejisini teorik olarak izah ettikten sonra sıra birkaç senaryo eşliğinde pratiksel olarak örneklendirmeye gelmiştir.
1. Senaryo
Kullanıcı ATM’ye hesap kartını takacak ve ardından pin değerini girerek belirttiği meblağda parayı çekmek isteyecektir.
Tabi bu süreç aşağıdaki durumlara istinaden gerçekleştirilecektir;
- Eğer ATM’de bir kart takılı değilse herhangi bir işlem yapılmayacak ve ilk olarak kullanıcıdan kartın takılması istenecektir. Kartın takılı olmadığı bu durumu (NoCard) sınıfı ile temsil edeceğiz.
- Kart takıldığı taktirde durum (HasCard)‘a geçecektir.
- Kart takılıyken;
- Kullanıcı kartı çıkarırsa tekrardan mevcut durum (NoCard)‘a geçecektir.
- Kullanıcı başka bir kart takmaya çalışırsa aynı anda birden fazla kart takılamayacağına dair kullanıcı uyarılacaktır.
- Kullanıcı pin giriyorsa doğruluğu kontrol edilecektir. Eğer doğruysa durum (HasPin)‘e çekilecek yok eğer değilse pinin geçersiz olduğu söylenerek kart çıkarılacak ve durum (NoCard)‘a çekilecektir.
- Eğer pin girilmeksizin direkt para çekme talebinde bulunulursa önce pin girilmesi gerektiği bildirilecektir.
- Girilen pin doğruysa eğer;
- Kullanıcı tekrardan pin girmeye çalışıyorsa eğer zaten mevcut bir pin değerinin var olduğu bildirilecektir.
- Eğer kullanıcı para çekme talebinde bulunursa ATM’de ki paraya göre çekim işlemi gerçekleştirilecektir. Çekilmek istenen para ATM’nin o anki bakiyesi tarafından karşılanabiliyorsa çekilebilecek yok eğer karşılanmıyorsa(yani ATM’de ki paradan fazla ise) duruma dair uygun uyarı verilecektir. Her iki durumda da mevcut durum (NoCard)‘a çekilecek ve kart çıkarılarak kullanıcıya teslim edilecektir.
- Eğer ATM’de ki para sıfırlandıysa yahut sıfırın altına düştüyse durum (NoCash)‘e çekilecek ve tüm işlemlerde kullanıcıya ATM’de para olmadığına dair bilgi verilecektir.
UYGULAMADAKİ TÜM DURUMLAR | |
---|---|
NoCard | ATM’ye kartın takılı olmadığı durumu ifade eder. |
HasCard | ATM’ye kartın takılı olduğu durumu ifade eder. |
HasPin | Kullanıcı tarafından doğrulanmış pin değerinin girildiği durumu ifade eder. |
NoCash | ATM’de hiç para kalmadığı durumu ifade eder. |
1. Senaryo Çözüm
Senaryomuza göz atarsanız eğer bu tarz bir çalışmanın if-else
/switch case
yapılanmalarıyla inşa edilebilmesi için ciddi bir koşul patlamasının yaşanabileceğinin sanırım hepiniz farkındasınız… Lakin şimdi uygulayacağımız State Design Pattern ile bu ve buna benzer duruma bağlı farklı davranışların sergilendiği tüm çalışmaları nasıl rahatlıkla yapabildiğimizi hep beraber gözlemleyeceğiz.
Şimdi çözüme ilk olarak uygulamadaki durumlara özgü davranışları temsil edecek olan State
nesnesini oluşturarak başlayalım.
// State abstract class ATMState { public abstract void InsertCard(ATMMachine context); public abstract void EjectCard(ATMMachine context); public abstract void InsertPin(int pin, ATMMachine context); public abstract void RequestCash(int cashToWithdraw, ATMMachine context); }
Yukarıdaki kod bloğuna göz atarsanız eğer ‘ATMState’ isimli abstract class tanımlanmıştır ve içerisinde belirli davranışları sergileyeceğimiz operasyonlar mevcuttur. Bu operasyonları kısaca izah etmemiz gerekirse eğer;
- InsertCard : ATM’ye kartın takılması davranışını gerçekleştirir.
- EjectCard : ATM’den kartın çıkarılması davranışını gerçekleştirir.
- InsertPin : Pin girilmesi davranışını gerçekleştirir.
- RequestCash : Peşin para talep etme davranışı gerçekleştirir.
Ayrıca her bir metot ‘ATMMachine’ türünden context parametresi almaktadır. İşte bu parametredeki nesne bizim uyguladığımız stratejideki Context
nesnesinin ta kendisidir. Birazdan bu nesnenin içeriğini de oluşturduğumuzda göreceksiniz ki, bu ‘ATMState’ nesnesinin referansı üzerinden Concrete State
nesnelerine erişim sağlayıp işlemleri gerçekleştirecektir.
Haliyle şimdi Context
sınıfımıza karşılık gelen ‘ATMMachine’ sınıfını oluşturmamız yerinde olacaktır.
//Context class ATMMachine { ATMState state = new NoCard(); public ATMState State { set => state = value; } /// <summary> /// CashInMachine : ATM'de ki peşin parayı ifade eder. /// </summary> public int CashInMachine { get; set; } = 2000; /// <summary> /// CorrectPinEntered : Pin'in girilip girilmediğini kontrol eder. /// </summary> public bool CorrectPinEntered { get; set; } = false; public void InsertCard() => state.InsertCard(this); public void EjectCard() => state.EjectCard(this); public void InsertPin(int pin) => state.InsertPin(pin, this); public void RequestCash(int cashToWithdraw) => state.RequestCash(cashToWithdraw, this); }
Evet, yukarıda oluşturulan ‘ATMMachine’ sınıfı bizim Context
sınıfımızdır. Dikkat ederseniz bu sınıf içerisinde 4. satırda bir ‘ATMState’ referansı mevcut ve ilk olarak default durumun ‘NoCard’ şeklinde ayarlandığını görüyoruz. Bu referans yukarıda ki paragraflarda da bahsetmeye çalıştığım gibi Context
‘in Concrete State
nesneleriyle iletişim kuracağı bir aracı olacaktır. Ayrıca 13 ile 21. satır aralığındaki metotlara nazar eylerseniz eğer bu metotlar ilgili ATM’nin davranışlarını tetikleyecek olan operatif fonksiyonlardır. Ama burada önemli bir noktaya dikkatinizi çekerim ki, bu metotlar State
nesnesindekilerle birebir aynı isimde olsalar da işlevsel açıdan külliyen farklıdırlar. Misal ‘ATMMachine’de ki ‘InserCard’ metodu kullanıcının ATM’de ki davranışını temsil ederken, bu metodun tetiklediği ‘ATMState’ içerisindeki ‘InsertCard’ metodu ise o anki duruma istinaden gerçekleştirilen yazılımın(ATM’nin) davranışını temsil etmektedir. Bu fark önemli! Tabi bu farkı daha iyi ortaya koyabilmek için ‘ATMMachine’de ki bu metotları farklı şekilde de isimlendirebilirdik 🙂
Ayrıca 9. satırdaki ‘CashInMachine’ property’sinin ATM’de ki bakiyeye karşılık geldiğine ve 13. satırdaki ‘CorrectPinEntered’ property’sinin kullanıcı tarafından girilen pin’in doğrulanıp doğrulanmadığını tuttuğuna dair alanlar olduğunu bilmenizde fayda var.
Tüm bu inşalardan sonra sırada durum nesnelerini yani Concrete State
‘leri oluşturmak vardır.
‘NoCard’;
//Concrete State /// <summary> /// Kartın takılı olmadığı durumu ifade eden sınıftır. /// </summary> class NoCard : ATMState { public override void EjectCard(ATMMachine context) => Console.WriteLine("Lütfen önce kartı takınız."); public override void InsertCard(ATMMachine context) { Console.WriteLine("Lütfen pin giriniz."); context.State = new HasCard(); } public override void InsertPin(int pin, ATMMachine context) => Console.WriteLine("Lütfen önce kartı takınız."); public override void RequestCash(int cashToWithdraw, ATMMachine context) => Console.WriteLine("Lütfen önce kartı takınız."); }
ATM’de kartın olmadığı durumu ifade eden ‘NoCard’ sınıfı kart olmadığından dolayı ‘EjectCard’, ‘InsertPin’ ve ‘RequestCash’ metotlarında önce kartın takılması gerektiğine dair mesaj vermekte ve doğal olarak sadece ‘InsertCard’ metoduyla işlem gerçekleştirerek kart takıldıktan sonra pin giriniz mesajını vererek durumu Context
üzerinden ‘HasCard’a çekmektedir.
‘HasCard’;
//Concrete State /// <summary> /// Kartın takılı olduğu sınıfı ifade eden sınıftır. /// </summary> class HasCard : ATMState { public override void EjectCard(ATMMachine context) { Console.WriteLine("Kart çıkarılmıştır."); context.State = new NoCard(); } public override void InsertCard(ATMMachine context) => Console.WriteLine("Aynı anda birden fazla kart takamazsınız!"); public override void InsertPin(int pin, ATMMachine context) { if (pin == 123) { Console.WriteLine("Pin doğrulandı!"); context.CorrectPinEntered = true; context.State = new HasPin(); } else { Console.WriteLine("Geçersiz pin girildi!"); context.CorrectPinEntered = false; Console.WriteLine("Kart çıkarılmıştır."); context.State = new NoCard(); } } public override void RequestCash(int cashToWithdraw, ATMMachine context) => Console.WriteLine("Lütfen önce pini giriniz."); }
ATM’de kartın takılı olduğu durumu ifade eden ‘HasCard’ sınıfı kart olduğu için ‘EjectCard’ ve ‘InsertPin’ metotlarında sırasıyla kart çıkarma ve pin doğrulama işlemlerini gerçekleştirmektedir. ‘EjectCard’ operasyonu ile kart çıkarıldığında durumu ‘NoCard’a çekmektedir. ‘InsertPin’ operasyonunda ise pinin doğrulanması eşliğinde Context
sınıfındaki ‘CorrectPinEntered’ property’sine ‘true’ değeri verilerek başarılı bir pin değeri girildiği bildirilmekte ve durum ‘HasPin’e çekilmektedir. Yok eğer pin doğrulanmıyorsa ilgili property’e ‘false’ değeri verilmekte ve durum ‘NoCard’a çekilerek kart çıkarılmaktadır. ‘RequestCash’ metodunda ise daha bu durumda ATM’den para çekme işlemi olmayacağına göre pin girilmesi gerektiğine dair kullanıcı uyarılmaktadır.
‘HasPin’;
//Concrete State /// <summary> /// Pin'in olduğu durumu ifade eden sınıftır. /// </summary> class HasPin : ATMState { public override void EjectCard(ATMMachine context) { Console.WriteLine("Kart çıkarılmıştır."); context.State = new NoCard(); } public override void InsertCard(ATMMachine context) => Console.WriteLine("Aynı anda birden fazla kart takamazsınız!"); public override void InsertPin(int pin, ATMMachine context) => Console.WriteLine("Doğrulanmış bir pin zaten girilmiştir."); public override void RequestCash(int cashToWithdraw, ATMMachine context) { if (cashToWithdraw > context.CashInMachine) { Console.WriteLine("Çekmek istenen tutar adil bedeli aşmaktadır."); Console.WriteLine("Kart çıkarılmıştır."); context.State = new NoCard(); } else { Console.WriteLine($"{cashToWithdraw} tutarında para çekilmiştir."); context.CashInMachine -= cashToWithdraw; //ATM'de ki para güncelleniyor. Console.WriteLine("Kart çıkarılmıştır."); context.State = new NoCard(); Console.WriteLine($"ATM'de kalan para : {context.CashInMachine}"); if (context.CashInMachine <= 0) context.State = new NoCash(); } } }
Başarılı bir pin değerinin girildiğini ifade eden ‘HasPin’ sınıfı içerisinde ‘EjectCard’, ‘InsertCard’ ve ‘InsertPin’ metotlarında olması gereken işlemleri gerçekleştirip, gerekli mesajlarla kullanıcıyı uyarmaktayken, esas son aşama olan ‘RequestCash’ metodunda ise para çekim işlemini gerçekleştirmektedir. Burada 17 ile 34. satır aralığına göz atılırsa eğer kullanıcı tarafından çekilmek istenen para(cashToWithdraw) ATM’de ki paradan(CashInMachine) fazla ise kullanıcı uyarılmakta ve kart çıkarılarak durum ‘NoCard’a çekilmektedir. Yok eğer fazla değilse ATM’de ki para, çekilmek istenen miktar kadar düşürülerek güncellenmekte ve gerekli mesajlar verildikten sonra kart çıkarılarak durum yine ‘NoCard’a çekilmektedir.
32. satırda ise ATM’nin mevcut bakiyesi 0 veya 0’ın altında ise o anki durum ‘NoCash’e çekilerek artık ATM’de para kalmadığı ifade edilmektedir.
‘NoCash’;
//Concrete State /// <summary> /// ATM'de paranın 0'a eşit veya küçük olduğu durumları ifade eder. /// </summary> class NoCash : ATMState { public override void EjectCard(ATMMachine context) => Console.WriteLine("Para yok para :)"); public override void InsertCard(ATMMachine context) => Console.WriteLine("Para yok para :)"); public override void InsertPin(int pinEntered, ATMMachine context) => Console.WriteLine("Para yok para :)"); public override void RequestCash(int cashToWithdraw, ATMMachine context) => Console.WriteLine("Para yok para :)"); }
ATM’de hiç paranın kalmadığı bir durumu ifade eden ‘NoCash’ sınıfı içerisindeki tüm aksiyonlarda paranın olmadığına dair gerekli mesajı vermekte ve başka hiçbir işlem gerçekleştirmemektedir.
Dikkat ederseniz tüm Concrete State
nesneleri ‘ATMState’ sınıfından türemektedir. Haliyle Context
sınıfı bu referans üzerinden Concrete State
‘lere erişebilmekte ve işlem gerçekleştirebilmektedir.
Artık bu inşadan sonra tek yapılması gereken Context
sınıfı üzerinden ATM’yi kullanmaktır.
static void Main(string[] args) { ATMMachine atm = new ATMMachine(); #region ATM'ye kart takıp geri çıkarma atm.InsertCard(); atm.EjectCard(); Console.WriteLine("*********"); #endregion #region Doğrulanmış pin ile para çekme ve üzerine tekrar para çekme talebinde bulunma atm.InsertCard(); atm.InsertPin(123); atm.RequestCash(1500); atm.RequestCash(100); Console.WriteLine("*********"); //ATM'de kalan bakiye 500 #endregion #region Para çekme atm.InsertCard(); atm.InsertPin(123); atm.RequestCash(100); Console.WriteLine("*********"); //ATM'de kalan bakiye 400 #endregion #region Para çekme atm.InsertCard(); atm.InsertPin(123); atm.RequestCash(300); Console.WriteLine("*********"); //ATM'de kalan bakiye 100 #endregion #region Para çekme atm.InsertCard(); atm.InsertPin(123); atm.RequestCash(100); Console.WriteLine("*********"); //ATM'de kalan bakiye 0 #endregion #region Para olmayan ATM'den para çekme talebi atm.InsertCard(); atm.InsertPin(123); atm.RequestCash(100); //Tüm işlemlerde para yok uyarısı! #endregion }
Yukarıdaki kodu derleyip çalıştırdığımızda aşağıdaki sonucu almaktayız.
Evet, işte görüldüğü üzere birden fazla condition’ın yoğun bir şekilde uygulanması gereken kompleks bir durumun State Design Pattern ile gerçekleştirilmesi bu kadar basit 🙂 Tamam kabul ediyorum ki her bir duruma karşılık bir nesne üretmek ve bu nesneleri kontrol etmek bazen uzun uzadıya yersiz bir işlem gibi gelebilir lakin emin olun ki bu tarz bir yaklaşım gelişmekte olan ve öngörülemez ihtiyaçlara sahip olan durumlarda hayat kurtarıcı olabilmektedir. Nihayetinde böyle bir operasyonda çözüm olarak bunca sınıf oluşturmaktansa if-else
ya da switch case
kontrolleri ile hızlı bir çözüm getirebilirdik lakin günler belki aylar sonra yeni bir ihtiyaca istinaden ilgili kodu tekrar geliştirmemiz gerektiği taktirde o kodu biz yazmış olsak dahi önce ne yaptığımızı tekrar hatırlamak ve hatta anlamak icap edecek ve ardından yeni ihtiyacı o kodun içerisine hassasiyetle işlememiz gerekecekti. Amma velakin bu şekilde tasarım uygulamamız durumunda yeni ihtiyaca uygun bir davranış modeli oluşturmamız ve o model üzerinden ilgili durumu tarif etmemiz yeterli olacaktır. Sadece tek yapılması gereken ilgili modeli State
nesnesinden türetmektir.
Ayrıca burada her bir durum nesnesini tekrar tekrar üretmenin yersiz olduğu kanaatinde olabilirsiniz. Evet, ben bu senaryoda aynı durumları ifade eden nesneleri dahi yeniden new
operatörü ile oluşturmuş bulunmaktayım. Bu yapıyı nesne üretimi açısından daha az maliyetli bir şekilde geliştirmek istiyorsanız 2. sayfadaki örneği inceleyebilirsiniz.
Velhasıl, bizler artık 2. senaryo ile pratik üzerinden konuyu daha da pekiştirmek için yolumuza devam edelim.
2. Senaryo
UYGULAMADAKİ TÜM DURUMLAR | |
---|---|
PlayMusic | Müziğin çaldığı durumu ifade eder. |
StopMusic | Müziğin durduğu durumu ifade eder. |
2. Senaryo Çözüm
Bu seferki senaryonun çözümünde daha temiz bir inşa gerçekleştireceğiz. (Esasında bir önceki ATM senaryosunun 2. sayfadaki temiz tasarımına benzer bir inşa gerçekleştireceğiz.)
Şimdi ilk olarak State
arayüzünü oluşturarak uygulamadaki durumlara özgü davranışları temsil edelim.
//State abstract class MusicPlayerState { public abstract void PlayMusic(); public abstract void StopMusic(); }
Ardından Context
sınıfımızı aşağıdaki gibi geliştirelim.
//Context class MusicPlayer { MusicPlayerState state; MusicPlayerState play; MusicPlayerState stop; public MusicPlayer() { play = new Play(this); stop = new Stop(this); state = play; } public void SetPlay() => state = play; public void SetStop() => state = stop; public void Play() => state.PlayMusic(); public void Stop() => state.StopMusic(); }
Context
sınıfına göz atarsanız eğer constructor’ında Concrete State
nesnelerini oluşturmakta ve bunları ilgili referanslarda tutmaktadır. Ayriyetten durum değişikliğinde kullanılacak ‘SetPlay’ ve ‘SetStop’ metotlarını barındırmaktadır. Bunların dışında müzik çaların düğmesine basıldığında davranışsal olarak ‘Play’ ve ‘Stop’ fonksiyonları barındırmakta ve bu fonksiyonlar içerisinde o anki duruma istinaden gerekli durum davranışları tetiklenmektedir.
Şimdi sıra Concrete State
sınıflarını oluşturmaya gelmiştir.
‘Play’;
//Concrete State class Play : MusicPlayerState { MusicPlayer _context; public Play(MusicPlayer context) => _context = context; public override void PlayMusic() => Console.WriteLine("Zaten müzik çalmaktadır!"); public override void StopMusic() { Console.WriteLine("Müzik durdurulmuştur."); _context.SetStop(); } }
‘Stop’;
//Concrete State class Stop : MusicPlayerState { MusicPlayer _context; public Stop(MusicPlayer context) => _context = context; public override void PlayMusic() { Console.WriteLine("Müzik başlatılmıştır."); _context.SetPlay(); } public override void StopMusic() => Console.WriteLine("Zaten müzik durmaktadır!"); }
Yukarıdaki Concrete State
sınıflarına göz atarsanız eğer duruma göre beklenen davranışları sergilemektedirler. Artık tasarımı başarıyla tamamlanmış olan bu yapılanmayı aşağıdaki gibi kullanabiliriz.
static void Main(string[] args) { MusicPlayer musicPlayer = new MusicPlayer(); musicPlayer.Play(); musicPlayer.Stop(); Console.WriteLine("*******"); musicPlayer.Stop(); musicPlayer.Play(); Console.WriteLine("*******"); musicPlayer.Stop(); Console.WriteLine("*******"); musicPlayer.Play(); musicPlayer.Stop(); }
Yukarıdaki inşayı derleyip çalıştırdığımızda aşağıdaki çıktıyı verecektir.
İşte bu kadar 🙂
State Design Pattern’ın Diğer Kalıplarla İlişkisi
Strategy Design Pattern | State Design Pattern, Strategy Design Pattern’ın bir uzantısı olarak düşünülebilir. Her ikisi de bazı işleri yardımcı nesnelere devrederek bağlamın davranışını değiştirirler. Strategy, bu nesneleri tamamen bağımsız ve birbirinden habersiz hale getirir. State ise Concrete State ‘ler arasındaki bağımlılıkları kısıtlamaz ve bağlamın durumunu istedikleri gibi değiştirmelerine izin verir. Yani State D.P.’de durumlar birbirlerinin farkındadır ve bir durumdan diğerine geçişler sağlanabilir. Oysa Strategy’de ise davranışlar neredeyse birbirlerini hiç bilmezler! |
Chain of Responsibility Design Pattern | Chain of Responsibility D.P.’da durumlar arasında sıralı bir geçiş söz konusudur. Lakin State tasarımında durum değişikliklerinin sıralı olması zorunlu değildir. Misal, State’de birinci durumdan üçüncü duruma geçilebilir lakin Chain of Responsibility’de birinci durumdan direkt olarak üçüncü duruma geçilemez! |
Uygulanabilirlik
Mevcut durumuna bağlı olarak farklı davranan bir nesne varsa ve bu durumların sayıları göreceli olarak fazlaysa ve duruma özgü kod ve davranış sık sık değişiyorsa State Design Pattern kullanılabilir.
Lehte ve Aleyhte Durumlar
✓ | ꭕ |
---|---|
Tek Sorumluluk Prensibi(Single Responsibility Principle – SRP) Belirli durumlarla ilgili davranışsal kodları ayrı sınıflarda düzenlemeyi öneririr. Böylece tek sorumluluk prensibini desteklemiş olur. |
Bir durum kontrol yapılanması yalnızca birkaç durumdan meydana geliyorsa ve durumları nadiren değişiyorsa kalıbı uygulamak aşırı olabilir. |
Açık Kapalı Prensibi(Open Closed Principle – OCP) Durumlara istinaden mevcut davranış sınıflarını ve Context sınıfını değiştirmeden uygulamaya yeni durumların eklenmesini/tanıtılmasını sağlar. Böylece gelişime açık lakin değişime kapalı bir tasarım meydana gelmiş olur.
|
Nihai olarak;
State Design Pattern, davranışsal tasarım kalıpları içerisindeki en kritik pattern’lardan birisidir diyebiliriz. Nihayetinde bir yazılımın gelişim sürecindeki yaşanabilecek sancıları baştan minimize edebilmek ve yönetilebilirlik açısından doğru temelleri atabilmek için kullanımına dair düşünülmesi ve ihtiyacı görüldüğü noktada kesinlikle devreye sokulup getirilerinden faydalanılması gereken bir stratejidir. Amma velakin unutulmamalıdır ki State Design Pattern’ın kullanılabileceği ideal durum sayısı beş(5) adettir. Yani oluşturulacak Concrete State
sayısı 5’i geçtiği taktirde sınıf patlaması olabilmektedir. Ayrıca bu tasarımda State
nesnelerinin genellikle Singleton olarak tasarlanması performans ve maliyet açısından daha avantajlıdır. Keza bunu 1. senaryoya alternatif olarak sunduğumuz 2. sayfadaki örneğimizde ve 2. senaryonun ta kendisinde görebilirsiniz.
Ha bu arada her makalede olmasa da bu içeriğimizi bir sürpriz ile noktalamayı istiyorum. O da bu içeriğin 3. sayfasında konuya dair zenginleştirme maksatlı farklı bir örneği daha sizlere sunmuş olmamdır 😉
Okuyup, eşlik ettiğiniz için teşekkür ederim.
İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…