Categories: Architectural

Repository Pattern Yaklaşımı Yerine Command/Query Object Pattern Yaklaşımı

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.

Gökhan Gökalp

View Comments

  • 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.

    • Merhaba teşekkür ederim yorumunuz için öncelikle. Validasyon biraz geniş bir kavram açıkcası. Business validasyonları, rule validasyonlar vb. Bu tarz cross-custting işlemler için eğer varsa engine katmanının üzerinde bir manager katmanı orada uygulanabilir veyahut aspect'lerle yönetilebilinir oda olmadı command'lar da da yönetmek gibi bir çok yöntem mevcut. Projenizin structure'ına göre değişir. Property injection tabi ki bulunmakta ve bu konuda okunurluğa göre de davranılmamalıdır. Dışarıya expose etmek istemediğim bir property'i inject etmek pek de uygun olmayacaktır.
      İyi günler dilerim.

  • 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.

    • Selamlar, öncelikle teşekkür ederim yorumunuz için. SP'ler pek ilgi alanım değil açıkcası code-review'lar haricinde. :)

      • Merhaba Usta
        Bu konularda takıntılıyım, şuan sorumlu olduğum projemi akıla mantığa en uygun şekilde refactor ediyorum. Projemde klasör isimlerinden hangi classın ne tür bir standart yapıyla dizilimesine kadar kusursuza yakın tasarım desenleri araştırırken gördüm makaleyi. Ben repostory paterni daha kullanışlı buldum açıkcası bu da fena değil ama bu sefer class dosyaları gözümü yoracak. Şuan refactore gitmemdeki en büyük neden DTO kullanmamış olmam. DTO(auto map edecek) ve asenkron destekli bir repository patern kurgulayıp işi ilerletmeyi düşünüyorum. Validasyon, log, hata yönetimi gibi işlemler için postsharp kullanmayı düşünüyorum. Hangi orm kullanıcağıma henüz karar veremedim. Bu ihtiyaçlarımı düşünerek örnek bir proje arıyorum ve yorumlarını bekliyorum.

        • Merhaba,

          Refactor etmeniz, etmeye takıntılı olmanız çok güzel. :) Açıkcası ben kusursuz bir tasarım diye bir şeye inanmıyorum. Fakat bu noktada Repository ile Command/Query'i karıştırmamak gerek. Burada gözünüzü yoracak class çokluklarından ziyade, buradaki seperation'ın amacına, elinizdeki probleme doğru odaklanmak gerek. Cross-cutting işlemler için ise aspect'ler güzel tercih. Hangi orm demişsiniz birde, EF bir çok derdinizi çözecektir. Bu arada Command/Query'deki diğer bir güzel yön ise, querying işlemlerini (örnek misali) Dapper tarzı micro orm'ler ile yaparken, diğer işlemler için EF nimetlerinden yararlanabilmek gibi işlemler de güzel handle edilebilmektedir. Dediğim gibi, probleme ve seperation'a iyi bakmak, düşünmek gerek. :) Göz yormaktan öte bir olay.

  • Bende şirketi değiştirdim zaten :D 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.

    • Merhaba, teşekkür ederim yorumunuz için. Kafamda canlandıramadığım için senaryoyu ne desem yalan. :) Command'ları, iyi define edilmiş one-shot'lık function'lar olarak düşünürsek, neden UoW ihtiyacınız var? Bu soruyaa cevap aramak lazım önce.

  • 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 ?

    • Selam, bu konunun çözümü sanırım ilgili business domain'ine ve ihtiyacına/developer'ın yoğurt yeme tarzına göre değişebilir diyebilirim. Genelde simple işlemler için request bazında command/query'lerimi call etmeyi tercih ederim. Fakat arada birbirine bağlı ve atomic olması gereken işlemler varsa, araya bir handler koyarım ve oradan ilgili işlemlerimi gerçekleştiririm. Iptal süreçlerini başlatma işlemi için microservice kullanmanıza gerek yok, local event'lerle de non-blocking olarak bu tarz işlemleri gerçekleştirebiliriz. Olabildiğince tek bir transaction ile çoklu işlem tercih etmemeye çalışıyorum. Bir çok zaman concurrency problemleri doğuracaktır.

Recent Posts

Securing the Supply Chain of Containerized Applications to Reduce Security Risks (Policy Enforcement-Automated Governance with OPA Gatekeeper and Ratify) – Part 2

{:tr} Makalenin ilk bölümünde, Software Supply Chain güvenliğinin öneminden ve containerized uygulamaların güvenlik risklerini azaltabilmek…

6 months ago

Securing the Supply Chain of Containerized Applications to Reduce Security Risks (Security Scanning, SBOMs, Signing&Verifying Artifacts) – Part 1

{:tr}Bildiğimiz gibi modern yazılım geliştirme ortamında containerization'ın benimsenmesi, uygulamaların oluşturulma ve dağıtılma şekillerini oldukça değiştirdi.…

8 months ago

Delegating Identity & Access Management to Azure AD B2C and Integrating with .NET

{:tr}Bildiğimiz gibi bir ürün geliştirirken olabildiğince farklı cloud çözümlerinden faydalanmak, harcanacak zaman ve karmaşıklığın yanı…

1 year ago

How to Order Events in Microservices by Using Azure Service Bus (FIFO Consumers)

{:tr}Bazen bazı senaryolar vardır karmaşıklığını veya eksi yanlarını bildiğimiz halde implemente etmekten kaçamadığımız veya implemente…

2 years ago

Providing Atomicity for Eventual Consistency with Outbox Pattern in .NET Microservices

{:tr}Bildiğimiz gibi microservice architecture'ına adapte olmanın bir çok artı noktası olduğu gibi, maalesef getirdiği bazı…

2 years ago

Building Microservices by Using Dapr and .NET with Minimum Effort – 02 (Azure Container Apps)

{:tr}Bir önceki makale serisinde Dapr projesinden ve faydalarından bahsedip, local ortamda self-hosted mode olarak .NET…

2 years ago