Liskov’s Substitution Principle (LSP)

Liskov's Substitution Principle (LSP)

“An object should be substitutable by its base class (or interface). Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.”

Türetilmiş sınıflar türetildikleri ana sınıf (base class) ile yer değiştirilebilir olmalıdır. Bir başka deyişle türetilen sınıflardan oluşturulan nesneler türetildikleri ana sınıfların (base class) nesneleriyle yer değiştirdiklerinde aynı davranışı göstermek zorundadırlar.


Bu prensibe aykırı olan durumu örnekleyecek olursak;

public interface IPersistedResource
{
   void Load();
   void Persist();
}

Geliştirdiğiniz uygulamada kullandığınız resource’ları kaydedip yüklemek için şöyle bir interface’imizin olduğunu düşünelim. Bu interface’i implement eden sınıflarımızı oluşturalım.

//Uygulama ayarları resource'ları için
public class ApplicationSettings:IPersistedResource
{
   public void Load()
   {
     // Uygulama ayarlarını yükleyen kodlar
   }
   public void Persist()
   {
     // Uygulama ayarlarını herhangi bir yere kaydeden kodlar
   }
}
//Kullanıcı ayarları resource'ları için
public class UserSettings:IPersistedResource
{
   public void Load()
   {
     // Uygulama ayarlarını yükleyen kodlar
   }
   public void Persist()
   {
     // Uygulama ayarlarını herhangi bir yere kaydeden kodlar
   }
}

Metod implementasyonları şu aşamada çok önemli değil kullanıcı ve uygulama ayarları için kaydetme ve yükleme işlemlerini yaptıklarını düşünelim.

Geliştirğimiz uygulamanın herhangi bir yerinde de aşağıdaki gibi tüm resource’ları yükleyen bir metodumuz olduğunu düşünelim:

static IEnumerable<IPersistedResource> LoadAll()
{
var allResources = new List<IPersistedResource>
                          {
                             new UserSettings(),
                             new ApplicationSettings()
                          };
   allResources.ForEach(r=> r.Load());
   return allResources;
}

Aynı şekilde aşağıdaki gibi tüm resource’ları kaydeden bir metodumuz olduğunu düşünelim:

static void SaveAll(IEnumerable<IPersistedResource> resources)
{
   resources.ForEach(r=> r.Persist());
}

Ve başka bir yerdede bu metodları kullanalım :

IEnumerable<IPersistedResource> resources = LoadAll();
Console.WriteLine("tüm resource ayarları yüklendi.");

// belki arada resource'larda değişiklik yapıldı.

SaveAll(resources );
Console.WriteLine("tüm resource ayarları kaydedildi.");

Şu ana kadar herşey çok güzel ancak yeni bir resource eklemek istediğinizde örneğin “special settings” :

public SpecialSettings:IPersistedResource
{
   public void Load()
   {
     // bazı kodlar
   }
   public void Persist()
   {
     throw NotImplementedException();
   }
}

Görünüşe Load metodu bir takım işlevleri yerine getirmekde fakat Persist Metodu impelement edilmemiş ve NotImplementedException throw edilmiş.

Resource’ları yükleyen metodumuza “special settings” sınıfını ekleyerek aşağıdaki şekilde güncelleyelim :

static IEnumerable<IPersistedResource> LoadAll()
{
   var allResources = new List
                          {
                             new UserSettings(),
                             new ApplicationSettings(),
                             // Yeni resource sınıfımız
                             new SpecialSettings()
                          };
   allResources.ForEach(r=> r.Load());
   return allResources;
}

Uygulamayı çalıştırdığımızda şimdilik herşey normal görünüyor taki yüklediğimiz resource’ları kaydetmek için SaveAll metodunu çalışana kadar. SaveAll metodu çalıştığında “NotImplementedException” exception’ı throw olacaktır.

SaveAll metodunu aşağıdaki şekilde düzenlendiğinde hata düzeltilmiş olacaktır:

static void SaveAll(IEnumerable<IPersistedResource> resources)
{
   resources.ForEach(r=>
                          // Eğer SpecialSettings ise kaydetme !
                          if (r is SpecialSettings)
                             return;
                          r.Persist();
                    );
}

Böyle bir çözüm zekice olabilir ama prensibe aykırı durum tam bu noktada bariz olarak gözümüze çarpmaktadır.

SaveAll metoduna baktığımızda “SpecialSettings” sınıfının “IPersistedResource” interface’ini implement etmesine rağmen aynı davranışı sergilemediğini gözlemlenebilmektedir; Persist metodu implement edilmediği için uygulama exception fırlatacaktır. Bu durumda Persist metodunu değiştirebilir ve hata alınması engellenmiş olur diyebiliriz ancak sınıf içerisinde kullanmadığımız bir metodun olması istenmeyen bir durumdur.

Peki bu durumu nasıl düzeltebiliriz?

Bu aykırı durumu düzeltmek için ihtiyacımız her davranış için ayrı bir intarface tanımlamak olabilir (Interface Segregation Principle,  ISP). Yükleme özelliği olacak resource’lar için LoadAll metodunu içeren bir interface, kaydetme özelliği olacak resource’lar için SaveAll metodu içeren bir interface tanımlanabilir. Diğer bir deyişle ihtiyacımız aşağıdaki gibi olacaktır :

static void SaveAll(IEnumerable<IPersistResource> resources)
{
   resources.ForEach(r=> r.Persist());
}
static IEnumerable<ILoadResource> LoadAll()
{
   var allResources = new List<ILoadResource>
                          {
                             new UserSettings(),
                             new ApplicationSettings(),
                             new SpecialSettings()
                          };
   allResources.ForEach(r=> r.Load());
   return allResources;
}

Böylece istenilen resource’lara kaydetme istenilenlerede yükleme yada her iki özellikte iki interface implement edilerek kazandırılabilir. Uygulama içerisinde sadece kaydedilecek resource’lar IPersistResource interface’ini implement edecek, yüklenecek resource’lar da ILoadResource interface’ini implement ederek gerekli davranışların sergilenmiş olması sağlanacaktır. Bahsettiğim interface tanımlamasıda aşağıdaki gibi olacaktır.

public interface ILoadResource
{
   void Load();
}
public interface IPersistResource
{
   void Persist();
}

Sonuç itibarı ile IPersistedResource interface’ini ihtiyacımız doğrultusunda 2 basit parçaya ayırdık. UserSettings sınıfı ve ApplicationSettings sınıfı kaydetme ve yükleme davranışlarını sergilediğinden 2 interface’i de implement etmektedirler. SpecialSettings sınıfı ise sadece ILoadResource interface’ini implement etmiştir çünkü yerine getiremeyeceği davranışların zorla yüklenmesi intenmeyen bir durumdu.

Kaynaklar;

Resim, © Jennifer M. Kohnke
Agile Principles, Patterns, and Practices in C# 
Claudio Lassala’s Blog

0 thoughts on “Liskov’s Substitution Principle (LSP)”

Bir cevap yazın