Specification Pattern Nedir?

Mehmet Can Tas
SDTR
Published in
5 min readJan 17, 2021

--

Domain Driven Design konusunda elbette çok fazla makale var ve hepsinin ortak olarak anlattığı şey kompleks iş süreçlerini ortak bir dil kullanarak (ubiqutious language) çözüme kavuşturmak diyebiliriz en basit ve yalın haliyle. Buradaki kompleks yapılardan kasıtları ise e-ticaret sistemleri ya da ödeme altyapıları gibi sistemler. Aslında DDD özünde bir işin ne olduğundan ziyade nasıl olması gerektiğini anlatıyor diyebiliriz.

DDD konusunda Türkçe kaynak olarak aşağıda sırasıyla Akın Sabri Çam, Avni Ozunlu ve Ali Kizildag’in makalelerine göz atabilirsiniz.

Yabancı kaynakları da tüketmek isteyenler için ilk adres elbette bu yaklaşımın yaratıcısı olan Eric Evans’ın Tackling Complexity in the Heart of Software kitabı ve Vladimir Khrikov’un blog adresini önerebilirim.

Konumuza dönecek olarak olursak Specification pattern ile ilgili özet olarak, belirli bir domain kuralını tek bir birim -specification- olarak soyutlayıp farklı senaryolar için kullanmamıza ve farklı domain kuralları ile birleştirebilmemize olanak sağlayan bir yapı diyebiliriz. Örnek olarak bir senaryo üzerinden ilerleyelim.

Bir e-ticaret sisteminde çalıştığınızda hizmet verdiğiniz departman (marketing ya da finans ekibi vs.) sizden “sevkiyatı gecikmiş siparişlerin listesini görmek istiyorum” diye bir istekte bulunabilir. Geliştirici olarak basit bir şekilde Unshipped durumundaki tüm siparişleri veri tabanından çekerek gösterebilirsiniz. Ancak burada bir eksiklik var. Siparişler kaç gün sevk edilmediğinde “sevkiyatı gecikmiş” olarak değerlendiriliyor? Bu konuda bir bilgimiz yok. Biz bilsek bile ekibe yeni katılan birisi bu bilgiye sahip olmayabilir.

Yukardaki örneği biraz değiştirerek, “sipariş oluşturulma tarihinden 5 gün geçtiği halde sevk edilmemiş siparişleri görmek istiyorum” diye bir istek geldiğinde ve bunun için ShipmentOverdueOrderSpecification isminde bir domain kuralı oluşturduğunuzda herkes için çok daha kolay bir süreç elde etmiş olursunuz. Oluşturduğumuz bu domain kuralı artık ekip içindeki herkes tarafından projenin herhangi bir yerinde kullanılabilir.

Örneğimizi tamamladığımızı düşünebiliriz ancak hala bir eksiklik var. Sipariş Havale/EFT yönetimi ile ödenmiş olabilir ve şirketimizin Havale/EFT ödemeleri için 7 günlük bir bekleme süresi olabilir. Belki de sipariş iptal edilmiş ya da silinmiş de olabilir. Bu durumda sadece ödemesi alınmış ve henüz hazırlanma sürecinde olan siparişlerin içerisinden sevkiyatı gecikmiş siparişleri aramamız gerekiyor. Bu noktada “o zaman ben az önce yarattığım domain kuralı içerisinde bu parametreleri de eklerim” diye düşünebilirsiniz ancak bu da yanlış. ShipmentOverdueOrderSpecification sadece sevkiyatı gecikmiş siparişler için oluşturduğumuz bir kural. Bunun yerine PaidActiveOrderSpecification -isimlendirmelere çok takılmayalım :)- isminde bir domain kuralı daha oluşturup bu iki kuralı birleştirerek kullanabiliriz.

Şimdi geriye yaslanıp baktığımız zaman oldukça açık bir talep, bu talebi karşılayabilmemiz için oluşturulmuş ve tekrar kullanılabilir 2 domain kuralı ve henüz yazılmamış bir feature var :) Şimdi implementasyon kısmına geçelim ve bize gelen bu isteği Specification Pattern kullanarak .Net Core projemizde nasıl gerçekleştirebiliriz ona bakalım.

İlk olarak aşağıdaki gibi bir Order sınıfı oluşturalım.

public class Order
{
public string OrderNumber { get; set; }
public PaymentStatus PaymentStatus { get; set; }
public ShipmentStatus ShipmentStatus { get; set; }
public PaymentType PaymentType { get; set; }
public OrderStatus OrderStatus { get; set; }
public bool Deleted { get; set; }
public DateTime CreatedOn { get; set; }
}

public enum PaymentStatus
{
NotPaid,
Paid,
Error
}
public enum ShipmentStatus
{
NotShipped, // Sevk edilmemiş
Shipped, // Sevk edilmiş
Delivered // Müşteriye sipariş teslimi gerçekleştirilmiş
}

public enum PaymentType
{
CreditCard, // Kredi kartı
CashOnDelivery, // Kapıda Ödeme
CheckMoneyOrder // Havale/EFT
}

public enum OrderStatus
{
Cancelled, // İptal edilmiş
Processing, // Henüz sevk edilmemiş
Completed, // sipariş sevk edildiğinde
Refunded // İade Edilmiş
}

Sınıfımızı oluşturduktan sonra Specification Pattern’in C# implementasyonuna başlayabiliriz. ISpecification isminde bir interface ve Specification isminde implementasyon sınıfını oluşturalım.

public interface ISpecification<T>
{
bool IsSatisfiedBy(T entity);
Expression<Func<T, bool>> ToExpression();
ISpecification<T> And(ISpecification<T> other);
}
public abstract class Specification<T> : ISpecification<T>
{

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

return predicate(entity);
}

public abstract Expression<Func<T, bool>> ToExpression();

public ISpecification<T> And(ISpecification<T> specification)
{
return new AndSpecification<T>(this, specification);
}
}

Burada oluşturduğumuz abstract sınıfı inceleyecek olursak IsSatisfiedBy metodu adından da anlaşılacağı üzere domain modelimizin bizim verdiğimiz kriterlere uygun olup olmadığını kontrol ediyor.

ToExpression metodu ise bizim belirlediğimiz domain kurallarını/bilgilerini soyutlamamızı sağlıyor.

Burada And metodu ise iki farklı domain kuralını birleştirerek kullanmamızı sağlıyor içeriğine baktığımızda ise AndSpecification adında bir sınıf return ettiğini görebilirsiniz. Son olarak bu sınıfı da oluşturalım.

public class AndSpecification<T> : Specification<T>
{
private readonly ISpecification<T> _leftSpecification;
private readonly ISpecification<T> _rightSpecification;

public AndSpecification(ISpecification<T> left, ISpecification<T> right)
{
_rightSpecification = right;
_leftSpecification = left;
}

public override Expression<Func<T, bool>> ToExpression()
{
Expression<Func<T, bool>> leftExpression = _leftSpecification.ToExpression();
Expression<Func<T, bool>> rightExpression = _rightSpecification.ToExpression();

BinaryExpression andExpression = Expression.AndAlso(leftExpression.Body, rightExpression.Body);

return Expression.Lambda<Func<T, bool>>(andExpression, leftExpression.Parameters.Single());
}
}

Burada AndSpecification’ın görevi az önce de bahsettiğimiz gibi verilen domain kurallarını birleştirmek. Elbette sadece And değil aynı zamanda, Or, OrNot, AndNot gibi farklı specification’lar da yaratıp kullanabilirsiniz.

https://en.wikipedia.org/wiki/Specification_pattern#/media/File:Specification_UML.png

Örneğimize geri dönelim. Az önce bahsettiğimiz senaryo için ShipmentOverdueOrderSpecification isminde bir domain kuralından bahsetmiştik. Oluşturacağımız bu specification ile “sipariş oluşturulma tarihinden 5 gün geçtiği halde sevk edilmemiş siparişler” kuralını soyutlayarak projemizde tekrar tekrar kullanabilecek ve bu kural ilerde değiştiğinde kodun her yerinden değil tek bir noktadan değiştirerek hayatımıza devam edebileceğiz.

public class ShipmentOverdueOrderSpecification : Specification<Order>
{
public override Expression<Func<Order, bool>> ToExpression()
{
return order =>
order.ShipmentStatus == ShipmentStatus.NotShipped &&
order.CreatedOn >= DateTime.Now.AddDays(-5);

}
}

Yukarda da gördüğünüz üzere aslında bir specification oluşturmak çok kolay ve zahmetsiz bir işlem. Burada çok anlatılacak bir şey olmadığından geçiyor ve diğer domain kuralımızı oluşturuyorum. PaidActiveOrderSpecification.

public class PaidOrderSpecification : Specification<Order>
{
public override Expression<Func<Order, bool>> ToExpression()
{
return order =>
order.PaymentStatus == PaymentStatus.Paid &&
order.OrderStatus == OrderStatus.Processing &&
order.Deleted == false;
}
}

Bu domain kuralına baktığımızda bizden istenen şeyi tam anlamıyla verdiğini söyleyebiliriz. Ödemesi tamamlanmış, henüz hazırlanıyor sürecinde olan ve silinmemiş siparişler için bir kural oluşturmuş olduk. Son olarak bu iki domain kuralını nasıl kullanabileceğimize bir göz atalım.

public class OrderService : IOrderService
{
private readonly IRepository<Order> _orderRepository;

public OrderService(IRepository<Order> orderRepository)
{
_orderRepository = orderRepository;
}

public List<Order> ShipmentOverdueOrders()
{
var overdue = new ShipmentOverdueOrderSpecification();
var paid = new PaidOrderSpecification();
return _orderRepository
.Where(overdue
.And(paid).ToExpression());
}
}

Günün sonunda Specification Pattern’i özetleyecek olursak;

  • Belirli domain kurallarını soyutlayarak tekrar kullanılabilir ve farklı domain kuralları ile birleştirilebilir bir şekilde tanımlamamıza olanak sağlar
  • DRY (Don’t Repeat Yourself) prensibini uygulamamıza yardımcı olur.
  • Domain kuralı tekrarlarını önler ve bu da az önce bahsettiğimiz DRY prensibine bağlı kalmamızı sağlar.
  • Kompleks iş kurallarını basite indirgeyerek departmanlar arası iletişimi geliştirerek ekip içerisinde daha stabil bir geliştirme ortamı sağlar.
  • Single responsibility’nin (Tek Sorumluluk İlkesi) uygulanmasına ve loosely coupled (gevşek bağlı) kod yazmamıza yardımcı olur.

Kaynaklar :

--

--