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.

dao

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.