İlk yazıda giriş yaptığımız SOLID prensiplerinin ilk ikisini örneklerle ve kodlarla açıklamaya çalışmıştık. İlk yazıya şuradan ulaşabilirsiniz.
O halde hemen yola koyularak üçüncü prensip ile devam edelim;
Liskov Substitution Principle
İlk olarak 1987 yılında MIT de profesör olarak görev yapmakta olan Amerikalı bilgisayar bilimcisi Barbara Liskov tarafından “Data Abstraction and Hierarchy” isimli konferansta söz edilen prensipten 1994 yılında yazdığı makalede Barbara Liskov prensibi şu şekilde tanımlıyor;
Let \phi (x) be a property provable about objects x of type T. Then {\displaystyle \phi (y)} should be true for objectsy of type S where S is a subtype of T.
Evet Liskovun yaptığı tanım biraz anlaşılmaz ve karmaşık gözüküyor. Bu durumdan mütevellit yazılım dünyasının Bob Amcası olarak tanıdığımız Robert C. Martin, 2006 yılında “Agile Principles, Patterns, and Practices in C#” adlı kitabında prensibi kısaca;
Derived classes must be substitutable for their base classes.
olarak tanımlıyor ve günümüzde bu tanım hala geçerliliğini korumaktadır.
Türkçeye geri dönecek olursak, Liskovs Substitution prensibi kısaca; Alt sınıflardan oluşturulan nesneler üst sınıfların nesneleriyle yer değiştirdiklerinde aynı davranışları göstermelidir. diye tanımlarsak yanlış olmayacaktır. Bu durumu biraz daha açacak olursak. Alt sınıflarda oluşturulan nesneler üst sınıfların nesneleriyle yer değiştirdiklerinde aynı davranışı göstermelidirler ve herhangi kullanılmayan bir özellik olmamalıdır. Şeklinde son tanımı yapabiliriz, tüm bu tanımlamalara rağmen hala oturmamış bir şeyler olduğunu düşünürseniz buyurun bir de kod tarafında bu prensip için sıkça verilen bir örnek üzerinden inceleyelim.
Örnek: Yeşilbaş ördek, yaz ördeği ve plastik ördek olmak üzere 3 farklı çeşit ördeğimiz olsun ve bu ördeklere sırasıyla ses çıkarma (vaklama), yüzme ve uçma özelliklerini ekleyelim.
public abstract class Duck{
public abstract void Quack();
}
public class MallardDuck : Duck
{
public override void Quack()
{
System.Console.WriteLine("Quack!");
}
}
public class MarbledDuck : Duck
{
public override void Quack()
{
System.Console.WriteLine("Quack!");
}
}
public class RubberDuck : Duck
{
public override void Quack()
{
System.Console.WriteLine("Squeak!");
}
}
Duck sınıfından kalıtım alarak oluşturulmuş tüm ördek sınıflarımız artık türlerine göre seslerini çıkartabiliyor hale geldiler. Duck (Ördek) sınıfının içine ses çıkarma (Quack()) metodunu yazmamızda ve ördek türlerinin bu sınıftan miras almasında herhangi bir sorun yoktur çünkü tüm ördekler vaklayabilir. Şimdi ördeklerimize yüzme özelliği ekleyelim.
public abstract class Duck{
public abstract void Quack();
public abstract void Swimming();
}
public class MallardDuck : Duck
{
public override void Quack()
{
System.Console.WriteLine("Quack!");
}
public override void Swim()
{
System.Console.WriteLine("Swimming!");
}
}
public class MarbledDuck : Duck
{
public override void Quack()
{
System.Console.WriteLine("Quack!");
}
public override void Swim()
{
System.Console.WriteLine("Swimming!");
}
}
public class RubberDuck : Duck
{
public override void Quack()
{
System.Console.WriteLine("Squeak!");
}
public override void Swim()
{
System.Console.WriteLine("Floating!");
}
}
Ördeklerimiz artık yüzebiliyor hale geldiler. Duck (Ördek) sınıfının içine yüzme (Swimming()) metodunu yazmamızda ve ördek türlerinin bu sınıftan miras almasında herhangi bir sorun yoktur çünkü tüm ördekler yüzebilir. Son olarak ördeklerimize uçma özelliğini ekleyecek olursak.
public abstract class Duck {
public abstract void Quack();
public abstract void Swim();
public abstract void Fly();
}
public class MallardDuck : Duck
{
public override void Fly()
{
System.Console.WriteLine("Flying!");
}
public override void Quack()
{
System.Console.WriteLine("Quack!");
}
public override void Swim()
{
System.Console.WriteLine("Swimming!");
}
}
public class MarbledDuck : Duck
{
public override void Fly()
{
System.Console.WriteLine("Flying!");
}
public override void Quack()
{
System.Console.WriteLine("Quack!");
}
public override void Swim()
{
System.Console.WriteLine("Swimming!");
}
}
public class RubberDuck : Duck
{
public override void Fly()
{
throw new Exception("Rubber ducks can't fly!");
}
public override void Quack()
{
System.Console.WriteLine("Squeak!");
}
public override void Swim()
{
System.Console.WriteLine("Floating!");
}
}
Son durumda uçamayan bir ördeğimiz oldu. Prensibi hatırlayacak olursak alt türlerin üst türlerin tüm özelliklerini taşıması ve üst türler yerine kullanılabilir olması gerektiğini söylemiştik, peki bu durumda plastik ördek aslında bir ördek midir? yada Duck sınıfı yerine RubberDuck kullanabilir miyiz? tabiki kulanamayız çünkü plastik ördekler uçamayacağından Fly() metodu exception fırlatıyor.
Peki doğru şekilde ördeklerimize özellikleri nasıl eklememiz gerekirdi hemen görelim;
public abstract class CommonDuck{
public abstract void Quack();
public abstract void Swim();
}
public interface IFly{
void Fly();
}
public class MallardDuck : CommonDuck, IFly
{
public void Fly()
{
System.Console.WriteLine("Flying!");
}
public override void Quack()
{
System.Console.WriteLine("Quack!");
}
public override void Swim()
{
System.Console.WriteLine("Swimming!");
}
}
public class MarbledDuck : CommonDuck, IFly
{
public void Fly()
{
System.Console.WriteLine("Flying!");
}
public override void Quack()
{
System.Console.WriteLine("Quack!");
}
public override void Swim()
{
System.Console.WriteLine("Swimming!");
}
}
public class RubberDuck : CommonDuck
{
public override void Quack()
{
System.Console.WriteLine("Squeak!");
}
public override void Swim()
{
System.Console.WriteLine("Swimming!");
}
}
kodumuzu yeniden düzenlediğimizde Duck sınıfı içindeki Quack() ve Swim() metodlarını CommonDuck() adında abstract class olarak ördeklerimize ekledik. İlk 2 ördeğimiz uçabilirken plastik ördeğin uçamayacağından bahsetmiştik ancak bu durumda her alt sınıf üst sınıfın yerine kullanılabilir hale geldi ve prensibi uygulamalı olarak görmüş olduk. Diğer taraftan yüzme ve ses çıkarma ve uçma özelliklerini ayrı interfaceler veya abstract classlar sayesinde de sağlayabilirdik hatta sağlamalıydık çünkü, gelecekte oluşturacağımız tüm ördeklerin yüzme ve ses çıkarma özelliği olacağını varsayarsak problem olmayacaktır. Ancak bir gün ses çıkaramayan ve uçamayan bir ördek oluşturmak istersek yine geçmişte oluşturduğumuz bağımlılıklardan dolayı istenmeyen bir durum ortaya çıkacaktı bu yüzden pratikte tüm özellikleri ayırmanın daha faydalı olacağını söyleyebiliriz.
Interface Segregation Principle
Software Craftsmanship manifestosunda “Üretilen yazılımın sadece çalışması yetmez, aynı zamanda bu yazılımın “iyi” üretilmiş olması gerekir.” şeklinde olan yazılım geliştirme süreçlerinin olması gereken teknik temeline vurgu yapan bir madde bulunmaktadır. Bu maddeden yola çıkacak olursak işimizi en çok kolaylaştıran kavramlardan birinin soyutlama olduğunu söylemek yanlış olmayacaktır. Bu kadar önemli bir rol üstlenen soyut yapıların kullanımına da aynı ölçüde dikkat edilmesi gerekliliğiyse kaçınılmaz bir gerçektir.
Türkçeye “arayüzlerin ayrımı prensibi” olarak çevirebileceğimiz bu prensipte arayüzlerin özelleştirilmiş olması gerektiği ve gerektiğinden fazla metod veya özellik olmaması gerektiğini belirtmektedir. Birden fazla metodu içeren interfacelerimizi ilerleyen süreçlerde bir çok farklı sınıf tarafından miras alınacak olursa içerisindeki tüm metodların sınıf içinde implemente edilmesi gerekir bu durumda zaman zaman istenmeyen metodlarında sınıf içerisine implemente edilmesiyle sonuçlanabilir. Bu durumun önüne geçmenin yolu ise Interface Segregation prensibini uygulayarak arayüzlerin doğru şekilde özelleştirilmesini sağmaktan geçmektedir. Şimdi anlattığımız tüm bu durumları bir örnek üzerinden inceleyelim.
Örnek: Elektrikli Otomobil, Benzinli Otomobil sınıflarımız ve özelliklerinin şöyle oluşturduğumuzu düşünelim.
public interface ICar
{
void Accelerate();
void Charge();
}
public class GasolineCar : ICar
{
public void Accelerate()
{
System.Console.WriteLine("Car accelerating!")
}
public void Charge()
{
throw new System.NotImplementedException();
}
}
public class ElectricCar : ICar
{
public void Accelerate()
{
System.Console.WriteLine("Car accelerating!");
}
public void Charge()
{
System.Console.WriteLine("Car is charging succesfully!");
}
}
Örneğimizde ElectricCar ve GasolineCar sınıflarımız ICar arayüzünden miras almaktalar fakat elektrikli araç sarj olma özelliğine sahipken benzinli aracın böyle bir özelliği bulunmamaktadır, dolayısıyla bu metodun GasolineCar sınıfı içinde yer almaması gerekmektedir. Kodumuzu düzenleyecek olursak;
public interface IAccelerate
{
void Accelerate();
}
public interface ICharge
{
void Charge();
}
public class GasolineCar : IAccelerate
{
public void Accelerate()
{
System.Console.WriteLine("Car is accelerating!");
}
}
public class ElectricCar : IAccelerate, ICharge
{
public void Accelerate()
{
System.Console.WriteLine("Car is accelerating!");
}
public void Charge()
{
System.Console.WriteLine("Car is charging");
}
}
Bu durumda ICar arayüzü içindeki özellikleri ayrı interfaceler haline getirerek ayırırsak/özelleştirirsek sorun ortadan kalkacaktır ve bu sayede benzinli aracımızın charging özelliğini implemente etmesine gerek kalmayacaktır.
Dependency Inversion Principle
SOLID prensiplerin son prensibi olan dependency inversion prensibini türkçeye “Bağımlılığın ters çevrilmesi prensibi” olarak çevirebiliriz, bağımlılık kelimesiyle yüksek seviyeli işlem yapan sınıfların, düşük seviyeli işlem yapan sınıflara bağımlı olması kast edilmektedir, daha da açacak olursak üst seviyeli sınıfların alt seviye sınıflara bağımlı olması nedeniyle alt seviyeli sınıflarda yapılacak değişiklikler üst sınıfların davranışlarını etkileyecektir.
Dependency inversion prensibi ise bu bağımlılığın önüne geçebilmek için yazdığımız programların soyutlamalar üzerine dayandırılması gerektiğini öğütler ve soyutlamalar üzerinden oluşturulan bağımsızlıklarda bağımlılığımız soyut sınıflara olacağından üst seviye sınıfların alt seviye sınıflara olan bağımlılığının önüne geçmiş oluruz. Bu durumu örnek üzerinden açıklayacak olursak.
Örnek: İnternet sitesi üzerinden satış yapılan bir e-ticaret sistemi kodladığımızı düşünelim ve bu e-ticaret sisteminde ürünlerin fiyatları şöyle hesaplanıyor olsun;
public class TaxCalculator{
public double CalculateTax(double productPrice){
var tax = productPrice * 0.18;
return (tax + productPrice);
}
}
public class PriceCalculator
{
private TaxCalculator taxCalculator = new TaxCalculator();
public double CalculateTotalProductPrice(double productPrice){
var totalPrice = taxCalculator.CalculateTax(productPrice);
return totalPrice;
}
}
Kodumuza bakarsak sorunsuz çalışıyor gibi gözüküyor ancak ilerde yeni bir yasayla kitap ürünlerinde KDV’nin %5’e düşürülmesine karar verildiğini düşünürsek bu durumda PriceCalculator içinde kesinlikle değişiklik yapmak zorundayız ve ortaya çıkan ihtiyaçlar neticesinde gerekli değişikliği yapmak için gereken zaman ve emek miktarı artacak, bunun sebebi ise yüksek seviye işlem yapan PriceCalculator sınıfının alt seviye işlem yapan TaxCalculator sınıfına bağımlı olmasıdır. Peki şimdide dependency inversion prensibiyle bu durumun önüne nasıl geçileceğini inceleyecelim;
public interface ITaxCalculator {
double CalculateTax(double price);
}
public class TaxCalculator18 : ITaxCalculator{
public double CalculateTax(double productPrice){
var tax = productPrice * 0.18;
return (tax + productPrice);
}
}
public class TaxCalculator5 : ITaxCalculator{
public double CalculateTax(double productPrice){
var tax = productPrice * 0.05;
return (tax + productPrice);
}
}
public class PriceCalculator
{
private ITaxCalculator taxCalculator;
public double CalculateTotalProductPrice(double productPrice){
taxCalculator = new TaxCalculator5();
return taxCalculator.CalculateTax(productPrice);
}
}
Yaptığımız soyutlama sayesinde artık PriceCalculator sınıfımız içinde %5’e vergi oranına göre KDV hesaplamayı kolayca yapabiliyoruz, aynı şekilde ilerleyen zamanlarda KDV oranı tekrar %18’e çıkarsa en az eforla tüm değişiklikleri kolayca yapabiliyor olacaktık, ayrıca dikkatimizi çekmesi gereken bir başka konu PriceCalculator sınıfı içerisinde TaxCalculator18 veya TaxCalculator5 sınıflarını doğrudan çağırmadan ITaxCalculator sınıfı üzerinden kullanmış olduk buda istediğimiz zaman istediğimiz kadar değişikliği kolayca yapmamızı sağlayan esnekliğe kavuşmuş olduk.
Diğer bir deyişle prensibi uygulayarak, yüksek seviyeli işlem yapan sınıf ile düşük seviyeli işlem yapan sınıf arasına tanımladığımız soyutlama katmanı sayesinde aradaki bağımlılığı düşük seviyeli işlem yapan sınıfın yüksek seviyeli işlem yapan sınıfa doğru olacak şekilde değiştirmiş olduk.
Kendi tecbürelerimden de yola çıkarak bu prensip için kısaca; Yazdığınız kodun herhangi bir yerinde bir sınıfın direkt instance’ını kullanıyorsanız. (var taxCalculator = new TaxCalculator18 gibi) bu durum ileride çok büyük bir ihtimalle çeşitli sorunlara yol açacaktır diyebilirim.
- https://www.alexaitken.nz/blog/l-for-liskov-substitution-principle/
- http://www.kurumsaljava.com/2009/10/29/liskov-substitution-principle-lsp-liskovun-yerine-gecme-prensibi/
- https://en.wikipedia.org/wiki/Liskov_substitution_principle
- https://www.gencayyildiz.com/blog/bagimliligin-ters-cevrilmesi-prensibidependency-inversion-principle-dip/
- https://www.c-sharpcorner.com/article/solid-principles-in-c-sharp-dependency-inversion-principle/
Be First to Comment