SOLID Prensipleri — Part 2

Batuhan güngör
8 min readJan 24, 2024

--

Herkese selamlar,

Bugün SOLID prensiplerine dair kendi penceremden yaptığım çıkarımlarıma ikinci kısım ile devam ediyor olacağım. Bu yazımda sizlere 3. ve 4. prensipten bahsedeceğim Yani LSP ve ISP.

SOLID :
S — Single Responsibility Principle
O — Open-Closed Principle
L — Liskow Substitution Principle
I — Interface Segregation Principle
D — Dependency Inversion Principle

Liskow Substitution Principle

Bir önceki yazımda da bahsettiğim SOLID prensiplerinin tamamı için verilen tanımlardan çok prensibin savunduğu yaklaşımların benimsenmesi gerektiğini düşünüyorum. Bu kapsamda LSP(Liskow Substitution Principle) için de kendi benimsediğim tanımı aktararak başlamak istiyorum.

LSP temel olarak aslında şunu ifade eder. Bir class türetildiği üst yapıdan(class ya da interface) aldığı özelliklerin tamamını desteklemeli ve bu yapının kullanıldığı her yerde gerekli fonksiyonaliteyi sağlamalıdır. herhangi bir özelliği sağlamaması durumunda uygulama hata alacak ya da yapısı bozulacaktır. Bu biraz Open-closed prensibine benzer ve bence LSP aslında Open-closed prensibinin sağlanması aşamasında uygulamanın bütünlüğünün bozulmamasını amaçlar. Türetilen sınıflar üzerinden geliştirme yaparak uygulamayı genişletebiliriz ancak bu geliştirme uygulamamızı bozmamalıdır.

Önce çok sıkça bulunabilecek örneklerden olan Kuş->(penguen — kartal) örneğini ele alalım. Kuşların uçma ve yürüme yetenekleri vardır. Ancak her kuş bu yeteneklere sahip midir? Mesela penguen uçabilir mi? Uçmasını istediğimizde, sitemi bozar mıyız? :)

Teorik tanımlar ve örnekler üzerinden yeterince ilerlediğimiz düşüncesindeyim. Hadi bu yaklaşımı biraz da gerçek projelerden örnekler ile açıklayalım. Diyelim ki bir ödeme sistemi geliştiriyoruz ve sistemimizde 2 alternatif ödeme yöntemimiz var. Kredi kartı ile ödeme ve ön ödemeli banka kartı ile ödeme. Bilmemiz gereken şudur, banka kartlarına taksit yapamayız. Temelde bu 2 ödeme yöntemimizi de bir base payment method classından türetebiliriz ve böylelikle open closed prensibini sağlarız. Hadi biraz kod yazalım.

Proje yapısı:
Bir API projesi olarak sistemi kodladım. Controller seviyesinde 2 farklı endpointten istekleri alıyorum. Biri kredi kartı ile ödeme yapıyor bir diğeri ise banka kartı ile ödeme alıyor. PaymentManager sınıfımız üzerinden ise ödemeyi gerçekleştiriyoruz.

PaymentController

using API.Business;
using API.Business.Models;
using API.Business.PaymentMethods;
using Microsoft.AspNetCore.Mvc;

namespace API.Controllers
{
[ApiController]
[Route("api/payment")]
public class PaymentController : ControllerBase
{
private readonly PaymentManager _paymentManager;
public PaymentController(PaymentManager paymentManager)
{
_paymentManager = paymentManager;
}

[HttpPost("CreditCard")]
public async Task<IActionResult> PayWithCreditCard([FromBody] PaymentRequest request)
{
var creditCardPayment = new CreditCardPaymentMethod(request.Amount, request.CardInfo, request.InstallmentCount);
var result = request.InstallmentCount > 1
? await _paymentManager.PayAsync(creditCardPayment)
: await _paymentManager.PayWithInstallmentAsync(creditCardPayment);

return Ok(result);
}
}
}

PaymentManager

using API.Business.PaymentMethods;

namespace API.Business
{
public class PaymentManager
{
public async Task<bool> PayAsync(BasePaymentMethod paymentMethod)
{
//Burada bazı işlemler yapılacak.
//orderlaştırma süreçleri için gerekli işlemler yapılabilir
//loglama, yapılabilir
//kullanıcı bazında ödeme limit kontrolleri gibi işlemler yapılabilir
var result = await paymentMethod.PayAsync();

//ödeme başarısına göre gerekli işlemler yapılabilir.
//kullanıcı limitleri güncellenebilir.
//sistemsel eventlar fırlatılarak gerekli dış servislere bilgiler verilebilir

return result;
}


public async Task<bool> PayWithInstallmentAsync(BasePaymentMethod paymentMethod)
{
//Burada bazı işlemler yapılacak.
//orderlaştırma süreçleri için gerekli işlemler yapılabilir
//loglama, yapılabilir
//kullanıcı bazında ödeme limit kontrolleri gibi işlemler yapılabilir
//taksit kontrolleri yapılabilir (Posumuz bu taksite izin veriyor mu?)

var result = await paymentMethod.PayWithInstallmentAsync();

//ödeme başarısına göre gerekli işlemler yapılabilir.
//kullanıcı limitleri güncellenebilir.
//sistemsel eventlar fırlatılarak gerekli dış servislere bilgiler verilebilir

return result;
}
}
}

BasePaymentMethod

namespace API.Business.PaymentMethods
{
public abstract class BasePaymentMethod
{
public BasePaymentMethod(decimal amount)
{
Amount = amount;
}
public decimal Amount { get; set; }
public abstract Task<bool> PayAsync();
public abstract Task<bool> PayWithInstallmentAsync();
}
}

CreditCardPaymentMethod

using API.Business.Models;
using Newtonsoft.Json;

namespace API.Business.PaymentMethods
{
public class CreditCardPaymentMethod : BasePaymentMethod
{
private readonly HttpClient _httpClient;
public readonly CardInfo _cardInfo;
public readonly int _installmentCount;
public CreditCardPaymentMethod(decimal amount, CardInfo cardInfo, int installmentCount = 1) : base(amount)
{
_httpClient = new HttpClient();
_httpClient.BaseAddress = new Uri("http://bankadress.com");
_cardInfo = cardInfo;
_installmentCount = installmentCount;
}

public override async Task<bool> PayAsync()
{
var stringContent = JsonConvert.SerializeObject(_cardInfo);
var result = await _httpClient.PostAsync("/pay", new StringContent(stringContent));
return result.IsSuccessStatusCode;
}

public override async Task<bool> PayWithInstallmentAsync()
{
var request = new
{
CardInfo = _cardInfo,
InstallmentCount = _installmentCount
};
var stringContent = JsonConvert.SerializeObject(request);
var result = await _httpClient.PostAsync("/paywithinstallment", new StringContent(stringContent));
return result.IsSuccessStatusCode;
}
}
}

Gördüğünüz üzere uygulamamız Open-closed yapıda kodladık. Sistemimize yeni bir ödeme yöntemi eklemek istediğimizde, sadece BankPaymentMethod class’ını eklememiz ve gerekli endpointi kodlamamız yeterli olacaktır.

BankCardPaymentMethod

using API.Business.Models;
using Newtonsoft.Json;

namespace API.Business.PaymentMethods
{
public class BankCardPaymentMethod : BasePaymentMethod
{
private readonly HttpClient _httpClient;
public readonly CardInfo _cardInfo;
public BankCardPaymentMethod(decimal amount, CardInfo cardInfo) : base(amount)
{
_httpClient = new HttpClient();
_httpClient.BaseAddress = new Uri("http://bankadress.com");
_cardInfo = cardInfo;
}

public override async Task<bool> PayAsync()
{
var stringContent = JsonConvert.SerializeObject(_cardInfo);
var result = await _httpClient.PostAsync("/pay", new StringContent(stringContent));
return result.IsSuccessStatusCode;
}

public override async Task<bool> PayWithInstallmentAsync()
{
throw new NotImplementedException();
}
}
}

PaymentController

using API.Business;
using API.Business.Models;
using API.Business.PaymentMethods;
using Microsoft.AspNetCore.Mvc;

namespace API.Controllers
{
[ApiController]
[Route("api/payment")]
public class PaymentController : ControllerBase
{
private readonly PaymentManager _paymentManager;
public PaymentController(PaymentManager paymentManager)
{
_paymentManager = paymentManager;
}

[HttpPost("CreditCard")]
public async Task<IActionResult> PayWithCreditCard([FromBody] PaymentRequest request)
{
var creditCardPayment = new CreditCardPaymentMethod(request.Amount, request.CardInfo, request.InstallmentCount);
var result = request.InstallmentCount > 1
? await _paymentManager.PayAsync(creditCardPayment)
: await _paymentManager.PayWithInstallmentAsync(creditCardPayment);

return Ok(result);
}

[HttpPost("BankCard")]
public async Task<IActionResult> PayWithBankCard([FromBody] PaymentRequest request)
{
var bankCardPaymentMethod = new BankCardPaymentMethod(request.Amount, request.CardInfo);
var result = request.InstallmentCount > 1
? await _paymentManager.PayAsync(bankCardPaymentMethod)
: await _paymentManager.PayWithInstallmentAsync(bankCardPaymentMethod);
return Ok(result);
}
}
}

Evet, yeni ödeme yöntemimizi sistemimize entegre ettik. Ancak sizlerin de görebileceği gibi BankCardPaymentMethod’umuz taksit ile ödemeyi desteklemiyor ve hata fırlatıyor. Tam olarak bu durum da LSP’nin söylediği yerine geçebilme ve bu durumda sistemi bozmama prensibini sağlamıyor.

Peki o halde ne yapabiliriz.?
Biz PaymentManager sınıfımızdaki taksitli ödeme metodumuzu temel ödeme metodu sınıfımızla değil de taksitlendirilebilir ödeme yöntemlerini destekleyen bir interface üzerinden çalıştırsak acaba nasıl olur? Hadi kodlayalım.

IInstallmentablePaymentMethod

namespace API.Business.PaymentMethods.Interfaces
{
public interface IInstallmentablePaymentMethod
{
Task<bool> PayWithInstallmentAsync();
}
}

BasePaymentMethod

namespace API.Business.PaymentMethods
{
public abstract class BasePaymentMethod
{
public BasePaymentMethod(decimal amount)
{
Amount = amount;
}
public decimal Amount { get; set; }
public abstract Task<bool> PayAsync();
}
}

CreditCardPaymentMethod

using API.Business.Models;
using API.Business.PaymentMethods.Interfaces;
using Newtonsoft.Json;

namespace API.Business.PaymentMethods
{
public class CreditCardPaymentMethod : BasePaymentMethod, IInstallmentablePaymentMethod
{
private readonly HttpClient _httpClient;
public readonly CardInfo _cardInfo;
public readonly int _installmentCount;
public CreditCardPaymentMethod(decimal amount, CardInfo cardInfo, int installmentCount = 1) : base(amount)
{
_httpClient = new HttpClient();
_httpClient.BaseAddress = new Uri("http://bankadress.com");
_cardInfo = cardInfo;
_installmentCount = installmentCount;
}

public override async Task<bool> PayAsync()
{
var stringContent = JsonConvert.SerializeObject(_cardInfo);
var result = await _httpClient.PostAsync("/pay", new StringContent(stringContent));
return result.IsSuccessStatusCode;
}

public async Task<bool> PayWithInstallmentAsync()
{
var request = new
{
CardInfo = _cardInfo,
InstallmentCount = _installmentCount
};
var stringContent = JsonConvert.SerializeObject(request);
var result = await _httpClient.PostAsync("/paywithinstallment", new StringContent(stringContent));
return result.IsSuccessStatusCode;
}
}
}

BankCardPaymentMethod

using API.Business.Models;
using Newtonsoft.Json;

namespace API.Business.PaymentMethods
{
public class BankCardPaymentMethod : BasePaymentMethod
{
private readonly HttpClient _httpClient;
public readonly CardInfo _cardInfo;
public BankCardPaymentMethod(decimal amount, CardInfo cardInfo) : base(amount)
{
_httpClient = new HttpClient();
_httpClient.BaseAddress = new Uri("http://bankadress.com");
_cardInfo = cardInfo;
}

public override async Task<bool> PayAsync()
{
var stringContent = JsonConvert.SerializeObject(_cardInfo);
var result = await _httpClient.PostAsync("/pay", new StringContent(stringContent));
return result.IsSuccessStatusCode;
}
}
}

PaymentManager

using API.Business.PaymentMethods;
using API.Business.PaymentMethods.Interfaces;

namespace API.Business
{
public class PaymentManager
{
public async Task<bool> PayAsync(BasePaymentMethod paymentMethod)
{
//Burada bazı işlemler yapılacak.
//orderlaştırma süreçleri için gerekli işlemler yapılabilir
//loglama, yapılabilir
//kullanıcı bazında ödeme limit kontrolleri gibi işlemler yapılabilir
var result = await paymentMethod.PayAsync();

//ödeme başarısına göre gerekli işlemler yapılabilir.
//kullanıcı limitleri güncellenebilir.
//sistemsel eventlar fırlatılarak gerekli dış servislere bilgiler verilebilir

return result;
}


public async Task<bool> PayWithInstallmentAsync(IInstallmentablePaymentMethod paymentMethod)
{
//Burada bazı işlemler yapılacak.
//orderlaştırma süreçleri için gerekli işlemler yapılabilir
//loglama, yapılabilir
//kullanıcı bazında ödeme limit kontrolleri gibi işlemler yapılabilir
//taksit kontrolleri yapılabilir (Posumuz bu taksite izin veriyor mu?)

var result = await paymentMethod.PayWithInstallmentAsync();

//ödeme başarısına göre gerekli işlemler yapılabilir.
//kullanıcı limitleri güncellenebilir.
//sistemsel eventlar fırlatılarak gerekli dış servislere bilgiler verilebilir

return result;
}
}
}

PaymentController

using API.Business;
using API.Business.Models;
using API.Business.PaymentMethods;
using Microsoft.AspNetCore.Mvc;

namespace API.Controllers
{
[ApiController]
[Route("api/payment")]
public class PaymentController : ControllerBase
{
private readonly PaymentManager _paymentManager;
public PaymentController(PaymentManager paymentManager)
{
_paymentManager = paymentManager;
}

[HttpPost("CreditCard")]
public async Task<IActionResult> PayWithCreditCard([FromBody] PaymentRequest request)
{
var creditCardPayment = new CreditCardPaymentMethod(request.Amount, request.CardInfo, request.InstallmentCount);
var result = request.InstallmentCount > 1
? await _paymentManager.PayAsync(creditCardPayment)
: await _paymentManager.PayWithInstallmentAsync(creditCardPayment);

return Ok(result);
}

[HttpPost("BankCard")]
public async Task<IActionResult> PayWithBankCard([FromBody] PaymentRequest request)
{
var bankCardPaymentMethod = new BankCardPaymentMethod(request.Amount, request.CardInfo);
/*
aşağıdaki blok artık hata verecek ve bizi doğru kullanıma zorlayacaktır.
var result = request.InstallmentCount > 1
? await _paymentManager.PayAsync(bankCardPaymentMethod)
: await _paymentManager.PayWithInstallmentAsync(bankCardPaymentMethod);//artık hata verecek
*/
var result = await _paymentManager.PayAsync(bankCardPaymentMethod);
return Ok(result);
}
}
}

Eveeeet… artık sadece taksitlendirilebilir sınıflarımızı işaretlemek için bir interface’e sahibiz. Ödeme yaparken de taksitli ödemeler sadece bu interface’den türetilmiş sınıflar için kullanılmak üzere özelleştirildi! Böylece artık yeni bir ödeme yöntemi eklemek istediğimizde gerekli işaretlemeleri yaparak tüm türeyen sınıflarımızı, base yapılarının yerine kullanabilir olduk.

Interface Segregation

ISP(Interace segregation principle) tüm SOLID prensipleri içerisinde, teorik olarak örneklenmesi en doğru olan prensiptir diye düşünüyorum. LSP ve ISP’yi tek makale üzerinden ele almamın asıl sebebi, ISP’nin aslında LSP’nin de savunduğu yerine geçebilme ve yerine geçtiğinde bozulmama prensibine çok benzer olması. Bu durumu biraz irdeleyelim ve yine tanım vererek başlayalım.

ISP’nin savunduğu yapı şudur; Bir sınıf, şablon olarak aldığı interface’in tüm özelliklerini sağlamalı, eğer sağlayamıyor ise bu özelliği implemente etmeye zorlanmamalıdır. Eğer bu özellik sağlanamıyorsa, interface doğru oluşturulmamıştır ve ayrı interfacelere bölünmelidir. Türetilen sınıflar bu özelliklerden hangilerini kullanacaksa buna uygun interfaceleri şablon kabul etmelidir. Böylece tüm özellikleri destekleyebilecektir.

Yukarıdaki tanım sizlere LSP örneğinde de göstermeye çalıştığım taksitlendirilebilir ödemeler için ayrı interface oluşturma mantığını çağrıştırmalıdır. Bizim ödeme metodu sınıflarımız eğer abstract bir kalıtım almak yerine interface üzerinden çalışıyor olsaydı, banka kartı ödemeleri taksitli ödemeyi destekleyemeyeceği için ISP’ye aykırı olacaktı. LSP’de verdiğimiz örneği interface’ler ile tekrar kodlayıp konuyu gereksiz uzatmak istemiyorum. Bu sebeple diyagramlar ile konuyu özetlemek istiyorum

Öncelikle LSP’de de verdiğimiz ilk örneği, yani hatalı kurguyu diyagrama dökelim

Bu diyagramda da görebileceğimiz gibi, taksitle ödemeyi desteklemeyen banka kartı ile ödeme metoduna, taksitli ödeme metodunu implemente etmeyi zorunlu hale getiriyoruz. ISP der ki; “bu hatalı bir kurgudur, türetilen sınıf desteklemediği fonksiyonu implemente etmeye zorlanmamalıdır.”

Olası çözüme bakalım.

Yukarıdaki diyagramdan da görebileceğimiz üzere interfaceleri gerekli özellik kırılımları bazında ayırdık ve türetilen sınıfların desteklenmeyecek fonksiyonları implemente etmelerini zorunlu olmaktan çıkardık. İşte ISP bu anlayışı savunur.

Şimdi bu iki prensip için yapılabilecek bazı eleştiriler var. Peşinen biz kendimizi eleştirerek işe başlayalım.

  • Öncelikle, taksitli işlemlerde direk false dönelim böylece hata almayız diyebiliriz. Böylece LSP’nin vurgu yaptığı sistemi bozmama kurgusunu sağlarız. Evet doğru bunu yapabiliriz.
  • Ben ayrılmış bir interface’den türettiğim sınıftan da hata atabilirim. Ne değişti ki hayatımda diyebiliriz. Evet doğru bunu da diyebiliriz.
  • Ben zaten banka kartlarına taksit yapılmadığını biliyorum. Dolayısıyla controller sınıfında zaten bu durumu handle ederdim. Gerekirse o seviyeden hata atardım. Ne gerek vardı şimdi ekstra kod yazmaya diyebiliriz. Evet çok haklısınız bu da söylenebilir.

Şimdi yukarıdaki eleştirilere aslında direkt olarak OOP, SOLID gibi kavramların neden var olduğu ile cevap vermek istiyorum. Bu kavramlar ya da onların referans ettiği pratikleri kullanmadan da pekala projeler geliştirebiliriz. Anlamamız gereken konu şu… Bunlar birer prensip. Prensipler kanun ya da kural değildir. Eğer benimserseniz, bir standart getirir ve faydaları ancak bir proje ekibinin ya da birlikte çalıştığınız paydaşlar kimler ise onların bu prensipleri benimsemesi ile değer kazanır. Hiç biri zorunlu değildir. Gerçek hayatta da olduğu gibi “Prensipler, onları benimsemiş olanlar için değerlidir.”

Biz benim verdiğim örnekteki pratikleri uygulamasak da bu kod bir şekilde çalıştırılır. Ancak bir gün projeye dahil olan bir yeni developer, dikkatsizlikle banka kartı ödemesini taksitli ödeme servisinden geçirmesin, ya da yeni bir ödeme yöntemi geliştiriyorsa, taksitli ödeme metodunu ayağa kaldırmak zorunda kalmasın diye bir standart inşa etmeye gayret ediyoruz. Bütün mesele bu.

Umarım iki yapı hakkında da açıklayıcı örnekler ve tanımlamalar yapabilmişimdir.
Örnek kodları github’da bulabilirsiniz
Bkz : https://github.com/batgungor/SOLID

Herkese iyi çalışmalar dilerim.

--

--

Batuhan güngör

Batuhan GÜNGÖR is a computer engineer who has been working since 2011 as a Software developer / Engineering Manager