Belirli bir iş mantığını uygulayan kodumuza yeni bir gereksinim eklemek istediğimizde aklımıza gelen ilk yöntem nedir? Genelde bir koşul bloğu açmak ve bu bloğun içine kodlarımızı yazmak olur. Açık-Kapalı prensibine (Open-Closed Principle, OCP) uymayan bu yaklaşım zamanla kodun okunabilirliğini azaltır, geliştirme ve test maliyetlerini yükseltir. Yeni bir gereksinim eklemenin yanı sıra mevcuttakileri kaldırmak da bir hayli zorlaşır.
Bu yazımızda yukarıdaki dezavantajları en aza indirgeyen ve OCP’ye bağlı kalmayı sağlayan yöntemlerden biri olan Kural Tasarım Deseni’nin örnek bir uygulamasını geleneksel yöntemle kodlanmış uygulama ile karşılaştırıyor olacağız.
Aşağıdaki iş kurallarını gerçekleştiren bir indirim hesaplama metodumuz olduğunu varsayalım.
- Öğrenci olan vatandaşlara ek %20 indirim uygulanmalıdır.
- Evli olan vatandaşlara ek %25 indirim uygulanmalıdır.
- Afetzedelere net %50 indirim uygulanmalıdır. (Herhangi bir limit kontrolüne takılmamalıdır.)
- İşlemlerin en sonunda limit kontrolleri yapılmalıdır. (Önce oran sonra tutar kontrolü yapılmalıdır.)
- İndirim oranı en fazla %40 olabilir.
- İndirim tutarı en fazla 10.000 birim olabilir.
Zamanla mevcut iş kurallarında değişiklik yapmak, yeni iş kuralları eklemek gerektiğini düşünelim.
- Öğrencilere yapılan indirimde vatandaşlık kontrolü kaldırılmalıdır.
- Evli olan vatandaşlara -en fazla 5 çocuğa kadar- her bir çocuğu için ek %2 indirim uygulanmalıdır.
Geleneksel Yöntem
Geleneksel yöntemler ile kodlanmış metodumuza yeni değişiklikler uygulandığında kaldırılan ve eklenen kodlar aşağıda belirtilmiştir. Basit bir örnek üzerinden ilerlediğimiz için bu değişiklikleri yapmak çok da sakıncalı görünmeyebilir. Ancak daha karmaşık iş kurallarının olduğu metotlarda daha önce bahsedilen zorluklar bir hayli hissedilir olacaktır.
RulesEngine Kütüphanesi
Yukarıdaki örneği Kural Tasarım Deseni ile gerçekleştirmeden önce bu deseni uygulamayı kolaylaştırmak için yazdığım kütüphaneyi inceleyelim. Yazıyı çok fazla uzatmamak adına bazı kodlara burada yer vermeyeceğim ancak incelemek isterseniz kütüphanenin ve ilgili örneklerin kodlarına github üzerinden ulaşabilirsiniz.
Aşağıdaki sınıf diyagramı kütüphanenin temel çalışma prensibini göstermektedir. Kısaca açıklamak gerekirse IRule arayüzünü uygulayan (implement) kurallar bulunur ve bu kurallar uygun sırada yürütülür.
Kütüphanenin içerdiği dosyalar aşağıdaki gibidir.
- IRule: Her bir kuralın uygulaması gereken temel arayüzdür.
- IRuleRequest, IRuleResponse: Girdi ya da çıktı değerlerini tutacak sınıfların uygulaması gereken arayüzlerdir.
- RuleAttribute: Kuralın yürütme sırasını (ExecutionOrder) ve üst kuralını (ParentRule) ayarlamayı sağlayan bir attribute’dur.
- RuleHelper: Yürütülecek kuralların bulunmasını sağlayan metotlar içeren yardımcı sınıftır.
- RuleExecutor: Orkestra şefliğini üstlenen sınıftır. İlgili kuralların uygun şartlar altında yürütülmesini sağlayan metotlar içerir.
Genel mantığı anlamak adına RuleHelper ve RuleExecutor sınıflarını daha detaylı inceleyelim.
GetRules metodu ilgili derlemelerde TRule kural tipini uygulayan ya da miras alan somut tipleri bulur ve bu tiplerin nesnelerini uygun sırada döner.
- GetConcretesWithAttribute ve HasParameterlessConstructor metotları kütüphane için yazılmış özel extension metotlardır.
Bir kurala RuleAttribute aracılığıyla ParentRule ataması yapılmış ise bu kural alt (çocuk) kural, aksi halde ana kural olarak kabul edilir. GetMainRules metodu ana kuralları, GetChildRulesOf metodu ise parametre olarak verilen kuralın alt kurallarını bulur.
ExecuteRules metodu parametre olarak verilen kuralları sırayla yürütür. İlgili kural ve ona bağlı alt kurallar yürütüldükten sonra bir sonraki kurala geçilir. Kurallar yürütülürken response nesnesindeki CanStopRulesExecution property’si kontrol edilir. Değerin true olması durumunda kural yürütme sonlandırılır.
- CanStopRulesExecution property’si IRuleResponse arayüzünde tanımlanmıştır.
Execute metodunun birden fazla overload metodu bulunmaktadır. Temelde yaptığı şey ilgili derlemelerde ana kuralları bulup ExecuteRules metoduna aktarmaktır.
Kural Deseni Yöntemi
İndirim hesaplama örneğinde ilk versiyon için 5 adet gereksinim vardır. Bunları ayrı ayrı aşağıdaki kural sınıflarında gerçekleştirdiğimizi varsayalım.
- VictimDiscountRule
- StudentDiscountRule
- MarriedDiscountRule
- RateLimitDiscountRule
- AmountLimitDiscountRule
Kurallar tanımlandıktan sonra indirim hesaplayan metotta yapılması gereken tek şey bu kuralları yürütecek metodu çalıştırmak olacaktır.
Şimdi 2. versiyondaki değişiklikleri uygulamak için değiştirilen ya da yeni eklenen kural sınıflarını yakından inceleyelim.
Yukarıda görüldüğü gibi StudentDiscountRule’da değişiklik yapılmıştır. Çocuklara indirim uygulanması için ise ChildDiscountRule adında yeni bir kural sınıfı tanımlanmıştır. Bu kural MarriedDiscountRule kuralına bağlı olan bir alt kuraldır. Burada indirim hesaplaması yapan asıl metotta (Bkz. Şekil 5) bir değişiklik yapılmadığına dikkat çekmek isterim. Kural Tasarım Deseni uygulandığında iş kurallarında istenen herhangi bir değişiklik ana metodu değil kural sınıflarını etkiler. Yeni bir gereksinim için yeni bir kural sınıfının oluşturulması, mevcuttakinin kaldırılması için ilgili kural sınıfının silinmesi yeterli olur.
Özet
Kural Tasarım Deseni ile çok daha anlaşılır ve yönetilebilir bir yapı elde etmiş olduk. Hangi gereksinimin uygulandığını sadece kural sınıfının ismine bakarak bile anlayabiliyoruz. Gereksinimleri iyi bir şekilde kural sınıflarına bölebildiğimizde bakım ve testlerin çok daha kolay olabilmesi, kural sınıfları içinde kurulacak bir loglama yapısının uygulamayı çok daha detaylı takip edilebilir kılması gibi avantajlar da elde edebiliyoruz. Her zaman olmasa bile uygun yerlerde bu deseni kullanmak aklımızın bir köşesinde mutlaka olmalı diye düşünüyorum. Buraya kadar okuduğunuz için teşekkür ederim.
Kaynakça
- https://github.com/furkanisitan/rules-engine
- Open Closed principle and Rule engine design pattern (Erişim Tarihi: 29.04.2024)
- Design Patterns: Learn More about the Rules Engine Pattern (Erişim Tarihi: 29.04.2