Unit Testleri ile Chain Of Responsibility Pattern Uygulaması ✨

Alperen Uzun
Wingie / Enuygun Tech
4 min readFeb 17, 2021

Bir fonksiyon içerisinde yazılan onlarca satır, farklı sorumlulukların bir ortamda toplanması sonucu oluşan karmaşalar, iyi planlanmadan yapılan projeler vb. durumlara şahit olmuşuzdur. Bu yazıda, karmaşayı ortadan kaldıran, sorumlulukların net olduğu ve akışı bir yerden kontrol edebileceğimiz bir design pattern inceleyeceğiz.

daywalk.com

Chain of Responsibility pattern’ini bağlı listelere(Linked List) benzetebiliriz. Uygulamamız içerisinde çeşitli sorumluluklar var ve bunları bir zincirin halkaları gibi düşünebiliriz. Bu yaklaşımda her bir halka birbirini takip eder, her birinin sorumlulukları bellidir. Bir koşula bağlı olarak ilgili halka eğer koşulu sağlıyorsa çalışır ve bir sonraki halkaya geçilir. Böylece zincir tamamlanarak sonuca ulaşılır.

Hangi türlerde kullanabiliriz?

  • Uygulamamızda çeşitli türleri olan bir request handle ediyorsak (Yetkilendirme sistemleri, filtreleme)
  • Uygulamamızın herhangi bir aşamasında birbirini takip eden farklı sorumluluklar içeren adımları mevcut ise (ardışık çalışacak olan)

Teknik Açıklama

Chain Of Responsibility Pattern

Yukarıdaki fotoğraftan görüldüğü gibi en basit haliyle, chain’i oluşturan handler’larımız mevcut ve her bir handler farklı görevlerden sorumlu. Ayrıca her bir handler’ın çalışması için bir koşul mevcut. Handler’lar abstract bir class’ı extend ederler. Her bir handler’ın bir sonraki handler bilgisini belirterek zinciri oluşturuyoruz ve zincirin akışı belirgin hale gelmiş oluyor.

Handler’ların çalışması için koşullar belirtebiliyoruz ve bu koşullar sağlanmadığı takdirde ilgili chain çalışmayacaktır. Her bir handler’ın işini yapacağı fonksiyon ismi de aynıdır. Böylece, uygulamamız kolayca erişilebilir ve hızlıca müdahale edilebilir bir hale gelmiş olur.

İşlerimizi parçalara ayırdık, sorumluluklar netleşti ve şimdi de bu zincir içerisinde verilerin iletişimini sağlamalıyız. Onu da bütün handler’lara sunduğumuz bir obje parametresi ile yapabiliriz. Bir token oluşturup başlangıçtaki handler’a verdiğimizde en son handler’a kadar hepsi işine yarayan kısmını alıp kullanacak, kendi işini de tamamlayacak ve ilgili parametreleri güncelleyerek bilgiyi taşımış olacaklar.

Problem

Cache kontrolü > Arama > Filtreleme aşamalarını içeren bir uygulamamız olduğunu düşünelim. Burada temel olarak bir arama yapılacak ve filtreleme işlemi de yapılarak sonuçlar gösterilecek. Eğer arama sonucunu cache içerisinde bulursak cache sonucunu, bulamazsak veritabanından gelen arama sonuçlarını göstereceğiz.

Uygulama

CoR uygulamasında abstract bir handler class aracılığıyla her bir chain için handle edilmesi, işlenme koşulu ve işlenmesi durumlarını barındıracağız. Daha sonra arama aşamasındaki her bir adım için bu abstract handler class’ı extend eden handler’larımız ve bunlar arasında iletişimi kuracak olan bir DTO olacak. Son olarak manager class’ımız aracılığıyla chain’in akışını belirleyerek arama işlemini başlatacağız.

Ayrıca, bu akışı test edeceğimiz unit test’ler yazacağız. Hatta, son kısımda CoR nasıl daha işlevsel hale getirebileceğimizi inceleyeceğiz. 🌟

  1. SearchParameter DTO 🎫

Chain akışında chain’ler arasındaki iletişimi, arama işleminde kullanacağımız parametreleri içeren SearchParameter DTO(Data Transfer Object) ile yapacağız. Buradaki içerik, yazılımı tasarlayan kişiye bağlıdır. Bazı özellikleri doğrudan property olarak tanımlanabilir veya çok fazla özellik varsa bir array tipindeki property ile birçok özellik de barındırılabilir.

Not: Tasarımınıza göre bu parametreler interface ile bağlanabilir.

Chain Handler’ların iletişimi için Data Transfer Object

2) Abstract Handler 🔧

Bütün handler’ların extend edeceği bir abstract handler oluşturalım. Burada;

  • nextHandler: İlgili handler işini tamamladıktan sonra çalışacak olan diğer handler.
  • isProcessable: Handler’ın çalışıp çalışmayacağının belirlendiği fonksiyon.
  • process: Handler’ın kendi işini yapacağı asıl fonksiyon.
  • handle: Process’i başlatan fonksiyondur. Eğer koşul sağlanıyorsa çalış, sonraki handler’ı mevcutsa onu handle et şeklinde çalışır.

Not: Handle fonksiyonu interface ile tanımlanıp abstract class’ta implement edilebilir.

3) Handlers (Tasarlanan akıştaki her bir adım) 🚥

Arama işlemi için belirlediğimiz;

  • CacheHandler: Chain’in başlangıcıdır ve cache’te veri varsa sonuçları DTO’ya set edecek, yoksa işlem devam edecek.
  • SearchHandler: CacheHandler sonucunda eğer cache sonucu yoksa bu handler çalışacak, veritabanından arama sonuçlarını getirecek.
  • FilterHandler: Arama sonrası en az 1 sonuç varsa çalışacak ve filtreleme yapacak.

4) SearchManager (Chain akışı ve arama) 🚆

Handler’ları yönettiğimiz SearchManager oluşturalım. Burada;

  • prepareHandlers: Bütün handler’ların akışını hazırladığımız fonksiyon.
  • search: Arama işlemini gerçekleştirecek olan chain’i başlatır. İşlem bittiğinde ise arama sonuçları ile birlikte bize geri dönmüş olacak.

Yorum satırındaki “@required” bu fonksiyonun otomatik olarak bir kere çalışmasını sağlar. Daha fazlası için “Php annotations” inceleyebilirsiniz.

Unit Testini Nasıl Yazarız? 💭

CoR pattern’ı için handler’ların testleri, onların hangi koşullarda process olduğu, exception durumları gibi testler yazılabilir fakat burada sadece chain akışını 2 durumda test edeceğiz. Bunlar;

  1. testItShouldHandleSearchChainWhenCacheExists: Cache sonucunda elimizde data var ise akış; CacheHandler > FilterHandler olmalı.
  2. testItShouldHandleSearchChainWhenCacheDoesNotExist: Cache sonucu boş ise akış; CacheHandler > SearchHandler > FilterHandler olmalı.

Test içerisinde kullanılan fonksiyonlardan bahsedecek olursak;

  • setUp: Test class’ında ilk çalışacak fonksiyondur, diğer test fonksiyonları bu fonksiyon sonrasında çalışacak. Bu fonksiyon içerisinde mock objelerimizi oluşturduk ve her bir testte onları kullandık.
  • expects: İlgili metodun kaç kere çağrılması gerektiği test edilir. (once, never)

Exception Handler Eklemek 💥

Buraya kadar geldiyseniz eğer, hadi gelin de CoR pattern’imize azıcık işlevsellik katalım.

Senaryo: Diyelim ki chain akışında hata durumları ile karşılaşıyoruz ve hata sonucunda log saklama vb. birçok işlem gerçekleştiriyoruz. Bunları da anlamlı hale getirmeliyiz.

Çözüm: Her bir hata durumunu ayrı bir handler olarak ele alabiliriz. 🚀

Bunun için;

  • SearchParameter içerisine son alınan hata propety’sini ekleyelim.
  • AbstractSearchHandler içerisinde try catch bloğu ile hatayı handle edelim ve ayrıca exceptionHandler property’si tanımlayarak catch bloğunda ilgili hatayı ona set edelim.
  • Bir exception class oluşturalım.
  • Bu exception’ı handle edecek bir chain handler oluşturalım ve ilgili exception alındığında çalışacak şekilde tanımlayalım.
  • SearchManager içerisinde cacheHandler’ın exceptionHandler’ını cacheExceptionHandler ile set edelim.
  • Son olarak, hatayı gözlemlemek için cacheHandler içinde hatayı throw edelim. 👇

Bu örnek gibi birçok işlevsel özellikler eklenebilir. Bundan sonrası, uygulama içeriğinize ve hayal gücünüze bağlı. 👊

Not: Exception durumlarınının unit testlerini yazmayı deneyebilirsiniz.

Bize Ne Faydası Oldu?

  • Requestleri handle ederken istediğimiz gibi sırasını değiştirebiliriz.
  • Her bir handler belirli bir görevden sorumludur. (Single Responsibility)
  • Yeni bir handler eklemek veya var olan handler’lardan herhangi birini çıkarmak istersek uygulama akışı bozulmaz. (Open/Closed Principle)

Bu yazıda, Chain Of Responsibility Pattern’ini inceledik, unit testinin nasıl yazılacağına değindik ve biraz daha işlevsel hale getirebilmek için önerilerde bulunduk. Daha güzel ve daha temiz kodlarla görüşmek dileğiyle. 🏃

Wingie / Enuygun’un büyüyen ekibinin bir parçası olmak ister misin? Açık pozisyonlarımızı LinkedIn sayfamızda bulabilirsin. Tech ekibimize başvurmak için CV’ni kariyer@enuygun.com’a iletebilirsin.

--

--