Single Responsibility Principle
Tek Sorumluluk ilkesi (Single Responsibility Principle), Tom DeMarco ve Meilir Page-Jones’un çalismalari kaynak alinarak, Robert C. “Uncle Bob” Martin tarafindan tanimlanmistir.
Bu ilkeyi, “Bir sinifi ilerde degistirmek için sadece bir neden olmalidir” olarak tanımlayabiliriz.
Bir class-in sadece bir görevi olmalidir ve bu amaç dâhilinde gelecekte ihtiyaç duyulacak bir degisiklik için degistirilmelidir sadece.
Farklı “nedenler”le ilgili, classta degisiklik yapılabilirse, bu sinifa birden fazla sorumluluk yüklenmiş demektir.
Böyle bir durumda sinifi “nedenler” kadar küçük parçalara (classlara) ayirmak gerekiyor. Elbette bu parçalarin her birinin yine SRP ye uygun oldugundan emin olmamiz gerekiyor.
SRP (Single Responsibility Principle) uymak en temel anlamda;
- Bagimliligi azaltir
- Degisikliklerde sadece degisikligin etkiledigi yapida çalisma yapilacagi ve sadece bu yapi etkilenecegi için hata çikma olasiligi azalir
- Farkli sorumluluklari olan classlar yapiyi daha esnek bir tasarima götürür.
- God Class-ların oluşmasını engeller
Konuyu daha iyi anlamak adina önce SRP'ye uygun olmayan bir örnek hazirlayip sonra SRP'ye nasil uygun hale getirecegimize bakalim.
Senaryo:
Bir oyuncakçi sirketimiz var. Ürünlerimizi, E-Ticaret sitemiz üzerinden ya da dogrudan depodan satiyoruz
Kurallarimiz;
- Web Sitesinden satis yapilmissa;
- Kredi kartindan çekim yapilir.
- Siparis kalemleri miktarlari kadar depodan rezerve edilir.
- Müsteriye bilgilendirme maili atilir
- Depodan satis yapilmissa
- Para nakit olarak alinir
Yapimiz söyle olsun.
public class Cart { public decimal TotalAmount { get; set; } public IEnumerable<OrderItem> Items { get; set; } public string CustomerEmail { get; set; } } public class OrderItem { public string Sku { get; set; } public int Quantity { get; set; } }
Ödeme ile ilgili yapi
public enum PaymentMethod { Cash, CreditCard } public class PaymentDetails { public PaymentMethod PaymentMethod { get; set; } public string CreditCardNumber { get; set; } public string ExpiresMonth { get; set; } public string ExpiresYear { get; set; } public string CardholderName { get; set; } }
ve nihayetinde Order clasi
public class Order { public void Checkout(Cart cart, PaymentDetails paymentDetails, bool notifyCustomer) { if (paymentDetails.PaymentMethod == PaymentMethod.CreditCard) { ChargeCard(paymentDetails, cart); } ReserveInventory(cart); if(notifyCustomer) { NotifyCustomer(cart); } } public void NotifyCustomer(Cart cart) { // müsteriye bilgilendirme maili gönder } public void ReserveInventory(Cart cart) { // Siparis içindeki ürünler kadar depodaki oyuncaklari rezerve et } public void ChargeCard(PaymentDetails paymentDetails, Cart cart) { // Kredi Kartindan çekim yap } }
Burada gördügümüz gibi bir taş ile koca kus sürüsü vurulmak istenmis.
Açiklamak gerekirse önce satis webten yapildiysa (yani kredi karti ile) önce o tahsil ediliyor, ardindan siparis kalemleri rezerve ediliyor.
Son olarakta müsteriye bilgi maili gidiyor.
Peki bu yapidaki hatalar neler?
- Depodan yapilan satislar için rezervasyon yapmaya gerek yoktur çünkü anlik olarak mallar depodan çikacaktir zaten
- Depodan yapilan nakit satislarin kredi karti islemlerine ihtiyaci yoktur
- Depodan yapilan satislar sonucunda müsteriye bilgilendirme maili gitmesine gerek yoktur.
- Order classi bir God Class olmaya dogru gidiyor
İsleri biraz daha gerçek dünyaya uyarlayalim. Depo satislari düsünüldügünden çok olunca orayada bir POS cihazi konusmus ve artik depodan da kredi karti ile satis yapilmis olsun.
Yukarda yaptigimiz yapida saniriyorum ki degismeyen yer kalmayacaktir.
Projemizi SRP ye uyarlayacak olursak. (Bir class-in sadece bir görevi olmalidir ve bu amaç dâhilinde, gelecekte ihtiyaç duyulacak bir degisiklik için degistirilmelidir)
- Online (Webten), Nakit ve Pos ile satis yapabiliriz, ilerde bunlara yeni siparis türleride eklenecegi için öncelikle Order i bir base class haline getirelim.
public abstract class Order { protected readonly Cart _cart; protected Order(Cart cart) { _cart = cart; } public virtual void Checkout() { // log islemleri yap } }
- Webten alinan siparisler için Order base den türeyen OnlineOrder clasimiz olmalidir. Bu yapida, hem kredi karti çekim hem stok Rezerve hemde müsteri bilgilendirme yapilacaktir
public class OnlineOrder : Order { private readonly INotificationService _notificationService; private readonly PaymentDetails _paymentDetails; private readonly IPaymentProcessor _paymentProcessor; private readonly IReservationService _reservationService; public OnlineOrder(Cart cart, PaymentDetails paymentDetails) : base(cart) { _paymentDetails = paymentDetails; _paymentProcessor = new PaymentProcessor(); _reservationService = new ReservationService(); _notificationService = new NotificationService(); } public override void Checkout() { _paymentProcessor.ProcessCreditCard(_paymentDetails, _cart.TotalAmount); _reservationService.ReserveInventory(_cart.Items); _notificationService.NotifyCustomerOrderCreated(_cart); base.Checkout(); } }
- Depoda POS cihazi ile yapilan satislar için bir siparis classi olusturmak gerekir
public class PoSCreditOrder : Order { private readonly PaymentDetails _paymentDetails; private readonly IPaymentProcessor _paymentProcessor; public PoSCreditOrder(Cart cart, PaymentDetails paymentDetails) : base(cart) { _paymentDetails = paymentDetails; _paymentProcessor = new PaymentProcessor(); } public override void Checkout() { _paymentProcessor.ProcessCreditCard(_paymentDetails, _cart.TotalAmount); base.Checkout(); } }
- Depodan yapilan Nakit ödemeler için de yine bir class olusturmak gerekir.
public class PoSCashOrder : Order { public PoSCashOrder(Cart cart) : base(cart) { } }
Interface-ler
public interface IPaymentProcessor { void ProcessCreditCard(PaymentDetails paymentDetails, decimal amount); } internal class PaymentProcessor : IPaymentProcessor { public void ProcessCreditCard(PaymentDetails paymentDetails, decimal amount) { throw new NotImplementedException(); } } public interface IReservationService { void ReserveInventory(IEnumerable<OrderItem> items); } public class ReservationService : IReservationService { public void ReserveInventory(IEnumerable<OrderItem> items) { throw new NotImplementedException(); } } internal interface INotificationService { void NotifyCustomerOrderCreated(Cart cart); } internal class NotificationService : INotificationService { public void NotifyCustomerOrderCreated(Cart cart) { throw new NotImplementedException(); } }
Sonuç:
- Ödeme Islemlerini, Müsteri bilgilendirme ve Stok Rezerve islemleri için Interface-ler yazdik ve bunlari implemente eden kodlar yazdik.
- Siparis islemlerindeki karmasayi giderdik, sistemimizi genislemeye müsait hale getirdik. Gelecekte yeni siparis türleri eklendiginde diger kodlara dokunulmayacak sadece yeni eklenecek Orderin classi eklenecek. Örnegin Kapida Teslim gibi. Sadece Kapida Teslim islemi ile ilgili bir class eklenecek, baska bir yer degistirilmeyecek.
- Order classi God class yapisindan kurtarilmistir.
Farkli sorumluluklari olan küçük classlarla yapiyi daha esnek bir tasarima kavusturuk.
Bununla birlikte Single Responsibility Principle, Open/Closed Principle, Interface Segregation Principle, Separation of Concerns kaliplari da projemize uygulayarak SOLID-e uyumlu hale getirdik.
2016 : memet tayanç