Merhaba arkadaşlar.
Bu makale konumuzda data access layer için Repository Pattern‘i yerine, Command/Query Object Pattern‘inin kullanımı ve faydaları inceliyor olacağız.
Sizlerinde bildiği gibi uzun zamanlardır data access layer’larımız için, vazgeçilmez bir hal almıştır Repository Pattern’i. Peki bunca zamandır kötü tasarımlardan sıyrılabilmek ve bağımlılık yönetimi(dependency management) adına birde SOLID SOLID diye bağırırken ilk prensiplerinden birisi olan Single Responsibility‘e baktığımızda, Repository Pattern’i ne derecede uyuyor? ve zamanla git gide büyümeye ve hantallaşmaya başlıyor.
Aslında konumuz burada Repository Pattern’ini kullanmamız veya kullanmamamız değil. Bu sebepten ötürü kullanmamalıyız da demek yanlış olur. Asıl amaç ise büyüyen bir sisteme sahip olacaksak, daha iyi bir separation of concerns için neler yapılmalıdır. Unutulmamalıdır ki her patterni spesifik bir problemi çözmek için tasarlanmıştır. Var olmayan bir sorunu çözmek için değil.
Command/Query Object Pattern’i Nedir?
Command/Query Object pattern’i, data access’e erişim için kullanılan pattern’lerden birisidir. Bu yaklaşım ile tüm database işlemleri büyük data access god class’ları yerine, Command ve Query’lerden oluşur. Bu sayede her bir class, isolated bir şekilde küçük parçalardan ve Single Responsibility prensibine de uygun oluşmaktadır. Öte yandan uygulama architecture’ımızı dış kaynaklardan veya değişimlerden korumak istiyorsak, bunun en iyi yolu ise kendi abstraction’ımızı oluşturmaktır. Command/Query Object pattern’i ise bunun bir yoludur.
Dilerseniz konuya biraz örnek üzerinden devam edelim. Varsayalım ki aşağıdaki gibi bir repository interface’imiz olsun.
public interface IProductRepository { Product GetProductDetailById(int id); void CreateProduct(Product product); }
Peki bu interface’i implemente edecek olan “ProductRepository” gün geçtikçe ve farklı ihtiyaçlar doğdukça ne hale gelecek? İşte asıl sorun burada başlıyor. Repository pattern’i ile business logic kısmını, data source’dan başarılı bir şekilde hep ayırdık. Fakat bir süre sonra repository’ler, git gide büyüyen kodlar ve method’lar ile bir god class haline geliyorlar. Bunun yanında başka bir dezavantajına da bakmak gerekirse:
public class ProductRepository : IProductRepository { public void CreateProduct(Product product) { //loglama vb. işlemler //return... } public Product GetProductDetailById(int id) { //loglama vb. işlemler //return... } }
Loglama gibi işlemleri gerçekleştirmek ise, duplicate logic’lere sebep olmaktadır. Elbette Template Pattern‘i ile bu sorun aşılabilir ve büyüyen sistemlerdeki bu tarz problemler için ise çözüm Decomposition. Haydi gelin Command/Query Object Pattern’ini implemente ederek bir bakalım.
Öncelikle “ICommand” ve “IQuery” isimlerinde iki adet interface tanımlayalım.
public interface ICommand { void Execute(IDbConnection db); } public interface IQuery<T> { T Execute(IDbConnection dbConnection); }
Tanımlamış olduğumuz interface’ler Execute method’larına sahip ve parametre olarak “System.Data” namespace’i altında bulunan “IDbConnection” interface’ini alıyor. Yukarıda tanımlamış olduğumuz repository interface’inde bulunan method’ları, sırasıyla kodlayalım. Kodlamaya başlamadan önce “Product” entity’sini aşağıdaki gibi tanımlayalım.
public class Product { public int Id { get; set; } public string Name { get; set; } }
“GetProductDetailById” method’u için “ProductDetailByIdQuery” isminde bir class tanımlayalım ve IQuery<T> interface’ini aşağıdaki gibi implemente edelim.
public class ProductDetailByIdQuery : IQuery<Product> { private readonly int _id; public ProductDetailByIdQuery(int id) { _id = id; } public Product Execute(IDbConnection dbConnection) { // Query... return new Product() { Id = _id }; } }
Constructor üzerinden “id” property’sini inject ediyoruz ve “Execute” method’u içerisinde istenen işlemleri gerçekleştiriyoruz. Bu Query sınıfı artık sadece Product’ın Detail bilgisini, “id” property’si ile getirmekle yükümlü. Şimdi “CreateProduct” method’u için ise “CreateProductCommand” isminde bir class oluşturalım ve “ICommand” interface’ini aşağıdaki gibi implemente edelim.
public class CreateProductCommand : ICommand { private readonly Product _product; public CreateProductCommand(Product product) { _product = product; } public void Execute(IDbConnection dbConnection) { // Commands... } }
Query’de olduğu gibi burada da constructor üzerinden create edeceğimiz “Product” entity’sini inject ediyoruz ve ilgili işlemleri “Execute” method’u içerisinde “dbConnection” üzerinden gerçekleştiriyoruz. Artık bu class’da sadece yeni bir Product Create etmek ile yükümlü.
Gördüğümüz gibi her bir işlem küçük parçalardan ve ufak sorumluluklardan oluşmaktadır. Şimdi database işlemlerini daha kolay yönetebilmek için ise “IDbConnection” interface’ini wrap’leyecek bir context oluşturalım.
public interface IFooDatabase { T Execute<T>(IQuery<T> query); void Execute(ICommand command); }
İçerisinde iki farklı “Execute” method’u bulunmaktadır. Birisi query’leri handle ederken diğeri command’ları handle etmektedir.
public class FooDatabase : IFooDatabase { private readonly IDbConnection _dbConnection; public FooDatabase(IDbConnection dbConnection) { _dbConnection = dbConnection; } public T Execute<T>(IQuery<T> query) { return query.Execute(_dbConnection); } public void Execute(ICommand command) { command.Execute(_dbConnection); } }
Wrap’leme işleminide tamamladığımıza göre artık “FooDatabase” context’ini kullanarak, Command/Query Object pattern’inin örnek kullanımına bir bakalım.
public class ProductController { private readonly IFooDatabase _fooDatabase; public ProductController(IFooDatabase fooDatabase) { _fooDatabase = fooDatabase; } public void UpdateProduct(int productId, string productName) { var product = _fooDatabase.Execute(new ProductDetailByIdQuery(productId)); if (product == null) { product = new Product() { Name = productName }; _fooDatabase.Execute(new CreateProductCommand(product)); } } }
“UpdateProduct” method’una dikkat ettiğimizde tüm işlemlerimiz “IFooDatabase” interface’i üzerindeki “Execute” method’u üzerinden ilerlemektedir. “_fooDatabase.Execute(new ProductDetailByIdQuery(productId))” satırı ile Product bilgisini çekebilirken, “_fooDatabase.Execute(new CreateProductCommand(product))” satırı ile de yeni bir Product yaratabilmekteyiz.
Bir makalenin daha sonuna geldik. Burada büyüyen bir sistemde daha iyi bir “separation of concerns” ün ve decomposition’ın nasıl uygulanabileceği yöntemlerinden birini gördük. Hangisini kullanıp kullanmamak kararı, ilgili domain’izin complexity’sine göre sizlere kalmış.
Umarım keyifli bir makale olmuştur.
Takipte kalın.
Merhaba,
Yazı için teşekkürler. Büyük sistemler için ideal görünüyor. Uzun vadeli işlerde dosya sayısının çokluğundan ziyade o dosyaları yönetebilmek de önemli bir konu tabi. Bu anlamda faydalı bir yazı.
Ben size bir soru sorayım, validasyon kurallarını controller içinde mi çalıştırıyorsunuz yoksa yazılan command ve query sınıfları içinde mi çalıştırıyorsunuz?
Son olarak .net’de property incject yok mu, constructer inject’den daha okunaklı oluyor da.
Kolay gelsin.
Hocam merhabalar,
Çalıştığım şirket LOGO kullandığından dolayı işlerimizde genellikle stored procedure kullanıyoruz.
Bu konuyla ilgili bir makale yazmanız mümkün mü ?
Teşekkürler.
Bende şirketi değiştirdim zaten 😀 Bu arada kitabınızı almıştım ancak CD’nizi kaybettim. Konuyla ilgili yapabileceğimiz bir şey var mı ? 🙁
Selamlar Gökhan Bey,
Makale için çok teşekkürler, çok yardımcı oldu. Bu pattern’de UnitOfWork kullanabilir miyiz? Sizce uygun mu? Şimdiye kadar hep repository pattern’de kullanmıştım.
Teşekkürler.
Merhabalar,
Öncelikle güzel makaleniz için teşekkürler. Ben bir önceki arkadaşın , UoW ile ilgili sorusuna ek yapmak istiyorum. Aşağıda yazacağım örnek işlemler tek transaction içinde olmalı ;
Azıcık uzun görünecek ama girdiğiniz zaman çok basitçe anlatmaya çalıştığım, klasik bir süreç var. Okursanız çok sevinirim 🙂
( Örnek Request 1 ) X Command : İlgili işlemi yapıp , değişiklikleri kaydedecek.
(Örnek Request 2) Y Command : İlgili işlemi yapıp değişiklikleri kaydedecek
(Örnek Request 3 ) X ve Y işleminin tek transaction içinde olmasını istiyorum. ikisinden birisi başarısız olduğunda , veritabanında herhangi bir değişiklik olmamasını istiyorum.
Request geldi , x işlemini yapmam gerekiyor. Aaa halihazırda bu işlemi yapan bir operasyonum var onu call edeyim.
Line 1 : x();
Daha sonra Y işlemini yapmam gerekiyor..Bunun için de halihazırda bulunan Y operasyonum var.
Line 2: Y();
Ama Y işlemi hata aldığında x işlemini de geri almam gerekiyor.
Bu problem mikroservislerle çalışırken duruma,yapıya göre farklı yöntemlerle ; iptal süreçlerini başlatma , event sourcing vs birçok yolla çözülebiliyor bildiğiniz gibi.
Hayal kuruyorum..Çok temel bir .net core api projem olduğunu düşünün. Herhangi bir mikroservis vs yaklaşımı kullanmıyorum. Transaction bloklar vs de kullanmak istemiyorum. Orm olarak ef core kullanıyorum. Ve request bazında logiclerim var. Tek bir işlemden sorumlu commandlarım var ve tekrar tekrar kullanmak istiyorum . Bir requestim boyunca , birbirine bağlı şekilde birkaç command çalıştırmam gerekiyor. Ama her command kendi içinde değişiklileri dbye yansıtıyor.Tabi ki bu durumlarda biz farklı yollarla bu sorunu aşıyoruz ama sizin ideal tavsiyeleriniz var mı ?
COMMANDlar request bazında mı yazılmalı sorusu çıkıyor ortaya ?