Liskov Substitution Prensibi Nedir?

Liskov Substitution Prensibi Nedir?

Liskov Substitution Prensibi

Liskov Substitution Prensibi, nesne yönelimli tasarımda ilgili sınıflar arasında ikame edilebilirliğin (substitutability) ve davranışsal alt tiplendirmenin (behavioral subtyping) önemini vurgulayan temel bir prensiptir. İyi bir yazılım tasarımında olması gereken sürdürülebilirliği ve esnekliği teşvik eden beş SOLID ilkesinden biridir. LSP olarak kısaltılır. Bu prensip ilk kez 1987 yılında Barbara Liskov tarafından yayınlanan bir makale ile ortaya çıkmıştır. Barbara Liskov bu prensibin matematiksel formülünü aşağıdaki gibi tanımlamıştır.

Tanım

If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

Tanım biraz karışık geldiyse, beraber bu tanımı açıklamaya çalışalım. Elimizde T adında bir sınıf olsun ve T sınıfından o2 adında bir nesne üretelim ve P adındaki uygulamamızda kullanalım. Ardından bir S sınıfı oluşturalım ve S sınıfından o1 adında başka bir nesne oluşturalım. Daha sonra biz o2 nesnesi yerine o1 nesnesini koyabilirsek ve bunu yaparken uygulamanın davranışları değişmezse S sınıfı, T sınıfının bir alt tipidir diyebiliriz. Bu durumda T sınıfı üst sınıf (parent class), S sınıfı ise alt sınıf (child class) diyebiliriz. Yani S sınıfı, T sınıfını miras almıştır ve T sınıfının tüm özelliklere sahiptir. Aslında bir çoğumuzda bu tanım nesne tabanlı programlamadaki inheritance konseptini aklımıza getiriyor olabilir. Böyle düşünmemiz doğrudur. Çünkü LSP, inheritance ile yakından ilişkilidir. LSP, inheritance nasıl uygulamamız konusunda bize bazı kurallar koyarak yol gösterir. Bu kurallar şunlardır:

  1. Davranışı Koruma (Behavior Preservation): Alt sınıflar, üst sınıflarının yerine geçebilmeli ve bu durumda programın doğruluğunu bozmamalıdır. Yani, bir alt sınıfın nesnesi, üst sınıfın nesnesi olarak kullanıldığında, beklenen davranışın sağlanması gerekmektedir.
  2. Değişmezler Korunmalıdır(Invariants must be preserved): Alt sınıflar, üst sınıflarının metot imzalarını (isim, parametreler, dönüş tipleri) korumalıdır. Bu, alt sınıf nesnelerinin üst sınıf nesneleri ile değiştirilebilir olmasını sağlar. Yani çok biçimlilik (Polymorphism) sağlar.
  3. Ön Koşullar Güçlendirilemez (Preconditions cannot be strengthened): Alt sınıflar, üst sınıflarının metotlarındaki ön koşulları güçlendirmemelidir. Üst sınıfın ön koşullarını ya da daha gevşek koşulları kabul etmelidir.
  4. Son Koşullar Zayıflatılamaz (Postconditions cannot be weakened): Alt sınıflar, üst sınıflarının metotlarındaki son koşulları zayıflatmamalıdır. Üst sınıfın son koşullarını ya da daha spesifik koşulları kabul etmelidir.
  5. İstisna Kuralı (Exception Rule): Eğer üst sınıfın bir metodu istisna (exception) fırlatıyorsa, alt sınıfın o metodu, üst sınıfın fırlattığı istisnaların aynısını ya da alt türlerini fırlatabilmelidir. Bu, istisna türlerinin uyumlu bir şekilde işlenmesini sağlar.

Bu kurallara uyum sağlayarak geliştirlen sistemlerin tasarımları esnek, genişletilebilir ve sürdürebilir olur. Şimdi hep beraber bu prensibi ve kuralları örneklerle anlamaya çalışalım.

İlk örneğimizde Licence adında bir üst sınıf, PersonalLicence ve BusinessLicence adında iki alt sınıfımız var. Licence sınıfı içerisinde CalcFee adında bir metot var ve bu metot lisans ücretini hesaplıyor. PersonalLicence ve BusinessLicence sınıflarıda Licence sınıfını miras alıyor ve CalcFee metodunu ezerek(overriding) farklı hesaplamalar yaparak lisans bedelini dönüyor.

Figure 1

public class Licence
{
public virtual decimal CalcFee()
{
Console.WriteLine("Licence is calculating");
// Some calculations
return 120;
}
}

public sealed class PersonalLicence : Licence
{
public override decimal CalcFee()
{
Console.WriteLine("Personal licence is calculating");
// Some calculations
return 85;
}
}

public sealed class BusinessLicence : Licence
{
public override decimal CalcFee()
{
Console.WriteLine("Business licence is calculating");
// Some calculations
return 100;
}
}

Ayrıca Billing adında bir sınıfımız var. Bu sınıf oluşturulurken Licence tipinde bir parametre alıyor.

public sealed class Billing
{
private readonly Licence _licence;

public Billing(Licence licence)
{
_licence = licence;
}

public void CalcFee()
{
_licence.CalcFee();
}
}

Aşağıdaki kod parçasında ise üç farklı billing nesnesi oluşturdum. Nesneleri oluştururken Licence, PersonalLicence ve BusinessLicence nesnelerini sırasıyla parametre olarak verdim. Daha sonra her birinde CalcFee metodunu çağırdım.

var billing1 = new Billing(new Licence());
var billing2 = new Billing(new PersonalLicence());
var billing3 = new Billing(new BusinessLicence());
billing1.CalcFee();
billing2.CalcFee();
billing3.CalcFee();

Uygulamayı çalıştırdığımda aşağıdaki çıktıyı aldım.

Licence is calculating
Personal licence is calculating
Business licence is calculating

Bu örnekte üst sınıf olan Licence, Billing ile birlikte çalışabiliyor. Licence yerine Personal Licence ve Business Licence alt sınıflarının nesnelerini parametre olarak Billing servisine verdiğimizde uygulamamızın akışı bozulmadan çalışmaya devam edebiliyor. Böylece bu örnekte LSP’ye uygun bir şekilde geliştirme yapmış olduk.

İkinci örneğimizde LSP’yi ihlel eden bir senaryo oluşturmaya çalışalım. Calculator adında bir üst sınıf ve PositiveCalculator adında bir alt sınıfımız olsun. Calculator sınıfındaki Sum metodu 2 sayıyı alıyor ve toplayıp geri dönüyor. PositiveCalculator sınıfı ise Sum metodunu eziyor (overriding). Number1 ve number2 parametrelerine sıfır ya da sıfırdan büyük olma koşulu uyguluyor. Eğer bu koşul sağlanmaz ise hata fırlatıyor.

Figure 2

public class Calculator
{
public virtual int Sum(int number1, int number2)
{
return number1 + number2;
}
}

public sealed class PositiveCalculator : Calculator
{
public override int Sum(int number1, int number2)
{
if (number1 < 0)
{
throw new InvalidOperationException("number1 must be positive");
}

if (number2 < 0)
{
throw new InvalidOperationException("number1 must be positive");
}

return number1 + number2;
}
}

Calculator sınıfından bir nesne ürettiğimizde ve Sum metodunu çağırdığımızda uygulamamız beklediğimiz gibi çalışıyor.

Calculator calculator = new Calculator();
int result = calculator.Sum(-2, 8);

if (result != 6)
{
Console.WriteLine($"expected result 6, but result is {result}");
}
else
{
Console.WriteLine("The result is as expected");
}

Figure 3

Şimdi aynı kod parçasını PositiveCalculator ile deneyelim.

Calculator calculator = new PositiveCalculator();
int result = calculator.Sum(-2, 8);

if (result != 6)
{
Console.WriteLine($"expected result 6, but result is {result}");
}
else
{
Console.WriteLine("The result is as expected");
}

Figure 4

Calculator sınıfı yerine PositiveCalculator sınıfını kullandığımız zaman uygulamamızda beklenmedik bir şekilde bir hata aldık ve bu uygulamamızın akışını bozdu. Bu akışı düzeltmek için kodumuzu try catch bloğu içerisine alabiliriz. Ancak böyle bir değişim uygulamamızın akışını değiştirmek oluyor. Bizim amacımız üst sınıfın rahatlıkla çalışabildiği ortamlarda alt sınıflarında bir değişim olmadan rahatlıkla çalışabilmesi olmalıdır. PositiveCalculator sınıfı içerisindeki sum metodu içerisine eklediğimiz ön koşul yüzünden Ön koşullar güçlendirilemez kuralını ihlal ettik. Üst sınıfın bilmediği bir hatayı alt sınıfta fırlattığımız için ve istemci böyle bir hatadan haberdar olmadığı için uygulamamızın beklenmedik bir şekilde kapanmasına neden oldu. Son olarak uygulamanın akışı bozulduğu için Davranışı Koruma kuralını da ihlal ettik. Yani LSP’yi ihlal etmiş olduk.

LSP, geliştiricileri “is-a” ilişkisine tüm yönleriyle uymaya çalışan sınıflar ve hiyerarşiler tasarlamaya teşvik ederek sürdürülebilir, genişletilebilir ve öngörülebilir yazılım sistemleri oluşturmak için bir temel sağlar. LSP’yi uygulayarak daha modüler, uyumlu çalışabilen kodlar yazılabilir. Yeni sınıfların ve özelliklerin kolay entegrasyonu sağlanabilir. Kod tabanını zaman içerisinde oluşabilecek değişikliklere karşı daha dayanıklı hale getirebilir. LSP uyumluluğu, yüksek kaliteli nesne yönelimli kod yazmanın önemli bir yönüdür. Yazılım projelerinin genel sağlığına ve uzun ömürlülüğüne katkıda bulunur.

Bu yazımda Liskov Substitution Prensibini anlatmaya çalıştım. Umarım sana dokunmuş olabilirim. Merak ettiğin bir şey olursa benimle iletişime geçebilirsin.

— Bana ulaşmak için: Twitter, Linkedin