Liskov Substitution Principle – LSP – ( Liskov Yerdeğiştirme Prensibi)

Liskov Substitution Principle

Merhaba sevgili yazılım dostları

SOLID prensiplerini irdelemeye Liskov Substitution Principle ile devam ediyoruz. Bu yazımda, sayın Barbara Liskov hanımın hayatımıza katmış olduğu prensibi anlatacağım. Hali hazırda MIT (Massachusetts Institute of Technology) programlama metodolojileri grup liderliği yapan bu dahi kadın bakın 1988 yılında Data Abstraction and Hierarchy adlı kitabında ne demiş?

Aranan yer değiştirme özelliği şöyle tanımlanabilir: T cinsinden parametre alan tüm programlar (fonksiyonlar) P olacak şekilde, S tipinde o1 nesnesi ve T tipinde o2 nesnesi olsun. Eğer o1 ile o2 nesneleri yer değiştirdiğinde P’nin davranışı değişmiyorsa S tipi T tipinin alt tipidir!

Ney? Çok iyi anlayan (hatta anlayan 🙂 ) varsa beri gelsin! Kendi adıma söyleyeyim; beynimde şiddetli bir karıncalanma hissetmiştim ilk okuduğumda. Hele bir de İngilizcesini okuyunca inanın bana kafayı yiyorsunuz. İşte Barbara ablamızın orijinal cümlesi:

 “What is wanted here is something like the following substitution property:  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.”

Ama bakın, bunu resmi olarak 1994 yılında nasıl formüle etmişler (baştan uyarıyorum; bu da matematik ile açıklanmış bir ifade):

q(x) fonksiyonu T tipindeki x nesnesi için kanıtlanabilirdir. O halde T tipinin alt tipi olan S tipindeki y nesnesi için de q(y) fonksiyonu kanıtlanabilir olmalıdır.

Eğer hala bu satırları okuyorsanız (yani “Aha! Şişman Adam sonunda delirdi. Yazık lan” dememişseniz), bu prensibin yazılım tarafında nasıl ifade edileceğini merak ediyorsunuz demektir. Kendi tanımlamalarıma geçmeden önce, bu tarz kavramları ilk kez basite indirgeyerek yazılım dünyasına anlatan Robert Martin’den de bir alıntı yapmamak olmaz.

Robert Martin (namı diğer Bob amca), bakın bu prensibi nasıl anlatıyor:

“Temel (base) sınıfın işaretçisini (pointer) ya da referansını kullanan fonksiyonlar, bu sınıftan türemiş olan (derived) sınıfları da ekstra bilgiye ihtiyaç duymaksızın kullanabilmelidir.” İşte biraz daha yazılım kavramlarıyla bezenmiş bir tanım.

Bakalım ben (namı diğer şişman adam) aynı prensibi nasıl tanımlayacağım?

“Aynı temel sınıftan türeyen tüm sınıflar, birbirlerinin yerine kullanılabilir olmalıdır. Bu yer değiştirme durumunda, sınıfa özel bir istisna kesinlikle oluşmamalıdır.” Tabii bu tanım için yorumlarınızı, bu sayfanın altına bekliyorum.

Elbette bu prensibin anlaşılması için yine bir örnekten faydalanacağız. Diyelim ki, farklı veri kaynakları (SQL, Excel dosyası vs.) ile çalışabilecek bir uygulama geliştiriyorsunuz. Tüm bu veri kaynakları ile “yükleme” ve “kayıt” işlemleri yapabileceğinizi analiz ettiniz ve bu metotları bir abstract sınıfta (ya da interface) tutmaya karar verdiniz. Sonuç olarak şöyle bir abstract sınıf geliştirmiş oldunuz:

Sonra da bu sınıftan türeyen sınıflarınızı aşağıdaki gibi oluşturdunuz.

Sıkıntı yok. Her şey yolunda… Şimdi de veri kaynakları ile işlem yapacak bir sınıfa ihtiyacınız var. Kolları sıvadınız ve aşağıdaki sınıfı oluşturdunuz.

Tamam. Her şey tıkırında. Şu ana dek geliştirmiş olduğunuz tasarım da LSP’ye gayet uyuyor. Ortak bir sınıftan türeyen DatabaseSource ve ExcelFile dosyaları birbirlerinin yerine kullanılabilir durumda. Hadi gelin şimdi işleri biraz karıştıralım.

Projenizi geliştirirken; XML kaynağına da ihtiyaç duydunuz ve yine aynı abstract sınıftan türeyen XMLSource sınıfını oluşturdunuz. Ancak, müşterinin size özellikle vurguladığı bir şey var: “XML dosyaları sadece yüklenebilmeli. Kaydedilmelerini istemiyoruz.” Ne yaparsınız?

Akla hemen iki seçenek geliyor. Bunlardan ilki, XmlSource sınıfında yer alan Save metodunun Exception fırlatması olabilir.

 

Ancak bu çözümü uygularsanız, DataSourceProcess sınıfında yer alan SaveAll() metodunda; try-catch bloğu kullanmanız gerekecek. Yani, “sınıfa özel istisnai bir durum” şüphesi içinde olacaksınız.

Diğer bir çözüm ise, SaveAll() metodu içerisinde tip kontrolü yapmak olacaktır. Yani metodunuzu şöyle geliştireceksiniz:

 

Fakat burada da yine “sınıfa özel istisna” yok mu sizce? Bariz bir şekilde tipe özel bir fonksiyon yazmış oluyorsunuz. Yani şu an itibariyle LSP’ye uymayan bir tasarım geliştirmiş bulunmaktasınız.

Peki, nasıl çözeceğiz bu işi?

Aslında bu problemin çözümüne hizmet edecek birkaç yöntem var. Benim burada göstereceğim çözüm, bunların içinden en basit olanı.

Bu yöntemde, özel bir durum oluşmasına sebep olan Save() metodunu ayrı bir abstract sınıf (veya interface) içerisine alacağız. Hadi burada Interface’i tercih edelim:

Böylece abstract DataSource sınıfımda yalnızca GetAllData metodunu bıraktım. Bu durumda ExcelFile ve DatabaseSource sınıflarım hem DataSource sınıfını miras alacak hem de IRecordable interfece’ini uygulayacak (implemente edecek). XMLSource sınıfım ise sadece DatabaseSource sınıfından türeyecek. Yani:

 

 

Sonuç olarak; DataSourceProcess sınıfımda SaveAll metodunu tekrar düzenleyerek LSP’ ye uygun bir hale getirebilirim.

 

Gördüğünüz gibi sevgili dostlarım; SaveAll metodunun son halinde, herhangi bir sınıfa özel bir durum söz konusu değil. IRecordable interface’ini uygulayan her tip birbirinin yerine kullanılabilir durumda.

Bu noktada bir yazımızın daha sonuna gelmiş bulunuyoruz. Bir sonraki yazımda görüşmek üzere kendinize iyi bakın.

Görüşlerinizi ve yorumlarınızı belirtmeyi unutmayın 🙂

26 thoughts on “Liskov Substitution Principle – LSP – ( Liskov Yerdeğiştirme Prensibi)

  1. Bir yazılımcı olarak belirtiyorum pek bir şey anlamadım ancak şuan için. Yazıyı özümleyebilmem için biraz zamana ihtiyacım var 🙂

  2. Biraz baş dönmesi ve mide bulantısı ile beraber 🙂 bana Dependency Injection'ı hatırlattı 🙂 neden bilmiyorum . Sanırım DataSourceProcess sınıfı içerisindeki SaveAll methoduna parametre olarak verilen List<IRecordable> kolleksiyonundan :). Bir yüzümü yıkayıp döneceğim 🙂

  3. Barbara abla ve uncle Bob'un ifadelerinden sonra sade ve basit anlatımın üzerine anlaşılır bir örnekle bitirmeniz süper olmuş, elinize sağlık.

  4. Hocam, DataSource sınıfı içince GetAllData() ve Save() methodlarını görüyorum. SOLID prensiplerini yeni yeni öğreniyorum, buna göre SRP'ye uymuyorsunuz yani DataSource tek bir sorumluluğu yerine getirmiyor, hem kayır dönüyor, hem de kaydedebiliyor. Bu durumda SRP kuralına uymadığınız düşünüyorum.

    SRP'yi doğru anlamış mıyım?

    1. Kesinlikle doğru anlamışsınız. Save ile Get işleminin farklı işlemler yaptığı düşünülürse, iki farklı sınıfa ayrılabilirdi. Ancak bildiğiniz gibi SRP sonsuza kadar da uygulanabilir 🙂

  5. Güzel makale, teşekkürler.
    Problemin çözümüne dair bahsettiğiniz diğer yöntemleride mümkün olursa paylaşırmısınız.

  6. Merhaba, güzel anlatmışsınız, elinize sağlık.
    Bir sorum olacak:
    DataSourceProcess class ı içindeki SaveAll metodunun parametresinin tipini List den List ye çevirdiğinizde bu metodu kullanırken eldeki veri kaynaklarının IRecordable olanlarının ayrıştırılması gerekliliği ortaya çıkacak sanıyorum.
    Bunun için SaveAll ın parametre tipi List olarak kalsa ve içeriğinide
    sourceList.ForEach(s => { if(s is IRecordable) (s as IRecordable).Save(); } );
    yapsak daha iyi olmaz mı ?

  7. Hocam çok anlaşılır bir makale olmuş sizi anladım fakat halen daha Liskov hanımın tanımını anlayamadım 🙂 Acaba sizi anlamış olmam yeterli mi 🙂 🙂

  8. “Aynı temel sınıftan türeyen tüm sınıflar, birbirlerinin yerine kullanılabilir olmalıdır.” Bu yanlış bir tanımlamadır
    Doğrusu: Alt sınıfların olduğu yere üst sınıflar sorun çıkarmdan gelebilmeli, olmalıydı. Downcasting’e gerek kalmadan üst sınıf üzerinden, alt sınıfların metotları çağırılabilmeli. LSP ihlali genellikle farklı metot imzaları taşıyan (override dolayısıyla) alt sınıfların olmasından dolayı yaşanır. Öyle ki, siz polimorfizm gereği temel sınıfda bulunan aynı adlı metot üzerinden alt sınıfın metodunu çağırmak istiyorsunuz ama alt sınıfda aynı adı taşıyan metodun imzası değişmiş. Dolayısyla downcasting yapıp, alt sınıfın metoduna doğrudan erişmek gerekliliği doğar. Bu bir ihlaldir. İhlali oluşturan başka şeyler de var aslında. Ya da alt sınıflar birden fazladır. Hepsi bazı farklı propertY’lere sahiptir. Bu property’lere değer atamak için gerçek sınıfı ele geçirmeniz gerekli. Bu da başka bir ihlal nedeni…

    1. Merhaba. Yorumunuz için teşekkürler. “Alt sınıflardan oluşturulan nesneler üst sınıfların nesneleriyle yer değiştirdiklerinde aynı davranışı göstermek zorundadırlar” cümlesi daha doğru olur aslına bakılırsa. Nitekim benim kurmuş olduğum cümleyi (birbirlerinin yerine kullanılabilme) yanlışlayan bir ifade değil bu. Sonuç olarak aslında aynı yere vurgu yapmaktayım. Casting, Type check ya da exception management kullanmak yerine doğrudan nesneyi atayabiliyorsanız prensibe uyuyorsunuz demektir.

      Zaman ayırıp yorum yazdığınız için tekrar teşekkürler.

  9. Şu yazıyı okuyana kadar anladım sanıyordum. Meğer olay bambaşkaymış. Eline emeğine sağlık. Örneğin çok güzel. Verdiğin örnek üzerinden çok rahat bir şekilde anlaşılıyor.

  10. Hocam “Aslında bu problemin çözümüne hizmet edecek birkaç yöntem var.” demişsiniz.

    Keşke diğer yöntemlerden de bahsetseydiniz. Bu yöntemler aynı soruna karşı birbirlerinin yerine kullanılabiliyor mu yoksa hepsinin soruna yönelik farklı bir yaklaşımı mı var? Veya farklı türdeki sorunlar için mi kullanılıyor bu çözümler?

    Teşekkürler.

  11. Türkay hocamın favori prensibi… 🙂 Prensibi kaleme alışınızın 10. yılında okudum, zannediyorum anladım ve selam bırakıyorum hocam, diğerleri gibi bu da çok açıklayıcı bir makale olmuş 🙂

  12. Şu ana kadar gördüğüm ve taradığım hiçbir kaynak bu kadar doğru ve güzel anlatamamıştı bu konuyu. Hatta basitleştireyim derken daha da karmaşıklaştıran yaklaşımları bile gördüm. Bravo hocam. Ellerine sağlık.

Leave a Reply