3 Eylül 2021 Cuma

SOLID Prensipleri -The Liskov Substitution Principle - Uncle Bob Çevirisi

Merhaba, bu seride sizlere Uncele Bob'un (Robert C. Martin) SOILD presnipleri için yazdığı makaleleri Türkçe'ye çevireceğim. Umarım yararlı bir yazı dizisi olur.

Seri'nin diğer yazıları :

The Single Responsibility Principle

Open Closed Principle

3- The Liskov Substitution Principle

GİRİŞ

Bu ilke, sürdürülebilir ve tekrar kullanılabilir kod oluşturma temelidir. İyi tasarlanmış kodun değiştirilmeden genişletilebileceğini belirtir; iyi tasarlanmış bir programda eski, halihazırda çalışan kodu değiştirmek yerine yeni kod eklenerek yeni özellikler eklenmesini amaç edinir.

Açık-Kapalı prensibinin ardındaki birincil mekanizmalar soyutlama ve polimorfizmdir. C ++ gibi statik olarak yazılmış dillerde, soyutlama ve polimorfizmi destekleyen temel mekanizmalardan biri kalıtımdır. Soyut temel sınıflarda saf sanal fonksiyonlar tarafından tanımlanan soyut polimorfik arayüzlere uyan türetilmiş sınıflar oluşturabiliriz.

Bu özel miras kullanımını yöneten tasarım kuralları nelerdir? En iyi miras hiyerarşilerinin özellikleri nelerdir? Açık-Kapalı prensibine uymayan hiyerarşiler yaratmamıza neden olacak tuzaklar nelerdir? Bunlar bu makalenin ele alacağı sorular olacak.

PRENSİP

POINTERLARI VEYA TEMEL SINIFLARIN REFERANSLARINI  KULLANAN FONKSİYONLAR, TÜREDİKLERİ SINIFLARIN NESNELERİNİ TİPLERİNİ BİLMEDEN KULLANABİLMELİDİRLER.

Yukarıdaki, Liskov İkame Prensibi'nin (LSP) bir açıklamasıdır. Barbara Liskov ilk olarak yaklaşık 8 yıl önce şunu yazmıştır:

Burada istenen şu ikame özelliğine benzer bir şeydir: S tipindeki her bir o1 nesnesi için T tipinde bir O2 nesnesi varsa, T olarak tanımlanan tüm P programları için, o1 o2 yerine ikame edildiğinde P'nin davranışı değişmiyorsa, o zaman S, T'nin bir alt tipidir.

Bu ilkenin önemi, ihlal etmenin sonuçlarını düşündüğünüzde belirginleşir. LSP'ye uymayan bir metod varsa, bu metod bir temel sınıfa bir işaretçi veya başvuru kullanır, ancak bu temel sınıfın tüm alt sınıflarını bilmelidir. Böyle bir işlev Açık-Kapalı ilkesini ihlal eder, çünkü temel sınıfın yeni bir alt sınıfı oluşturulduğunda değiştirilmesi gerekir.

LSP İhlaline Basit Bir Örnek

Bu ilkenin en göze çarpan ihlallerinden biri, bir nesnenin tipine dayalı bir metod seçmek için C ++ Çalışma Zamanı Türü Bilgilerinin (RTTI) kullanılmasıdır. yani .:

void DrawShape(const Shape& s)
{
if (typeid(s) == typeid(Square))
  DrawSquare(static_cast < square > (s)); 
else if (typeid(s) == typeid(Circle))
  DrawCircle(static_cast < circle > (s));
}

[Not: static_cast yeni cast operatörlerinden biridir. Bu örnekte normal bir cast gibi çalışır. yani DrawSquare ((Square &) s); Bununla birlikte, yeni sözdiziminin kullanımı daha güvenli ve grep gibi araçlarla bulunması daha katı kurallara sahiptir. Bu nedenle tercih edilir.]

Açıkçası DrawShape metodu kötü bir şekilde oluşturulmuştur. Shape sınıfının her olası alt sınıfı bilmeli ve yeni Shape alt sınıfları her oluşturulduğunda değiştirilmelidir. Gerçekten de birçoğu bu metodun yapısını Nesneye Dayalı Tasarıma lanetli olarak görmektedir.

Kare ve Dikdörtgen, Daha İnce Bir İhlal.

Ancak, LSP'yi ihlal etmenin çok daha incelikli başka yolları da vardır. Aşağıda açıklandığı gibi Rectangle sınıfını kullanan bir uygulama düşünün:



class Rectangle
{
	public:
		void SetWidth(double w) {itsWidth=w;}
		void SetHeight(double h) {itsHeight=w;}
		double GetHeight() const {return itsHeight;}
		double GetWidth() const {return itsWidth;}
	private:
		double itsWidth;
		double itsHeight;
};

Bu uygulamanın iyi çalıştığını ve birçok sitede kurulu olduğunu hayal edin. Tüm başarılı yazılımlarda olduğu gibi, kullanıcılarının ihtiyaçları değiştikçe yeni fonksiyonlara ihtiyaç duyulmaktadır. Bir gün kullanıcıların dikdörtgenlere ek olarak kareleri de işleme yeteneği talep ettiğini hayal edin.

C++'da kalıtımın ISA ilişkisi olduğu sıklıkla söylenir. Başka bir deyişle, yeni bir tür nesnenin eski tür bir nesneyle ISA ilişkisini yerine getirdiği söylenebilirse, o zaman yeni nesnenin sınıfı eski nesnenin sınıfından türetilmelidir.

Açıkça, bir kare, tüm normal niyet ve amaçlar için bir dikdörtgendir. ISA ilişkisi geçerli olduğundan, Square sınıfını Rectangle'dan türetilmiş olarak modellemek mantıklıdır.


ISA ilişkisinin bu kullanımı birçok kişi tarafından Nesne Yönelimli Analizin temel tekniklerinden biri olarak kabul edilir. Bir kare bir dikdörtgendir ve bu nedenle Square sınıfı, Rectangle sınıfından türetilmelidir. Bununla birlikte, bu tür bir düşünce, bazı ince, ancak önemli sorunlara yol açabilir. Genellikle bu sorun, biz uygulamayı gerçekten kodlamaya çalışana kadar öngörülemez. İlk ipucumuz, bir Square'in hem ItsHeight hem de itsWidth üye değişkenlerine ihtiyaç duymadığı gerçeği olabilir. Yine de onları yine de miras alacak. Açıkçası bu israf. Ayrıca, yüz binlerce Square nesnesi oluşturacaksak (örneğin, karmaşık bir devrenin her bileşeninin her pininin bir kare olarak çizildiği bir CAD/CAE programı), bu israf son derece önemli olabilir.

Ancak, bellek verimliliğiyle pek ilgilenmediğimizi varsayalım. Başka sorunlar var mı? Aslında! Square, SetWidth ve SetHeight işlevlerini devralır. Bir karenin genişliği ve yüksekliği aynı olduğundan, bu işlevler bir Kare için kesinlikle uygun değildir. Bu, tasarımda bir sorun olduğuna dair önemli bir ipucu olmalıdır. Ancak, sorunu ortadan kaldırmanın bir yolu var. Set-Width ve SetHeight'ı aşağıdaki gibi geçersiz kılabiliriz:

void Square::SetWidth(double w)
{
	Rectangle::SetWidth(w);
	Rectangle::SetHeight(w);
}
void Square::SetHeight(double h)
{
	Rectangle::SetHeight(h);
	Rectangle::SetWidth(h);
}
Şimdi, birisi bir Square nesnesinin genişliğini ayarladığında, yüksekliği de buna bağlı olarak değişecektir. Ve birisi yüksekliğini ayarladığında genişlik de onunla birlikte değişecektir. Böylece, Karenin değişmezleri bozulmadan kalır. Square nesnesi matematiksel olarak uygun bir kare olarak kalacaktır.
 
  Square s;
s.SetWidth(1); // Fortunately sets the height to 1 too.
s,SetHeight(2); // sets width and heigt to 2, good thing.

Ama aşağıdaki fonksiyonu bir düşünün

void f(Rectangle r)
{
r.SetWidth(32); // calls Rectangle::SetWidth
}

Bu fonksiyona bir Square nesnesine bir referans iletirsek, yükseklik değişmeyeceği için Square nesnesi bozulacaktır. Bu, LSP'nin açık bir ihlalidir. f işlevi, bağımsız değişkenlerinin türevleri için çalışmaz. Başarısızlığın nedeni, SetWidth ve SetHeight'ın Rectangle'da virtual olarak bildirilmemesidir.

Bunu kolayca düzeltebiliriz. Ancak, türetilmiş bir sınıfın oluşturulması, temel sınıfta değişiklik yapmamıza neden olduğunda, genellikle tasarımın hatalı olduğu anlamına gelir. Gerçekten de Açık-Kapalı ilkesini ihlal ediyor. Buna, SetWidth ve SetHeight'ı sanal yapmayı unutmanın gerçek tasarım hatası olduğu ve şu anda onu düzelttiğimiz iddiasıyla karşı çıkabiliriz. Ancak, bir dikdörtgenin yüksekliğini ve genişliğini ayarlamak son derece ilkel işlemler olduğundan, bunu haklı çıkarmak zordur. Square'in varlığını tahmin etmeseydik, hangi akıl yürütmeyle onları sanal hale getirirdik.

Yine de, argümanı kabul ettiğimizi ve sınıfları düzelttiğimizi varsayalım. Aşağıdaki kodla tamamlıyoruz:
 
   class Rectangle
	{
	public:
		virtual void SetWidth(double w) {itsWidth=w;}
		virtual void SetHeight(double h) {itsHeight=h;}
		double GetHeight() const {return itsHeight;}
		double GetWidth() const {return itsWidth;}
    private:
		double itsHeight;
	double itsWidth;
	};
	class Square : public Rectangle
	{
	public:
		virtual void SetWidth(double w);
		virtual void SetHeight(double h);
	};
	void Square::SetWidth(double w)
	{
		Rectangle::SetWidth(w);
		Rectangle::SetHeight(w);
	}
	void Square::SetHeight(double h)
	{
		Rectangle::SetHeight(h);
		Rectangle::SetWidth(h);
	}
   
   
Gerçek Sorun

Bu noktada, işe yarıyor gibi görünen Kare ve Dikdörtgen olmak üzere iki sınıfımız var. Bir Square nesnesine ne yaparsanız yapın, matematiksel bir kare ile tutarlı kalacaktır. Ve bir Rectangle nesnesine ne yaparsanız yapın, o matematiksel bir dikdörtgen olarak kalacaktır. Ayrıca, bir Dikdörtgen'e bir işaretçi veya referans kabul eden bir fonksiyona bir Kare iletebilirsiniz ve Kare yine bir kare gibi davranacak ve tutarlı kalacaktır.

Böylece, modelin artık kendi içinde tutarlı ve doğru olduğu sonucuna varabiliriz. Ancak bu sonuç yanlış olacaktır. Kendinden tutarlı bir model, tüm kullanıcıları ile mutlaka tutarlı olmak zorunda değildir! Aşağıdaki g fonksiyonunu düşünün.

void g(Rectangle& r)
{
	r.SetWidth(5);
	r.SetHeight(4);
	assert(r.GetWidth() * r.GetHeight()) == 20);
}

Bu işlev, bir Dikdörtgen olduğuna inandığı şeyin SetWidth ve SetHeight üyelerini çağırır. İşlev, bir Dikdörtgen için gayet iyi çalışır, ancak bir Kare iletilirse bir onaylama hatası bildirir. İşte asıl sorun şu: Bu işlevi yazan programcı, bir Dikdörtgenin genişliğini değiştirmenin yüksekliğini değiştirmediğini varsaymakta haklı mıydı?


Açıkçası, g programcısı bu çok makul varsayımı yaptı. Programcıları bu varsayımı yapan fonksiyonlara bir Kare iletmek problemlere yol açacaktır. Bu nedenle, Rectangle nesnelerine işaretçiler veya referanslar alan, ancak Square nesneleri üzerinde düzgün çalışamayan işlevler vardır. Bu işlevler, LSP'nin ihlal edildiğini ortaya çıkarır. Dikdörtgen'in Kare türevinin eklenmesi bu işlevi bozmuştur; ve böylece Açık-Kapalı ilkesi ihlal edilmiştir.


Geçerlilik İçsel Değildir

Bu bizi çok önemli bir sonuca götürüyor. Tek başına bakıldığında bir model anlamlı bir şekilde doğrulanamaz. Bir modelin geçerliliği ancak müşterileri açısından ifade edilebilir. Örneğin Square ve Rectangle sınıflarının son halini ayrı ayrı incelediğimizde kendi içinde tutarlı ve geçerli olduklarını gördük. Ancak onlara temel sınıf hakkında makul varsayımlarda bulunan bir programcının bakış açısından baktığımızda model bozuldu.

Bu nedenle, belirli bir tasarımın uygun olup olmadığı değerlendirilirken, çözüme tek başına bakmamak gerekir. Bu tasarımın kullanıcıları tarafından yapılacak makul varsayımlar açısından değerlendirilmelidir.

Ne yanlış gitti?

Peki ne oldu? Görünüşe göre makul olan Kare ve Dikdörtgen modeli neden kötü gitti? Sonuçta, Kare Dikdörtgen değil mi? ISA ilişkisi devam etmiyor mu?

Hayır! Bir kare bir dikdörtgen olabilir, ancak bir Square nesnesi kesinlikle bir Rectangle nesnesi değildir. Niye? Çünkü bir Square nesnesinin davranışı, bir Rectangle nesnesinin davranışıyla tutarlı değildir. Davranışsal olarak, Kare Dikdörtgen değildir! Ve yazılımın gerçekten ilgili olduğu şey davranıştır.

LSP, OOD'de ISA ilişkisinin davranışla ilgili olduğunu açıkça ortaya koymaktadır. İçsel özel davranış değil, dışsal kamusal davranış; clientlerın bağlı olduğu davranış. Örneğin, yukarıdaki g fonksiyonunun yazarı, Dikdörtgenlerin yükseklikleri ve genişlikleri birbirinden bağımsız olarak değişecek şekilde davranmasına bağlıydı. İki değişkenin bu bağımsızlığı, diğer programcıların muhtemelen bağımlı olduğu dışsal bir genel davranıştır.

LSP'nin ve onunla birlikte Açık-Kapalı ilkesini tutması için, tüm türevlerin, müşterilerin kullandıkları temel sınıflardan beklediği davranışa uyması gerekir.

Sözleşmeye Göre Tasarım (Design By Contract)


LSP ile Bertrand Meyer2 tarafından açıklandığı üzere Sözleşmeye Göre Tasarım kavramı arasında güçlü bir ilişki vardır. Bu şemayı kullanarak, sınıf metotları önkoşulları ve sonkoşulları bildirir. Yöntemin uygulanabilmesi için ön koşulların doğru olması gerekir. Tamamlandığında, yöntem, son koşulun doğru olacağını garanti eder.

Rectangle::SetWidth(double w) öğesinin son durumunu şu şekilde görebiliriz:

assert((itsWidth == w) && (itsHeight == old.itsHeight)); Şimdi, Meyer3 tarafından belirtildiği gibi, türevler için ön koşullar ve son koşullar için kural şudur:

...bir rutini [türevde] yeniden tanımlarken, yalnızca ön koşulunu daha zayıf olanla ve son koşulunu daha güçlü olanla değiştirebilirsiniz.

Başka bir deyişle, bir nesneyi temel sınıf arabirimi aracılığıyla kullanırken, kullanıcı yalnızca temel sınıfın ön koşullarını ve son koşullarını bilir. Bu nedenle, türetilmiş nesneler, bu tür kullanıcıların, temel sınıfın gerektirdiğinden daha güçlü olan ön koşullara uymasını beklememelidir. Yani, temel sınıfın kabul edebileceği her şeyi kabul etmeleri gerekir. Ayrıca, türetilmiş sınıflar, tabanın tüm son koşullarına uygun olmalıdır. Yani, davranışları ve çıktıları, temel sınıf için belirlenen kısıtlamaların hiçbirini ihlal etmemelidir. Temel sınıfın kullanıcıları, türetilmiş sınıfın çıktısıyla karıştırılmamalıdır.

Açıkça, Square::SetWidth(double w) öğesinin son koşulu, “(itsHeight == old.itsHeight)” temel sınıf yan tümcesine uymadığından, yukarıdaki Rectangle::SetWidth(double w) öğesinin son koşulundan daha zayıftır. Bu nedenle, Square::SetWidth(double w) temel sınıfın sözleşmesini ihlal eder.

Eiffel gibi bazı diller, ön koşullar ve son koşullar için doğrudan desteğe sahiptir. Bunları gerçekten bildirebilir ve çalışma zamanı sisteminin bunları sizin için doğrulamasını sağlayabilirsiniz. C++'ın böyle bir özelliği yoktur. Yine de C++'da bile her yöntemin ön koşullarını ve son koşullarını manuel olarak değerlendirebilir ve Meyer kuralının ihlal edilmediğinden emin olabiliriz. Ayrıca, bu ön koşulları ve son koşulları her bir yöntem için yorumlarda belgelemek çok yardımcı olabilir.

A real Example Kısmını çevirmedim. Orjinal makaleden gözatabilirsiniz.


ÖZET SONUÇ

Açık-Kapalı prensibi, OOD için yapılan iddiaların çoğunun kalbinde yer alır. Bu ilke yürürlükte olduğunda, uygulamalar daha sürdürülebilir, tekrar kullanılabilir ve sağlamdır. Liskov İkame İlkesi (Diğer adıyla Sözleşmeli Tasarım (Design by Contract)), Açık-Kapalı prensibine uyan tüm programların önemli bir özelliğidir. Sadece alt sınıflar temel tipleri için tamamen ikame edildiğinde, bu temel sınıfları kullanan metodlar cezasızlıkla yeniden kullanılabilir ve alt tipler cezasızlıkla değiştirilebilir.

Metnin Orjinali :


0 yorum: