İçeriğe geç

Gizli Silah: Specification Pattern

Sanırım specification pattern‘ı en son bir buçuk yıl önce implemente etme ihtiyacım olmuştu. Amacım ise ilgili business domain’ini çok fazla kompleks bir hale getirmeden ve domain bilgilerini duplicate etmeden, domain kurallarını encapsulate ederek tekrar kullanılabilir bir hale getirebilmekti.

Bir çoğumuzun bildiği gibi specification pattern, yeni bir pattern değil. Son dönemlerde ise bu pattern hakkında farklı düşünceler ve tartışmalara denk geldim. Böylece bu pattern hakkında bende bir şeyler yazmaya karar verdim. Dürüst olmak gerekirse gerekli gördüğüm noktalarda bu pattern’ı implemente etmek, benim için hala hoş bir yaklaşım.

Bu makale kapsamında ise biraz specification pattern’dan bahsedip, en basit haliyle nasıl implemente edebileceğimizi göstermeye çalışacağım.

Peki, nedir?

Specification pattern için en basit haliyle, istediğimiz domain bilgilerini/kurallarını encapsulate ederek tekrar kullanılabilir parçalar oluşturabilmemize olanak sağlayan bir pattern’dır diyebiliriz.

Böylece uygulama içerisinde aynı domain kuralına ait lambda expressions’ları yaymak yerine, single responsibility prensibine bağlı kalarak ilgili tüm domain kurallarını tek bir noktadan yönetip, tekrar kullanılabilir bir hale getirebilmekteyiz.

Örneğin bir e-ticaret firmasının ürün domain’i içerisinde çalıştığımızı ve sanal stok’lu ürünleri listelemek istediğimizi düşünelim.

Ürün modeli aşağıdaki gibi property’lere sahip olsun.

public class Product
{
    public string Name { get; set; }
    public bool IsVirtualStock { get; set; }
    public bool IsFreeShipping { get; set; }
}

Genelde bu gibi bir işlemi, aşağıdaki gibi bir lambda expression ile basitçe çözebiliriz.

List<Product> products = _dbContext.Products.Where(p => p.IsVirtualStock == true).ToList();

Bu noktaya kadar her şey güzel.

Daha sonra farklı bir noktada ise ürünlerin, sanal stok’lu olup olmadıklarını kontrol etme ihtiyacımızın olduğunu düşünelim. Elbette bu işlemi de aşağıdaki gibi basitçe çözebiliriz.

if(product.IsVirtualStock)
{
    ..
}

Bu örnekte olduğu gibi her yeni bir ihtiyacımızda, ilgili domain kuralını sürekli farklı noktalarda tekrar etmek zorunda kalacağız ve DRY prensibini ihlal edeceğiz. Ayrıca bu domain kuralı, birden fazla gereksinimin bir araya gelmesiyle de oluşabilir.  Bunun gibi bir araya geldiğinde önem kazanan domain kurallarını specification pattern yardımıyla encapsulate edebilir, tek bir noktadan tekrar kullanılabilir bir hale getirebiliriz.

Birden fazla domain kuralının zincirleme olarak bir arada kullanıldığı senaryolarda ise, lambda expression karmaşası meydana gelebiliyor ve okunabilirliği de oldukça düşürebiliyor. Bu durumu da specification pattern yardımıyla tersine çevirebilmek mümkün.

Haydi Kodlayalım!

Öncelikle aşağıdaki gibi “Specification” isimli bir abstract class oluşturalım.

public abstract class Specification<T>
{
    public abstract Expression<Func<T, bool>> Expression();

    public bool IsSatisfiedBy(T entity)
    {
        Func<T, bool> predicate = Expression().Compile();

        return predicate(entity);
    }
}

Gördüğümüz gibi specification pattern temelinde, bir domain model’inin istenilen domain kuralına uyumlu olup olmadığını kontrol edebilmek için “IsSatisfiedBy” isimli bir method yer almaktadır.

Concrete specification class’larında ise istediğimiz domain kurallarını, “Expression” method’u içerisinde encapsulate edeceğiz.

Şimdi sanal stoklu ürün specification’ını aşağıdaki gibi oluşturalım.

public class VirtualStockSpecification : Specification<Product>
{
    public override Expression<Func<Product, bool>> Expression()
    {
        return p => p.IsVirtualStock == true;
    }
}

Hepsi bu kadar.

Oluşturmuş olduğumuz bu specification’ı ister bir query işlemi sırasında, istersek de bir validation işlemi sırasında kullanabiliriz.

Örneğin repository içerisinden geriye bir IQueryable dönmek yerine, aşağıdaki gibi specification kabul edebilir bir hale getirebiliriz.

public class ProductRepository
{
    private readonly DbContext _dbContext;

    public ProductRepository(DbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public IEnumerable<Product> Filter(Specification<Product> specification)
    {
        return _dbContext.Products.Where(specification.Expression()).ToList();
    }
}

Ardından aşağıdaki gibi farklı amaçlarla specification’ı kullanabiliriz.

public class ProductService
{
    private readonly ProductRepository _productRepository;

    public ProductService(ProductRepository productRepository)
    {
        _productRepository = productRepository;
    }

    public List<ProductDTO> GetProducts()
    {
        List<Product> products = _productRepository.Filter(new VirtualStockSpecification()).ToList();

        // ...
    }

    public ProductDTO AnotherMethod(int id)
    {
        Product product = _productRepository.Get(id);

        var virtualStockSpecification = new VirtualStockSpecification();

        if(virtualStockSpecification.IsSatisfiedBy(product))
        {
            // do something
        }

        // ...
    }
}

Yukarıdaki kod bloğundan görebileceğimiz üzere, hem query işlemi için hem de diğer method içerisinde validation işlemi için “VirtualStockSpecification” ı kullandık.

Elbette specification’ın kullanımı sadece bunlardan ibaret değil. Daha farklı ihtiyaçlar için specification’ları zincirleme olarak “AND“, “OR“, “NOT” gibi yeteneklerle birleştirerek kullanabilmekte mümkün.

Bu yaklaşım ise Composite Specification olarak adlandırılıyor. Bununla ilgili bir örneğe ise, buradan erişebilirsiniz.

Sonuç

Her ne kadar bu pattern hakkında farklı fikir ayrılıkları olsada, çoğu zaman benim için hala kullanışlı bir pattern. Specification’ları kullanabilmek için oluşturduğumuz base class’ları, projenin karmaşıklığını arttıran unsurlar olarak görebilirsiniz. Fakat bize kazandırabilecek olduğu tekrar kullanılabilirlik ve test edilebilirliği göz önüne aldığımızda, özellikle söz konusu domain kuralları ise, bence kabul edilebilir bir hale geliyor. Keza projenin maintenance’ı üzerinde de doğrudan bir etkisi olduğuna inanıyorum.

Peki bu konuda sizin düşünceleriniz nedir?

Referanslar

https://en.wikipedia.org/wiki/Specification_pattern
https://stackoverflow.com/questions/9709764/specification-inside-linq-with-ef-4-3

Kategori:.NET.NET CoreArchitecturalTasarım Kalıpları (Design Patterns)

8 Yorum

  1. Ebu Ahmed Ebu Ahmed

    thanks for sharing this

  2. Turkel Turkel

    Eline saglik guzel yazi.

    C# yazıyorsam specification pattern yerine extension method kullanımını tercih ediyorum reusable olacak mantık için. Deneyimlerni merak ediyorum, bu çizgiyi nasıl ayırıyorsun ?

    • Selam, teşekkür ederim yorumunuz için. Bana göre o ayrımı yapma konusu oldukça zor, çünkü aralarında herhangi bir üstünlük görmüyorum açıkcası. Sanırım developer’ın yoğurt yeme biçimine de bağlı diyebiliriz. Dediğiniz gibi bu işlemleri extension method’larla da kolaylıkla gerçekleştirebiliriz. Hatta domain modeller içerisinde getter’larla da gerçekleştirebiliriz eğer high cohesion konusuna takıntılı isek.

  3. Alireza Alireza

    Thank you for this great post

  4. Onur Onur

    Hocam merhabalar,

    Katmanlı mimaride Specification hangi katmanda olmalı? Servis katmanında kullandığımızda data katmanında referans alamıyoruz. Direk data katmanında kullanılması doğru mu? Specification ISpecification’u data katmanına aldığımızda query oluşturabilmek için yine servis katmanında kullanmak gerekiyor. Açıkcası hangi katmanlarda daha uygun olacak belirleyemedim.

    • Merhaba, sorunun cevabı kullanmış olduğunuz mimari tarzına göre değişir. Eğer clean architecture tercih ediyorsanız, specification’lar da business’ın bir parçası olduğu için kesinlikle domain layer’da yer alması gerekmektedir. Clean architecture’ın doğası gereği tüm mimarinin ortasında konumlandığı ve diğer katmanların onu referans aldığından da dolayı, specification’lara data katmanından da erişebilirsiniz.

  5. ali ali

    this is just overkill and certainly over engineering.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Bu site, istenmeyenleri azaltmak için Akismet kullanıyor. Yorum verilerinizin nasıl işlendiği hakkında daha fazla bilgi edinin.