API Refactoring : Kaostan Sakinliğe Geçiş -Mediator & CQRS Pattern

Semih Şenvardar
hesapkurdu-development
7 min readSep 9, 2020
source : entrepreneur

API seviyesinde uygulamalar geliştirirken genellikle oldukça sade ve okunabilir olarak hayata başlayan controller’lar, olgunlaşan süreçler ve yeni gereksinimler doğrultusunda epey kompleks yapılar haline gelebiliyorlar.

Bu durum hızlı üretim ihtiyacı,zaman darlığı,kaynak eksikliği vb. durumlar nedeniyle de uzun bir süre ihmal edildiğinde altından kalkılması çok da kolay olmayan technical debt’ler , herkesin kulaç atmaktan özenle kaçındığı refactor havuzları gibi çok da hoş olmayan sonuçlara sebep olabiliyor.

Bu gibi durumlarda “abi çalışıyorsa ellemeyelim,başımıza iş açılmasın” yaklaşımı yerine mevcut fonksiyonaliteyi jenga oyunundaki gibi minik minik dokunuşlarla, çok da başımızı ağrıtmayacak ve test edilebilir hamlelerle belirli bir düzene oturtabilir ve daha anlaşılır hale getirebiliriz. Teorik olarak anlatmaya çalıştığım bu durumu bir örneklem ile pekiştirip kafamızdaki soru işaretlerini giderelim.

Çeşitli hesaplamalar yaptığımız bir API uygulamamız olsun. Geliştirdiğimiz bu uygulama Asp.Net Core tabanlı ve RESTful servisler içeriyor.Zamanla yapılan her geliştirmenin neticesinde elimizde şu şekilde bir controller var.

İçeride ödeme planı,faiz oranı,amortisman hesabı yapan pek çok farklı servis modülü ve ihtiyaca yönelik cache, log ve mapper çözümleri var. İyimser yaklaşımla bu halde olmasına rağmen genelde çok daha fazla bağımlılık içerebiliyor. Anlaşılabilirlik adına örneği biraz daha sade tutma ihtiyacı hissettim. Controllerdaki her API methodu içerisindeki logging & caching kontrolleri, mapping operasyonları ve servis call’lar zaman geçtikçe test edilebilirliği zorlaşan, okunması ve anlaşılması güç kod blokları haline dönüşüveriyor. Hatta çoğu durumda bu controller’lar God-class’lar haline geliyor ki bu da kaçınılması gereken durumlardan biri.

Bu durumu çözebilmek için pek çok farklı yöntem mevcut. Bu aşamada hem hedeflediğimiz refactoring düzeyine ulaşmak hem de SOLID prensiplerini daha efektif uygulayabilmek için yapacağımız dokunuşlardan biri Mediator ve CQRS pattern’lerini beraber uygulamak olacak. İmplementasyondan önce derinlemesine olmasa da kısa ve öz bir biçimde bu patternleri anlamaya çalışalım.

Mediator Pattern

Behavioral design pattern’lerden olan Mediator Pattern, nesneler arası iletişimi mediator nesnesi aracılığıyla soyutlamaktadır(encapsulation). Nesneler birbirleriyle direkt iletişim kuramazlar,aralarındaki iletişim mediator nesnesinin sorumluluğundadır. Bu durum nesneler arası bağımlılığı ve dolayısıyla da coupling’i de azaltmaktadır. Bu tasarım kalıbını kendi metotlarımızla uygulayabileceğimiz gibi, .Net ekosisteminde oldukça popüler olan ve yazarının da (Jimmy Bogard) deyimiyle basit,iddiasız bir mediator implementasyon kütüphanesi olan MediatR’ı da kullanabiliriz.

CQRS (Command Query Responsibility Segregation)

İlk olarak Greg Young tarafından CQS adıyla kullanılan ve onun uzantısı olan CQRS pattern, genel anlamıyla bir metodun bir nesnenin ya durumunu değiştirmesi gerektiğini ya da geriye değer döndürmesi gerektiği tezini savunur.

Command : Bir nesnenin veya sistemin durumunu değiştirendir. Geriye herhangi bir değer döndürmez.

Query : Nesnenin veya sistemin durumunu değiştirmez. Sadece geriye değer döndürür.

Refactor sürecimizde CQRS’e ihtiyaç duymamızın nedeni, Command ve Query ayrımı ile read ve write işlemlerinin yüklerini ayırarak birbirlerinden bağımsız olarak scale olabilmelerini sağlamak. Command’leri ve query’leri ayrı modellerde tutmak consistency konusunda bizlere yeni bir challenge sunabilir. DDD ve Event Sourcing yapılarına uygunluğu ve complex domain’lere olan yatkınlığı da bu pattern’in artı özellikleri olarak bahsedilebilir. (*Basit iş kurallarının işletildiği mimarilerde ve CRUD operasyonların ihtiyaçları karşıladığı bir dünyada kullanımı gereksiz bir complexity yükü getireceği için CQRS kullanımı için detaylı düşünülüp karar verilmesi gerekiyor.*)

Mediator pattern ve CQRS konularında daha detaylı açıklamalar yapmak bizi yazının esas amacından biraz uzaklaştırabilir. Çünkü esas olarak bu iki pattern’i kullanarak nasıl daha efektif test edilebilir ve daha kolay maintain edilebilir API level uygulamalara kavuşuruz bunu açıklamak istiyorum. Bu iki konu hakkında detaylı bilgi için yazının referans kısmına göz atabilirsiniz. Ek olarak CQRS uygulamadan sadece Mediator pattern ile de yapacağımız refactor işlemlerini gerçekleştirebilirsiniz.

Gerçek hayattaki senaryolardan esinlenerek (👼) yukarıda bir controller örneği paylaşmıştım. Şimdi o controller içerisinden örnek bir API metodunu ele alalım.

Bu controller içerisinde iyimser bir durumda bile bu metoda benzer onlarca metot kullanıldığını düşündüğümüzde genel reflekslerden bazıları refactor sürecinden kaçınma, durumu görmezden gelme veya zaten çalışır olduğundan bahsedip süreci kendi haline bırakmaya ikna çabaları oluyor. Ancak biz elimizi taşın altına sokacağız. Aksi takdirde umulmadık taşlar kaşımızı gözümüzü yarmaya başlayacak 😄

stone balance

Önceden yazdığımız API metotları için command’lar ve query’ler oluşturduktan sonra ilk yapmamız gereken, bu command’lar ve query’ler için ayrı mediator handler’larını oluşturmak olacak.

Controller’daki caching, logging ve mapping mekanizmalarını bu Handler’lar içerisinde çağıracağız. Her bir Handler tek bir command’ı veya query’i handle edeceği için Single Responsibility prensibini sağlamış olacağız. Ek olarak mevcut Handler’ları değiştirmeden yeni mediator nesneleri tanımlayabileceğiz. Böylece Open/Closed prensibini de gerçekleştirmiş olacağız. Controller’da oluşturduğumuz mediator nesnesi ile de Handler’lara command’ları ve query’leri göndereceğiz. Handler’lar da bu istekleri işleyecekler ve böylelikle zorlu ama keyifli bir refactoring sürecini tamamlamış olacağız.

İşe ilk olarak Mediator pattern’i uygulamak için kullanacağımız Mediatr kütüphanesini ve dependency injection için kullanacağımız eklenti paketini yükleyerek başlayacağız.

Install-Package MediatR
Install-Package MediatR.Extensions.Microsoft.DependencyInjection

.Net Core ekosisteminde geliştirme yapıyorsanız dotnet cli ile de yükleme işlemlerinizi gerçekleştirebilirsiniz.

dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

Devamında Startup içerisindeki injection ile Mediatr’ı kullanmaya başlayabiliriz.

*Not : Örneğimizde mediator handler’larını API içerisinde tanımlayacağız. Bu nedenle injection için Startup’ı kullandık. Çok katmanlı yapılarda mediator handler’larının tanımlandığı assembly inject edilmelidir. Bu gibi durumlarda typeof metoduna tanımladığınız herhangi bir Handler’ı parametre geçmeniz yeterli olacaktır.

Command’lar ve query’ler’i inherit edeceğimiz abstract sınıfları da şu şekilde tanımlayabiliriz.

Mediatr kütüphanesindeki IRequest nesnesinin geriye değer döndüren ve döndürmeyen iki çeşidi mevcut. Command’lar için geriye değer döndürmeyen tipi ve query’ler için de geriye değer döndüren versiyonunu kullandık. Mediator’ın ileteceği her bir isteğin Handler’lar tarafından işletilebilmesi için tüm command ve query’lerin IRequest kontratını implement etmesi gerekiyor.

Yukarıda örnek olarak verdiğimiz ödeme planı hesaplayan API metodu için öncelikle command/query ayrımını yapmaya çalışalım. Eğer hesaplama işlemi sırasında veritabanında ekleme, güncelleme, silme gibi operasyonlar gerçekleştiriyorsak veya hesaplama tamamlanınca PaymentPlanCalculated gibi bir event fırlatıp state değişikliği yapıyorsak command, veritabanından okunan birtakım değerlerle sadece hesaplama işlemi gerçekleştiriyorsak query şeklinde bir ayrım yapabiliriz. Örneğimizi query olarak gerçekleştireceğiz ancak command’lar da benzer bir yapıda olacaktır.

Query içerisinde servis modülündeki methodun ihtiyaç duyduğu parametreler yer alıyor ve geriye döndüreceği değer de tanımlama da belirtilmiş durumda. Sıra mediator’ın göndereceği bu query’yi handle edecek Handler’ı yazmaya geldi. Oluşturacağımız Handler içerisine controller’daki kontrolleri de taşıyacağız ve o bloktaki işlerin sorumluluğunu Handler’a devredeceğiz.

Bu noktadan sonra geriye kalan tek şey, controller içerisinde yeni oluşturacağımız mediator nesnesi aracılığıyla ilgili API metoduna gelen isteği Handler’a iletmek olacak. Bu refactoring’i controller içerisindeki her bir API metodu için uygulayabilirsek ne kadar lightweight, okunup anlaşılması kolay, geliştirici dostu bir API controller’a kavuşacağımızın fragmanı aşağıdaki kod bloğunda yer alıyor.

Eski halindeki kompleks dependency’ler, farklı ihtiyaçlara yönelik geliştirilen kontrollerin getirdiği okuma zorluğu ve hantal kod blokları yerini, şefliğini mediator nesnesinin yaptığı ruhu dinlendiren bir filarmoni orkestrası dinletisine bırakmış gözüküyor.

before-after refactoring with mediator pattern
before-after refactoring with mediator pattern

Her bir command & query’nin kendilerine özel Handler’lara sahip olmasıyla SRP’ye uygun ve test edilebilirliği kolay modüler yapılara kavuştuk. Farklı controller’larda farklı mediator nesneleri kullanacağımız için anti-pattern omnipotent yapıların yani God Object’lerin oluşumunun da önüne geçmiş olduk. Elimizdeki yapı artık daha kolay maintain edilebilir ve daha az eforla genişletilebilir bir hale geldi.

Peki ya Testler, Testlerimiz ?

Artık API metotlarımızı test etmek için kullandığımız entegrasyon testlerimizin ve servislerimizi test etmek için kullandığımız unit testlerimizin yanına, yeni yapımızda mediator nesnesinin isteklerini işleyen handler’ların da testlerini eklemek gerekiyor. Refactor sürecimizi Open/Closed prensipini dikkate alarak ilerlettiğimiz için, mevcut testlerimizde hiçbir değişiklik yapmamıza gerek yok. Tek yapmamız gereken yeni handler’larımız için yeni unit testlerimizi yazmak. Örneğimizdeki handler için oluşturacağımız test yapısı aşağıdaki gibi olabilir.

Sadelik açısından sadece happy path senaryo içeren bir test örneği paylaştım. Test senaryoları elbette ki gerçek hayatta çok çok daha fazla. Yeni yapımızla birlikte elimizde artık daha loosely coupled modüllerimiz var. Bu esnekliği kullanarak code coverage değerlerimizi çok yükseklere taşıyabiliriz.

Özetlemek Gerekirse

Birim ve entegrasyon testlerinin olmadığı noktalardaki refactor süreçlerinin çok sancılı olabileceği durumlarda mediator pattern & cqrs implementasyonu ile hem Solid prensiplerine uygun hem de bakımı kolay test edilebilir dönüşümler gerçekleştirebiliriz. Buradaki en kritik nokta gereksinim tespitinin doğru yapılması olacaktır. Altyapıdaki karmaşıklığı çözmek için atılan adımlar hedeflenenin aksine mevcut yapıyı daha da karmaşık bir hale getirebilir. Yazının amacından uzaklaşmamak için bazı yerlerde yüzeysel olarak anlatmak zorunda kaldığım kavramlar için detaylı bilgileri yazının referanslar kısmında bulabilirsiniz.

Ekip içerisindeki ihtiyaçlar doğrultusunda yapmış olduğumuz refactoring sürecinde edindiğimiz tecrübeleri elimden geldiğince aktarmaya çalıştım. Bu tür konular ile ilgili destek almak veya bizlerle görüşmek isterseniz dev@hesapkurdu.com adresinden bizimle iletişime geçebilirsiniz.

Yeniden görüşmek üzere.

Saygılarımla.

Referanslar

--

--

Semih Şenvardar
hesapkurdu-development

just some stuff about software development, sharing is caring i guess.