Liskov yer değiştirme ilkesi ihlal edilirse ne yanlış gidebilir?


27

Liskov Değişimi ilkesinin olası ihlali konusundaki bu yüksek oyu alan soruyu takip ediyordum . Liskov Değişimi ilkesinin ne olduğunu biliyorum ama aklımda hala net olmayan şey, geliştirici olarak nesne yönelimli kod yazarken prensip hakkında düşünmezsem, neyin yanlış gidebileceğidir.


6
LSP'yi takip etmezseniz ne yanlış gidebilir? En kötü senaryo: Code-thulhu! ;)
SinirliFormsDesigner

1
Bu orijinal sorunun yazarı olarak, bunun oldukça akademik bir soru olduğunu eklemek zorundayım. İhlaller kodda hatalara neden olsa da, hiçbir zaman LSP ihlaline neden olabileceğim ciddi bir hata veya bakım sorunu yaşamadım.
Paul T Davies

2
@Paul Böylece, kendiniz tasarlamadığınız ancak belki de uzatmak zorunda kaldığınız) oO hiyerarşileri nedeniyle temel sınıfın amacı konusunda belirsiz olan sözleşmelerin sağdan sola kırıldığı için programlarınızda hiçbir zaman sorun yaşamadınız başlamak için Seni kıskanıyorum! :)
Andres F.

@PaulTDavies, sonucun ciddiyetine, kullanıcıların (kütüphaneyi kullanan programcıların) kütüphanenin uygulaması hakkında ayrıntılı bilgiye sahip olup olmadığına (yani kütüphanenin koduna erişip erişmediğine) bağlıdır. LSP olmayanları (sınıfa özgü davranışlar) hesaplamak üzere kütüphanenin çevresinde. En kötü senaryo, kütüphane kapalı kaynaklı bir ticari ürün ise gerçekleşir.
rwong,

@Andres ve rwong, lütfen bu sorunları bir cevapla gösterin. Kabul edilen cevap, Paul Davies'i iyi bir derleyici, statik analizör veya minimum birim testiniz varsa sonuçların hızlıca fark edilip düzeltilmesi gibi küçük (Bir İstisna) gibi görünmesi nedeniyle desteklemektedir.
user949300

Yanıtlar:


31

Çok fazla oylanan sebeplerden biri olan bu soruda çok iyi ifade edildiğini düşünüyorum.

Şimdi bir Görevdeki Close () işlevini çağırırken, başlangıç ​​durumu olan bir ProjectTask ise, bir temel Görev olmasa bile çağrının başarısız olma olasılığı vardır.

Yapacağınızı düşünün:

public void ProcessTaskAndClose(Task taskToProcess)
{
    taskToProcess.Execute();
    taskToProcess.DateProcessed = DateTime.Now;
    taskToProcess.Close();
}

Bu yöntemde, zaman zaman .Close () çağrısı patlayacaktır, bu nedenle şimdi türetilmiş bir türün somut uygulamasına dayanarak, Görev'in olabilecek bir alt türü yoksa, bu yöntemin davranış biçimini, bu yöntemin nasıl yazılacağından değiştirmeniz gerekir. bu yönteme teslim edildi.

Liskov'un ikame ihlalleri nedeniyle, türünüzü kullanan kodun, bunları farklı şekilde ele almak için türetilmiş türlerin içsel işleyişiyle ilgili açıkça bilgi sahibi olması gerekir. Bu, sıkı bir şekilde kodları birleştirir ve uygulamanın tutarlı bir şekilde kullanılmasını zorlaştırır.


Bu, bir çocuk sınıfının ebeveyn sınıfında bildirilmeyen kendi genel yöntemlerine sahip olamayacağı anlamına mı geliyor?
Songo

@Songo: zorunlu değil: olabilir, ancak bu yöntemlerin bir temel göstergeden (veya başvuru veya değişken veya kullandığınız dilin adı ne olursa olsun) "erişilemez" ve nesnenin ne türünü sorgulamak için bir çalışma zamanı türü bilgisine ihtiyacınız var. Bu fonksiyonları çağırmadan önce Ancak bu, dillerin sözdizimi ve anlambilim ile yakından ilgili bir konudur.
Emilio Garavaglia 17:12

2
Hayır. Bu, bir alt sınıfa bir üst sınıfa aitmiş gibi atıfta bulunulduğu zamandır, bu durumda üst sınıfa dahil edilmemiş üyelere erişilemez.
Chewy Gumball

1
@Phil Yep; sıkı kaplin tanımı budur: Bir şeyi değiştirmek, diğer şeylerde değişikliklere neden olur. Gevşek bir şekilde birleştirilmiş bir sınıf, dışında kod değiştirmenizi gerektirmeden uygulamasının değiştirilmesini sağlayabilir. Sözleşmelerin iyi olmasının nedeni budur, nesnelerinizin tüketicilerinde değişiklik yapılmasını istememe konusunda size rehberlik ederler: Sözleşmeyi yerine getirin ve tüketicilerin herhangi bir değişiklik yapmasına gerek kalmayacak, böylece gevşek bağlantı sağlanacaktır. Tüketicilerinizin sözleşmeniz yerine uygulamanızı kodlaması gerektiğinde, bu sıkı bir birleşmedir ve LSP'yi ihlal ederken gerekir.
Jimmy Hoffa

1
@ user949300 İşini başarmak için herhangi bir yazılımın başarısı, kalitesinin, uzun vadeli veya kısa vadeli maliyetlerinin bir ölçütü değildir. Tasarım ilkeleri, yazılımın "çalışmasını" sağlamak için değil, yazılımın uzun vadeli maliyetlerini azaltmak için yönergeler getirme girişimleridir. İnsanlar hala çalışan bir çözümü uygulamada başarısız olduklarında istedikleri tüm prensipleri uygulayabilir ya da hiçbirini takip edip bir çalışma çözümü uygulayamazlar. Java koleksiyonları birçok insan için çalışsa da, bu uzun vadede onlarla çalışma maliyetinin olabildiğince ucuz olması anlamına gelmez.
Jimmy Hoffa,

13

Temel sınıfta tanımlanan sözleşmeyi yerine getirmezseniz, sonuçların düştüğü zaman işler sessizce başarısız olabilir.

Wikipedia eyaletlerinde LSP

  • Bir alt türde ön koşullar güçlendirilemez.
  • Son koşullar bir alt tipte zayıflayamaz.
  • Üst tipin değişmezleri bir alt tipte korunmalıdır.

Bunlardan herhangi birinin tutmaması durumunda, arayan beklemeyeceği bir sonuç alabilir.


1
Bunu göstermek için herhangi bir somut örnek düşünebilir misiniz?
Mark Booth,

1
@MarkBooth Circle elips / kare dikdörtgen sorunu göstermek için yararlı olabilir; wikipedia makalesi başlamak için iyi bir yer: en.wikipedia.org/wiki/Circle-ellipse_problem
Ed Hastings

7

Görüşme sorularının yıllıklarından klasik bir örnek düşünün: Circle'ı Elips'ten türettiniz. Niye ya? Çünkü bir daire IS-AN elips, elbette!

Bunun dışında ... elipsin iki işlevi vardır:

Ellipse.set_alpha_radius(d)
Ellipse.set_beta_radius(d)

Açıkça, bunlar Çember için yeniden tanımlanmalıdır, çünkü Çemberin düzgün bir yarıçapı vardır. İki ihtimaliniz var:

  1. Set_alpha_radius veya set_beta_radius çağrıldıktan sonra her ikisi de aynı miktarda ayarlanır.
  2. Set_alpha_radius veya set_beta_radius çağrıldıktan sonra, nesne artık bir Çember değildir.

Çoğu OO dili ikinciyi desteklemez ve iyi bir sebepten dolayı, Çevrenizin artık bir Çevresel olmadığını bulmak şaşırtıcı olacaktır. Yani ilk seçenek en iyisidir. Ancak, aşağıdaki işlevi göz önünde bulundurun:

some_function(Ellipse byref e)

Some_function fonksiyonunun e.set_alpha_radius dediğini hayal edin. Fakat e gerçekten bir Çember olduğundan, şaşırtıcı bir şekilde beta yarıçapı da ayarlanmıştır.

Ve burada ikame prensibi yatmaktadır: bir alt sınıf bir üst sınıf için ikame edilebilir olmalıdır. Aksi takdirde şaşırtıcı şeyler olur.


1
Değişken nesneler kullanırsanız başınızın belaya girebileceğini düşünüyorum. Bir daire aynı zamanda bir elips. Ancak, aynı zamanda başka bir elipsle çember olan bir elipsin yerini alırsanız (bu, bir belirleyici yöntem kullanarak yaptığınız şeydir), yeni elipsin de bir çember olacağının garantisi yoktur (çemberler elipslerin uygun bir alt kümesidir).
Giorgio

2
Tamamen işlevsel bir dünyada (değişmez nesnelerle) set_alpha_radius (d) yöntemi geri dönüş tipi elipse sahip olacaktır (hem elips hem de circle sınıfında).
Giorgio

@Giorgio Evet, bu sorunun yalnızca değişken nesnelerle ortaya çıktığını söylemeliydim.
Kaz Ejderha

@KazDragon: Bir elipsin Çember OLMADIĞINI bildiğimizde neden bir elipsi bir çember nesnesiyle değiştirir ki? Biri bunu yaparsa, modellemeye çalıştığı varlıkları doğru bir şekilde anlamazlar. Ancak bu değişime izin vererek, yazılımlarımızda modellemeye çalıştığımız temel sistemin eksik olduğunu ve dolayısıyla da kötü yazılımlar yarattığımızı, teşvik etmiyor muyuz?
maverick

@maverick Ben geriye doğru tarif ettiğim ilişkiyi okuduğuna inanıyorum. Önerilen bir ilişki bir başka yoldur: bir çember bir elipstir. Spesifik olarak, bir daire, alfa ve beta yarıçaplarının aynı olduğu bir elipstir. Ve bu nedenle beklenti, bir elipsin parametre olarak olmasını bekleyen herhangi bir fonksiyonun eşit olarak bir daire alabilmesi olabilir. Calculate_area (Elips) olarak düşünün. Buna dair bir daire geçmek aynı sonucu verir. Ancak sorun, Elips'in mutasyon işlevlerinin davranışının Circle'dakilerin yerine geçmemesidir.
Kaz Ejderi

6

Layman'ın sözleriyle:

Kodunuzda çok fazla CASE / switch cümlesi olacak .

Bu CASE / switch cümlelerinin her biri zaman zaman eklenen yeni durumlara ihtiyaç duyacaktır; bu, kod tabanının olması gerektiği gibi ölçeklenebilir ve bakımsız olmadığı anlamına gelir.

LSP, kodun donanım gibi çalışmasını sağlar:

İPod'unuzu değiştirmeniz gerekmez, çünkü hem eski hem de yeni harici hoparlörler aynı arabirime saygı duyduğundan yeni bir harici hoparlör çifti satın aldınız; iPod, istenen işlevselliği kaybetmeden değiştirilebilir.


2
-1: Kötü cevap etrafında
Thomas Eding 17:12

3
@ Tommas katılmıyorum. Güzel bir benzetme. O, LSP'nin ne olduğu ile ilgili beklentileri kırmamaktan bahsediyor. (dava / anahtarla ilgili bölüm biraz zayıf olsa da, katılıyorum)
Andres F.

2
Ve sonra Apple konektörleri değiştirerek LSP'yi bozdu. Bu cevap devam ediyor.
Magus

Anahtar ifadelerinin LSP ile ne yapması gerektiğini anlamıyorum. typeof(someObject)“ne yapmanıza izin verildiğine” karar vermek için geçiş yapmaktan bahsediyorsanız , o zaman kesin, ama bu tamamen başka bir anti-patern.
sara

Anahtar ifadelerinin miktarında belirgin bir azalma, LSP'nin arzu edilen bir yan etkisidir. Nesneler, aynı arayüzü genişleten başka bir nesneye dayanabileceğinden, özel durumların dikkate alınmasına gerek yoktur.
Tulains Córdova

1

Java'nın UndoManager'ı ile gerçek bir yaşam örneği vermek

devralır gelen AbstractUndoableEditkimin sözleşme belirtir o 2 durumu vardır ki (geri ve yeniden yapıldı) ve tek görüşmeleri ile aralarında gidebilir undo()veredo()

bununla birlikte UndoManager'ın daha fazla durumu vardır ve geri alma arabellekleri gibi davranır (her düzenlemeyi undobazılarını geri alır, ancak tüm düzenlemeleri geri alır, sonraki koşulu zayıflatır)

Bu, aramadan önce bir CompoundEdit'e bir UndoManager eklediğiniz, end()ardından CompoundEdit'in undo()düzenlemeleri kısmen geri almayı bıraktıktan sonra her düzenlemede çağrılmasını sağlayacak olan geri alma işleminin çağrılacağı varsayımsal duruma yol açar.

Bundan UndoManagerkaçınmak için kendimi yuvarladım (muhtemelen adını değiştirmeliyim UndoBuffer)


1

Örnek: Bir UI çerçevesi ile çalışıyorsunuz ve Controltemel sınıfı alt sınıflandırarak kendi özel UI kontrolünüzü yaratıyorsunuz . ControlTemel sınıf bir yöntemi tanımlar gereken iç içe denetimler topluluğu (varsa) döndürür. Ancak, aslında ABD başkanlarının doğum günlerinin listesini döndürme yöntemini geçersiz kılıyorsunuz.getSubControls()

Peki bu konuda ne yanlış gidebilir? Beklendiği gibi bir kontrol listesi döndürmediğiniz için kontrolün oluşturulmasının başarısız olacağı açıktır. Büyük olasılıkla UI çökecektir. Sen edilir sözleşme kırma Kontrol alt sınıfları uymaları beklenmektedir.


0

Ayrıca modelleme bakış açısıyla bakabilirsiniz. Bir sınıf örneğinin Aaynı zamanda bir sınıf örneği olduğunu söylerken, "bir sınıf örneğinin Bgözlemlenebilir davranışının, bir sınıf örneğinin Agözlemlenebilir davranışı olarak da sınıflandırılabileceğini " ima edersiniz B(Bu, yalnızca sınıf Bdaha az özel ise mümkündür. sınıf A.)

Dolayısıyla, LSP'yi ihlal etmek, tasarımınızda bazı çelişkilerin olduğu anlamına gelir: nesneleriniz için bazı kategoriler tanımlıyorsunuz ve ardından uygulamalarınızda onlara saygı duymuyorsunuz, bir şeyler yanlış olmalı.

Etiketi olan bir kutu yapmak gibi: "Bu kutu sadece mavi toplar içeriyor" ve ardından kırmızı bir top atıyor. Yanlış bilgi gösteriyorsa böyle bir etiketin kullanımı nedir?


0

Son zamanlarda içinde bazı büyük Liskov ihlal edenlerin bulunduğu bir kod temeli miras aldım. Önemli sınıflarda. Bu bana çok fazla acı verdi. Nedenini açıklamama izin ver.

Ben Class A, türetilen var Class B. Class Ave kendi uygulamasıyla geçersiz kılan Class Bbir dizi mülkü paylaşın Class A. Bir Class Amülkün ayarlanması veya elde edilmesi, aynı mülkün ayarlanması veya elde edilmesi üzerinde farklı bir etkiye sahiptir Class B.

public Class A
{
    public virtual string Name
    {
        get; set;
    }
}

Class B : A
{
    public override string Name
    {
        get
        {
            return TranslateName(base.Name);
        }
        set
        {
            base.Name = value;
            FunctionWithSideEffects();
        }
    }
}

Bunun .NET'te çeviri yapmak için tamamen berbat bir yol olduğu gerçeğini bir kenara bırakmak gerekirse, bu kodla ilgili başka birçok sorun var.

Bu durumda Name, birçok yerde bir indeks ve bir akış kontrol değişkeni olarak kullanılır. Yukarıdaki sınıflar, kod tabanı boyunca hem ham hem de türetilmiş formları ile doldurulur. Bu durumda Liskov ikame ilkesini ihlal etmek, temel sınıfı alan işlevlerin her birine yapılan her bir çağrının içeriğini bilmem gerektiği anlamına gelir.

Kod her ikisinin nesnelerini kullanır Class Ave Class Bbu yüzden Class Ainsanları kullanmaya zorlamak için soyut yapamam Class B.

Üzerinde çalışan bazı çok faydalı yardımcı işlevler Class Ave üzerinde çalışan diğer çok yararlı yardımcı işlevler vardır Class B. İdeal olarak, Class Aüzerinde çalışabilecek herhangi bir yardımcı işlevi kullanabilmek istiyorum Class B. LSP'nin ihlali için olmasaydı, Class Bbir çok fonksiyonu alan bir şeyi kolayca alabilirdi Class A.

Bununla ilgili en kötü şey, bu uygulamanın bu iki sınıfa dayandığı, her zaman her iki derste de çalıştığı ve yüzlerce şekilde bozulacağı için (ki bunu yapacağım) kırmak için gerçekten zor bir durum. Neyse).

Bunu düzeltmek için yapmam gereken , mülkün sürümü NameTranslatedolacak bir mülk oluşturmak ve yeni mülkümü kullanmak için türetilmiş mülke yapılan tüm referansları çok, çok dikkatli bir şekilde değiştiriyorum . Bununla birlikte, bu referanslardan bir tanesinin yanlış yapılması bile tüm başvuru patlayabilir.Class BNameNameNameTranslated

Kod tabanının çevresinde birim testlerinin bulunmadığı göz önüne alındığında, bu, geliştiricinin karşılaşabileceği en tehlikeli senaryo olmaya oldukça yakın. İhlali değiştirmezsem, her yöntemde ne tür bir nesnenin kullanıldığını takip ederek büyük miktarda zihinsel enerji harcamam gerekir ve ihlali giderirsem ürünün tamamını uygun olmayan bir zamanda patlatabilirim.


Eğer aynı adı [mesela, yuvalanmış sınıf] ve oluşturulan yeni tanımlayıcımız şey farklı bir tür ile kalıtsal özellik gölgeli türetilmiş sınıf içinde olsaydı acaba ne olurdu BaseNameve TranslatedNameerişim A sınıfı stili hem Nameve B sınıfı anlam? Daha sonra Name, bir tür değişkene erişme denemesi Bderleyici hatasıyla reddedilir, böylece tüm referansların diğer formlardan birine dönüştürülmesini sağlayabilirsiniz.
Süperkat

Artık o yerde çalışmıyorum. Düzeltmek çok garip olurdu. :-)
Stephen

-4

LSP'yi ihlal etme sorununu hissetmek istiyorsanız, yalnızca .dll / .jar temel sınıfının (kaynak kodun olmadığı) varsa ve yeni türetilmiş bir sınıf oluşturmalısınız. Bu görevi asla tamamlayamazsınız.


1
Bu sadece bir cevap olmak yerine daha fazla soru açar.
Frank,
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.