Design Patterns — Chain of Responsibility
Nedir?
Sorumluluk Zinciri -Chain of Responsibility (CoR)- tasarım deseni (behavioral design patterns) davranışsal tasarım desenlerinden biridir.
Davranışlar tasarım desenleri; yazılım bileşenlerinin nasıl etkileşimde bulunacağını ve nasıl işbirliği yapacaklarını yöneten şablonlardır. Tasarım deseni olmaları hasebiyle tekrar kullanılabilirlik sağlamış olur ve kodu isterlerin değişimine direnç göstermeyen esnek yapılı bir hale getirir.
UML Diyagram
Sorumluluk zincirini prensibinde bağlı listelerdeki düğüm yapısına benzeyen bir yapı kullandığımızı düşünebiliriz.
Zincirini birden çok halkası olur, bu halkalara(chain) işleyici(handler) denir. İşleyicilerin ortak noktası işleyeceği nesnedir. İşleyicilerin hepsi ortak bir sınıftan türer (bağlı listelerdeki düğüm gibi).
İşleyiciler bir sonraki işleyici sınıfın bilgisini (referansını) tutar (bağlı listelerdeki node.next yapısı gibi) ve bu bilgiyi esnek biçimde güncelemek için bir set methodu kullanır.
Gelen istek oluşturulan sıraya göre işleyicilere (handler) verilir ve eğer süreç bir işleyici tarafından sonlandırılmadıysa bir sonraki düğüm olmayana dek devam eder.
- UML diyagramındaki Handler sınıfı halkalarınızın ortak işlevi olan HandleRequest veya ProgressRequest olarak isimlendirilebilen isteği işleme methoduna sahiptir.
- İşleme methodu soyuttur, sınıfların polimorfik özellik göstermesi için soyut olmalıdır ki her işleyici kendi üstlendiği göreve göre methodu ezebilsin (override)
- Her Handler (işleyici) bir sonraki handler(Successor) bilgisini taşır ve bu değer setSuccessor methodu ile düzenlenebilir.
Yapının esnekliği buradan gelir, isterlere göre zincir değişik şekilde oluşturulabilir ve değişikliğe direnç göstermez.
Esnek bağlı olan bu yapıda zinciri oluşturan halkaların ortak özelliği ise aldıkları yani işleyecekleri istek, uygulamada buna Request diyeceğiz.
Şimdi gerçek hayattan bir örnek verip bir senaryo üzerinde kullandığımız yapıları simule ederek açıklayalım.
Gerçek Hayattan Bir Örnek
Gerçek hayatta bu prensibin karşımıza çıktığı yerler var mı?
Müşteri hizmetlerini aradığınızda sizi işlemleri sıralayarak ve sesli yanıt veya tuşlama ile ilgili kişiye ulaştırdığı senaryoda sorumluluk prensibi benzeri bir yapı işliyor. Çünkü sırayla kontroller sağlanarak eğer zincirin şuanki halkası isteğinizi karşılayabiliyorsa ona yönlendiriliyor eğer karşılayamıyorsa bir sonraki halkaya aktarılıyorsunuz ta ki isteğinizi karşılayabilecek yetkiliye ulaşana kadar.
Gerçek Hayat Senaryosu
.Net teknolojileriyle MVC web uygulaması veya WebApi geliştirdiyseniz Middleware yapısını duymuşsunuzdur. Middleware yapısının arkasında yatan kavram da Sorumluluk Zinciri Prensibidir.
Bir web uygulamasında yetkilendirme olsun, doğrulama olsun, swagger api desteklesin, gelen istekler şu desene göre controller’a yönlendirilsin, view olsun-olmasın gibi tüm kararları ara yazılım denilen “middleware” ler ile düzenleyebiliriz.
Uygulama
Bir console uygulaması oluşturup web uygulamalarındaki middleware işleyişini simüle ederek konuyu daha iyi açıklamaya çalışacağım.
Senaryomuz şöyle: Kullanıcı sisteme istek atarak (request) sipariş oluşturabilir ancak siparişin oluşması için atılan istek bazı şartları sağlamalıdır.
- İstek içerisinde henüz geçerliliğini yitirmemiş ve bir değere sahip token olmalıdır. Bu token bize kullanıcının geçerli olup olmadığı bilgisini verecek. Eğer token’ın bir değeri var ve henüz expired olmamış ise yani hala geçerli ise istek işlenmeye devam edebilir, aksi takdirde kullanıcı doğrulanamadı cevabını dönerek isteği sonlandırmalıdır. Keza diğer işlemlerin uygulanmasının bir anlamı yoktur.
- İstek içerisindeki token’da rollerin bulunduğu bir liste olmalıdır, eğer bu rol listesinde “Customer” kullanıcı tipi varsa istek işlenmeye devam etmelidir aksi takdirde istek işlenişi sonlanmalıdır.
- İstek içerisindeki sipariş verilen üründen yeterli adette olmalıdır. Kodu dallandırıp budaklandırmamak adına burada Order diye bir nesnenin içerisindeki InStock alanına bakacağız eğer true ise stok vardır, false ise stok yoktur ve sipariş doğrulanmadan istek cevabı dönülmelidir.
- Eğer kullanıcı doğrulandı ise ve kullanıcı yetkisi varsa ve sipariş verilen ürün de stokta varsa sipariş işlemi gerçekleştirilmelidir ve cevap dönmelidir. İlgili işlemleri yapmak için Controller sınıfına yönlendirilmesi gerekir.
İstek için oluşturduğumuz class şöyle olabilir:
namespace ChainOfResponsibility.Data
{
public class Request
{
public Token Token { get; set; }
public Order Order { get; set; }
}
public class Order
{
public int Id { get; set; }
public double Price { get; set; }
public bool InStock { get; set; }
}
public class Token
{
public string Value { get; set; }
public string[] Roles { get; set; }
public bool IsExpired { get; set; }
}
}
Token ve Order alanlarının nasıl doldurulacağını uygulama yaparken göreceğiz, nasıl kullanılacağını ise az önceki adımlarda açıklandı.
Bu isteğe bir cevap dönmemiz gerekli. İşlem tamamlandı ise başarılı, yarıda kesildi ise başarısız sonuç dönerek kullanıcıya bir mesaj döndürmeliyiz.
Cevap için oluşturduğumuz sınıf ise Response:
namespace ChainOfResponsibility.Data
{
public class Response
{
public bool Success { get; set; }
public string Message { get; set; }
}
}
Şimdi sorumluluk zinciri yapısı ile isteği işlememiz gerekiyor. Bunun için UML’de de belirtildiği gibi bir Handler sınıfı oluşturuyoruz. Bu abstract bir sınıf olacak çünkü içerisinde kalıtım alan sınıfların ortak kullanacağı ve gövdesi olan bir method barındıracak(setSuccessor) ve bu method sayesinde her işleyicinin (handler) bir sonraki işleyicisinden haberi olmuş olur ve böylece BAĞIMLILIK olmadan bu yapı gerçekleşir.
namespace ChainOfResponsibility.Handlers
{
public abstract class Handler
{
public Handler Successor { get; set; }
public void SetSuccessor(Handler successor)
{
this.Successor = successor;
}
public abstract Response ProcessRequest(Request request);
}
}
Yukarıda verdiğimiz 4 adım için ilgili ConcreteHandler yani Handler soyut sınıfından türetilen somut sınıfları üreteceğiz. Sırasıyla authentication, authorization, validation ve controller olarak ilerleyeceğiz.
Çünkü
- Adımdaki token geçerliliği authentication (kullanıcı doğrulaması) işlemidir.
- Adımdaki rol tabanlı kullanıcı doğrulaması bir authorization (kullanıcı yetkilendirme) işlemidir.
- Adımdaki stok bilgisi kontrolü bir doğrulama (validation) işlemidir. (Fluent validation kullananlar oradaki yapıdan anımsayacaktır)
- Adımda ise tüm şartları sağlayan bir sipariş nesnesi elimizde olacağı için bu nesneyi web simülasyonundaki controller’a vermemiz gerekecek ki ilgili işlemi yapıp başarılı sonuç dönsün.
Authentication Handler
namespace ChainOfResponsibility.Handlers
{
public class AuthenticationHandler : Handler
{
public override Response ProcessRequest(Request request)
{
if (request.Token.IsExpired || string.IsNullOrEmpty(request.Token.Value))
{
return new Response
{
Success = false,
Message = "Kullanıcı doğrulanamadı."
};
}
else
{
return this.Successor.ProcessRequest(request);
}
}
}
}
Authorizaiton Handler
namespace ChainOfResponsibility.Handlers
{
public class AuthorizationHandler : Handler
{
public override Response ProcessRequest(Request request)
{
if (!request.Token.Roles.Contains("Customer"))
{
return new Response
{
Success = false,
Message = "Kullanıcı yetkisi doğrulanamadı"
};
}
else
{
return this.Successor.ProcessRequest(request);
}
}
}
}
Validation Handler
namespace ChainOfResponsibility.Handlers
{
public class ValidationHandler : Handler
{
public override Response ProcessRequest(Request request)
{
if (!request.Order.InStock)
{
return new Response
{
Success = false,
Message = "Sipariş oluşturulamadı, stok yetersiz"
};
}
else
{
return this.Successor.ProcessRequest(request);
}
}
}
}
Controller Handler
namespace ChainOfResponsibility.Handlers
{
public class ControllerHandler : Handler
{
public override Response ProcessRequest(Request request)
{
return new Response
{
Success = true,
Message = "Sipariş başarıyla oluşturuldu"
};
}
}
}
Oluşturduğumuz bu yapıyı Program.cs içerisinde kullanacağız.
Sorumluluk zincirini oluşturalım
using ChainOfResponsibility.Data;
using ChainOfResponsibility.Handlers;
Handler authentication = new AuthenticationHandler();
Handler authorization = new AuthorizationHandler();
Handler validation = new ValidationHandler();
Handler controller = new ControllerHandler();
authentication.SetSuccessor(authorization);
authorization.SetSuccessor(validation);
validation.SetSuccessor(controller);
Kullanıcıdan alınacak isteği oluşturalım
Request request = new Request
{
Token = new Token
{
IsExpired = true,
Roles = new string[] {},
Value = "token"
},
Order = new Order
{
Id = 1,
InStock = false,
Price = 10
}
};
İlgili isteği sorumluluk zincirine vererek cevabını yazdıralım.
Response response = authentication.ProcessRequest(request);
if (response.Success)
Console.ForegroundColor = ConsoleColor.Green;
else
Console.ForegroundColor = ConsoleColor.Red;
System.Console.WriteLine(response.Message);
Console.ResetColor();
Console.ReadLine();
Yukarıda verilen ve kullanıcı isteğini temsil eden request nesnesinde token isExpired alanı true olduğu için token geçerlilik süresini yitirdiğini anlıyoruz. Bu durumda sorumluluk zincirinin ilk halkası olan authentication kısmında başarısız sonuç dönecek ve success false durumunda console color red verildiği için hata mesajı kırmızı yazacak.
Token IsExpired=false yapıp tekrar deneyelim, bu durumda token expire olmadığı için yani geçerli olduğu için ve token value da geçerli bir string olduğu için (bu değerin kontrolü authentication handler’da null veya empty değil olarak yapılıyor)
IsExpired = false,
Bu durumda kullanıcı doğrulanır ancak roles dizisi boş olduğu için yetki hatası verir ve çıktı şöyle olur.
Şimdi roles içerisinde customer rolü ekleyelim ve tekrar deneyelim. Yukarıdaki kullanıcı doğrulaması geçerli olduğu durumda progress handler methodu bir sonraki halkanın progress handler methodunu işletip sonucunu döndürür.
Roles = new string[] {"Customer"},
Bu durumda authorization handler’a gidecek ve roles dizisinde Customer olup olmadığına bakacak. Customer tanımlaması roller içerisinde var olduğu için ise bir sonraki halkaya atlayacak ve stok kontrolüne bakacak.
Şimdi isteğin başarılı sonuç verebilmesi için order içerisindeki InStock alanını true yaparak stok olan bir ürünü sipariş vermek isteyelim.
InStock = true,
Böylece validation handler’dan geçip controller handler’a gelebilsin.
İsteğin son hali controller handler’ kadar ilerleyebildiği için ve son halkada response başarılı döndüğü için Program.cs’deki success == true ise kontrolü sebebiyle konsol rengi yeşil olarak bastı.
Sonuç
Nedir? kısmında söylemiştik: Davranışsal tasarım desenleri durum değişikliğine karşı nasıl ilerleyeceğimizi tasarlamak için kullanılır. Uygulama üzerinde de gelen Request’in değişimiyle kodun nasıl davrandığını görmüş olduk.
Sorumluluk zinciri tasarım desenini kullanarak esnek bağlı yapılar oluşturabiliriz. Böylece proje içindeki kontrollerimizi temiz bir şekilde kodlayabilir, karar değişimlerine esnek cevap veren, if-else satırlarında boğulmayan kodlar yazabiliriz.
Bu konu hakkında daha fazla okuma yapmak için aşağıdaki makalelerden faydalanabilirsiniz.
https://www.dofactory.com/net/chain-of-responsibility-design-pattern
https://refactoring.guru/design-patterns/chain-of-responsibility