SetWidth ve SetHeight yöntemlerini geçersiz kılarsak, Kare'yi Dikdörtgen'den devralmak neden sorunlu olsun?


105

Bir Kare bir Dikdörtgen türü ise, neden bir Kare Dikdörtgenden miras alamaz? Ya da neden kötü bir tasarım?

İnsanların söylediklerini duydum:

Kare'yi Dikdörtgen'den türetmişseniz, bir dikdörtgen beklediğiniz her yerde bir Kare kullanılabilir olmalıdır

Burada problem nedir? Ve neden bir dikdörtgen beklediğiniz her yerde Kare kullanılabilir? Yalnızca Square nesnesini oluşturursak ve Square için SetWidth ve SetHeight yöntemlerini geçersiz kılarsak neden herhangi bir sorun çıkacaksa kullanılabilir.

Rectangle ana sınıfınızda SetWidth ve SetHeight yöntemleriniz varsa ve Rectangle referansınız bir Kareye işaret ediyorsa, SetWidth ve SetHeight bir tane gösterecek şekilde ayarlayamaz, çünkü bir tane ayarlamak diğerine uyacak şekilde değişir. Bu durumda Kare, Liskov'un Dikdörtgen ile Yerine Geçme Testinde başarısız olur ve Kare'nin Dikdörtgen'den miras alması çok kötüdür.

Birisi yukarıdaki argümanları açıklayabilir mi? Yine, SetWidth ve SetHeight yöntemlerini Square'de aşarsak, bu sorunu çözmez mi?

Ayrıca duydum / okudum:

Asıl mesele, dikdörtgenleri modellemememiz değil, "yeniden şekillendirilebilir dikdörtgenler" yani, genişlik veya yüksekliği oluşturulduktan sonra değiştirilebilen dikdörtgenler (ve hala aynı nesne olarak görüyoruz). Dikdörtgen sınıfına bu şekilde bakarsak, bir karenin "yeniden şekillendirilebilir bir dikdörtgen" olmadığı açıktır, çünkü bir kare yeniden şekillendirilemez ve yine de kare olamaz (genel olarak). Matematiksel olarak, problemi görmüyoruz çünkü değişkenlik matematiksel bir bağlamda anlam ifade etmiyor bile

Burada "yeniden boyutlandırılabilir" in doğru terim olduğuna inanıyorum. Dikdörtgenler “yeniden boyutlandırılabilir” ve kareler de öyle. Yukarıdaki tartışmada bir şey eksik mi? Bir kare herhangi bir dikdörtgen gibi yeniden boyutlandırılabilir.


15
Bu soru çok soyut görünüyor. Sınıfları ve mirası kullanmanın, bazı sınıfların bir sınıftan miras almasının da iyi bir fikir olup olmadığı, genellikle bu sınıfları nasıl kullanmak istediğinize bağlı olan bir gazillion yolu vardır. Pratik bir durum olmadan, bu sorunun nasıl bir cevap alabileceğini göremiyorum.
aaaaaaaaaaaa

2
O kare olduğu hatırlanacak olursa bazı sağduyu kullanarak bir dikdörtgen bunu gereklidir nerede kare sınıfın nesnesi kullanılamaz eğer öyleyse, bir dikdörtgen muhtemelen yine bazı uygulama tasarımı kusurdur.
Cthulhu

7
Bence daha iyi soru Why do we even need Square? İki kalem olması gibi. Bir mavi kalem ve bir kırmızı mavi, sarı veya yeşil kalem. Mavi kalem fazlalıktır - kare olması durumunda, faydası olmadığından.
Gusdor

2
@ eBusiness Özeti, onu iyi bir öğrenme sorusu yapan şeydir. Hangi alt tip kullanımlarının belirli kullanım durumlarından bağımsız olarak kötü olduğunu fark edebilmek önemlidir.
Doval

5
@Cthulhu Gerçekten değil. Altyazı tamamen davranışlarla ilgilidir ve değişken bir kare değişken bir dikdörtgen gibi davranmaz. Bu nedenle "bir ..." metaforu kötüdür.
Doval

Yanıtlar:


189

Temel olarak olayların mantıklı davranmasını istiyoruz.

Aşağıdaki sorunu göz önünde bulundurun:

Bana bir grup dikdörtgen verildi ve alanlarını% 10 artırmak istiyorum. Yaptığım şey, dikdörtgenin uzunluğunu öncekinden 1.1 kat daha fazla ayarlamam.

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    rectangle.Length = rectangle.Length * 1.1;
  }
}

Şimdi bu durumda, tüm dikdörtgenlerim artık uzunluklarını% 10, alanlarını% 10 artıracak. Ne yazık ki, birisi aslında bana kareler ve dikdörtgenlerin bir karışımını iletti ve dikdörtgenin uzunluğu değiştiğinde, genişlik de öyle oldu.

Birim testlerim başarılı oldu çünkü tüm birim testlerimi bir dikdörtgen koleksiyonu kullanmak için yazdım. Şimdi başvurumda aylarca farkedilemeyecek ince bir böcek tanıttım.

Daha da kötüsü, muhasebeden Jim, yöntemimi görüyor ve kareleri benim yöntemime geçirirse, boyutunda% 21'lik çok güzel bir artış elde ettiği gerçeğini kullanan başka bir kod daha yazıyor. Jim mutlu ve kimse bilge değil.

Jim, mükemmel çalışmalar için farklı bir bölüme terfi etti. Alfred şirkete junior olarak katılır. İlk hata raporunda, Advertising'den Jill, karelerin bu yönteme geçmesinin% 21 artışla sonuçlandığını ve hatanın düzeltilmesini istediğini bildirdi. Alfred, Karelerin ve Dikdörtgenlerin kodun her yerinde kullanıldığını görür ve kalıtım zincirinin kırılmasının imkansız olduğunu fark eder. Ayrıca, Muhasebenin kaynak koduna da erişimi yok. Öyleyse Alfred böyle bir hatayı düzeltir:

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

Alfred, uber hack becerilerinden memnundur ve Jill, hatanın düzeltildiğini onaylar.

Gelecek ay kimse ödeme yapmıyor çünkü Muhasebe IncreaseRectangleSizeByTenPercentmetoda kareleri geçebilmeye ve% 21'lik bir alanda artış elde etmeye bağlıydı . Tüm şirket, sorunun kaynağını bulmak için "öncelik 1 hata düzeltme" moduna geçer. Sorunu Alfred'in düzeltmesine kadar takip ediyorlar. Hem Muhasebe hem de Reklamcılık'ı mutlu etmeleri gerektiğini biliyorlar. Böylece, kullanıcıyı böyle bir yöntem çağrısı ile tanımlayarak sorunu çözdüler:

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  IncreaseRectangleSizeByTenPercent(
    rectangles, 
    new User() { Department = Department.Accounting });
}

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles, User user)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle || user.Department == Department.Accounting)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    else if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

Ve diğerleri ve diğerleri.

Bu fıkra, günlük olarak programcıların karşılaştığı gerçek dünya durumlarına dayanmaktadır. Liskov Değişimi ilkesinin ihlali, yalnızca yazıldıktan yıllar sonra toplanan çok ince böcekler ortaya çıkarabilir; bu süre zarfında ihlali düzeltmek bir çok şeyi kıracak ve düzeltmemek en büyük müşterinizi kızdıracaktır.

Bu sorunu çözmenin iki gerçekçi yolu vardır.

İlk yol, Dikdörtgen'u değişmez kılmaktır. Dikdörtgen kullanıcısı Uzunluk ve Genişlik özelliklerini değiştiremezse, bu sorun çözülür. Farklı uzunluk ve genişlikte bir Dikdörtgen istiyorsanız, yeni bir tane oluşturun. Kareler dikdörtgenlerden mutlu bir şekilde miras alabilir.

İkinci yol ise kalıtım zincirini kareler ve dikdörtgenler arasında kırmaktır. Bir karenin tek bir SideLengthözelliğe sahip olması ve dikdörtgenlerin bir Lengthve Widthözelliğine sahip olması tanımlanırsa ve kalıtım olmazsa, bir dikdörtgen bekleyerek ve bir kare alarak şeyleri kazara kırmak imkansızdır. C # terimleriyle, aldığınız sealtüm Dikdörtgenlerin gerçekte Dikdörtgenler olmasını sağlayan dikdörtgen sınıfınız olabilir .

Bu durumda, sorunu çözmenin "değişmez nesneler" şeklini seviyorum. Bir dikdörtgenin kimliği uzunluk ve genişliktir. Bir nesnenin kimliğini değiştirmek istediğinizde, gerçekten istediğiniz şeyin yeni bir nesne olduğu anlamına gelir. Eski bir müşteriyi kaybeder ve yeni bir müşteri kazanırsanız, Customer.Idalanı eski müşteriden yenisine değiştirmezsiniz, yeni bir şey yaratırsınız Customer.

Liskov Değişimi ilkesinin ihlali gerçek dünyada yaygındır, çünkü çoğunlukla yetersiz olan / zaman baskısı altında olan / umursamayan / hata yapmayan kişiler tarafından yazılmış birçok kod vardır. Bazı çok kötü sorunlara yol açabilir ve açabilir. Çoğu durumda, miras yerine kompozisyonu tercih etmek istersiniz .


7
Liskov bir şey ve depolama başka bir konudur. Çoğu uygulamada, Dikdörtgen'den miras kalan bir Kare örneği, yalnızca bir tane gerekli olsa bile iki boyutun depolanması için alan gerektirir.
el.pescado

29
Noktayı göstermek için bir hikayenin mükemmel kullanımı
Rory Hunter

29
Güzel hikaye ama aynı fikirde değilim. Kullanım durumu şuydu: bir dikdörtgenin alanını değiştirin. Düzeltme, Square'de uzmanlaşmış olan dikdörtgene geçersiz kılınabilir bir 'ChangeArea' yöntemi eklemelidir. Bu, devralma zincirini kırmaz, kullanıcının ne yapmak istediğini açıkça belirtir ve bahsi geçen düzeltmenizin (uygun bir hazırlama alanında yakalanırsa) ortaya koyduğu hatanın ortaya çıkmasına neden olmaz.
Roy T.

33
@RoyT .: Neden bir Rectangle alanı nasıl ayarlayacağını bilmeli ? Bu tamamen uzunluk ve genişlikten türetilmiş bir özelliktir. Ve daha fazlası, hangi boyutta değişmelidir - uzunluk, genişlik veya her ikisi de?
cHao

32
@Roy T. Sorunu farklı bir şekilde çözdüğünüzü söylemek çok güzel, ancak gerçek şu ki - bunun basitleştirilmiş de olsa - geliştiricilerin eski ürünleri korurken günlük olarak karşılaştıkları gerçek dünya durumlarının bir örneği olduğu. Ve bu yöntemi uygulasanız bile, mirasçıların LSP'yi ihlal etmesini ve buna benzer hatalar vermesini engellemez. Bu nedenle, .NET çerçevesindeki her sınıf hemen hemen mühürlendi.
Stephen

30

Tüm nesneleriniz değişmez ise, sorun yoktur. Her kare aynı zamanda bir dikdörtgendir. Bir Dikdörtgenin tüm özellikleri ayrıca bir Kare'nin özellikleridir.

Nesneleri değiştirme yeteneği eklediğinizde sorun başlar. Ya da gerçekten - sadece özellik alıcıları okumak yerine, nesneye argümanları iletmeye başladığınızda.

Genişliği veya yüksekliği değiştirmek gibi, tüm Dikdörtgen sınıfınızın değişmezlerini koruyan bir Dikdörtgene yapabileceğiniz değişiklikler var. Birdenbire bir Dikdörtgenin davranışı sadece onun özellikleri değil, aynı zamanda olası değişikliklerdir. Bu sadece Dikdörtgenden çıkardığınız şey değil, aynı zamanda yerleştirebileceğiniz şeydir .

Dikdörtgenin, setWidthgenişliği değiştirdiği ve yüksekliği değiştirmeyen bir yöntemi varsa, Kare uyumlu bir yöntemi kullanamaz. Genişliği değiştirip yüksekliği değiştirmezseniz, sonuç artık geçerli bir Kare değildir. Kullanırken Meydanın hem genişliğini hem de yüksekliğini değiştirmeyi seçtiyseniz setWidth, Dikdörtgen'in özelliklerini uygulamıyorsunuzdur setWidth. Sadece kazanamazsın.

Bir Dikdörtgene ve Kareye ne koyabileceğinizi, hangi mesajları kendilerine gönderebileceğinizi görünce, muhtemelen bir Kareye geçerli olarak gönderebileceğiniz herhangi bir iletinin bir Dikdörtgene gönderebileceğinizi göreceksiniz.

Bu, bir varyansa karşı kontra-varyans meselesidir.

Üst sınıfın beklendiği her durumda örneklerin kullanılabileceği uygun bir alt sınıfın yöntemleri, her yöntemin şunları yapmasını gerektirir:

  • Yalnızca üst sınıfın döndüreceği değerleri döndür - yani, geri dönüş türü, üst sınıf yönteminin dönüş türünün bir alt türü olmalıdır. Dönüş ortak değişkendir.
  • Üst tipin kabul edeceği tüm değerleri kabul edin - yani argüman türleri, üst sınıf yönteminin argüman türlerinin üst tipleri olmalıdır. Argümanlar kontra-değişkendir.

Yani, Dikdörtgen ve Kare'ye dönelim: Kare'nin bir Dikdörtgenin alt sınıfı olup olmadığı, tamamen Dikdörtgenin sahip olduğu yöntemlere bağlıdır.

Rectangle genişlik ve yükseklik için ayrı ayarlayıcılara sahipse, Square iyi bir alt sınıf oluşturmaz.

Aynı şekilde, bazı metotların compareTo(Rectangle), Dikdörtgen ve compareTo(Square)Kare üzerinde olduğu gibi, argümanlarda ortak değişken olmalarını sağlarsanız , Kare'yi Dikdörtgen olarak kullanmada bir probleminiz olur.

Kare ve Dikdörtgeni uyumlu olacak şekilde tasarlarsanız, muhtemelen işe yarayacak, ancak birlikte geliştirilmeleri gerekir, yoksa işe yaramayacaklarına bahse girerim.


"Tüm nesneleriniz değişemezse, sorun yok" - bu açıkça, genişlik ve yükseklik için belirleyicilerden bahseden bu soru bağlamında anlamsız bir ifadedir
gnat

11
Bunu ilginç buldum, "görünüşte alakasız" olsa bile
Jesvin Jose

14
@gnat konuyla ilgili olduğunu iddia ediyorum, çünkü sorunun gerçek değeri, iki tür arasında geçerli bir alt yazı ilişkisi olduğunu kabul etmektir. Bu, süper tipin hangi operasyonları açıkladığına bağlıdır, bu yüzden mutasyon metotları ortadan kalkarsa sorunun ortadan kalktığını belirtmek gerekir.
Doval

1
@gnat da ayarlayıcılar mutasyona uğramışlar , bu yüzden aslında "Bunu yapma ve sorun değil" diyor. Basit tipler için değişmezliğe katılıyorum, ancak iyi bir noktaya değindiniz: Karmaşık nesneler için sorun o kadar basit değil.
Patrick M,

1
Bu şekilde düşünün, 'Rectangle' sınıfının garanti ettiği davranış nedir? Birbirinin genişliğini ve yüksekliğini BAĞIMSIZ olarak değiştirebilme. (yani setWidth ve setHeight) yöntemi. Şimdi Kare Dikdörtgen'den türetilmişse, Kare bu davranışı garanti etmelidir. Kare bu davranışı garanti edemediğinden, kötü bir kalıtımdır. Bununla birlikte, setWidth / setHeight yöntemleri Rectangle sınıfından kaldırılırsa, böyle bir davranış yoktur ve bu nedenle Square sınıfını Rectangle'dan türetebilirsiniz.
Nitin Bhide

17

Burada birçok iyi cevap var; Stephen'ın özellikle cevabı, ikame ilkesinin ihlal edilmesinin neden takımlar arasında gerçek dünya çatışmalarına yol açtığını göstermek için iyi bir iş çıkarır.

LSP'nin diğer ihlalleri için bir metafor olarak kullanmak yerine, dikdörtgenlerin ve karelerin kendine özgü problemi hakkında kısaca konuşabileceğimi düşündüm.

Nadiren söz edilen, özel bir kare olan dikdörtgen türüyle ilgili ek bir sorun var ve bu da: Neden kareler ve dikdörtgenlerle duruyoruz ? Bir karenin özel bir dikdörtgen olduğunu söylemek istiyorsak, o zaman kesinlikle şunu söylemeliyiz:

  • Kare, özel bir eşkenar dörtgen türüdür - kare açılı bir eşkenar dörtgendir.
  • Eşkenar dörtgen, özel bir paralelkenar türüdür - eşit kenarlı bir paralelkenar.
  • Bir dikdörtgen, özel bir paralelkenar türüdür - kare açılı bir paralelkenar
  • Dikdörtgen, kare ve paralelkenarların hepsi özel bir yamuk türüdür - iki paralel tarafa sahip yamuklardır
  • Yukarıdakilerin hepsi özel dörtgen tipleridir.
  • Yukarıdakilerin tümü özel düzlemsel şekil türleridir
  • Ve bunun gibi; Burada bir süre daha devam edebilirim.

Bütün ilişkiler burada ne olmalı? C # veya Java gibi sınıf devralma temelli diller, çok sayıda farklı türden kısıtlamayla bu tür karmaşık ilişkileri temsil edecek şekilde tasarlanmamıştır. Tüm bunları, alt ilişki ilişkileri olan sınıflar olarak göstermeye çalışarak sorunu tamamen ortadan kaldırmak en iyisidir.


3
Eğer şekiller nesneleri değişken değilse, biri IShapesınırlayıcı bir kutu içeren bir tipe sahip olabilir ve çizilebilir, ölçeklenebilir ve serileştirilebilir ve IPolygonköşelerin sayısını ve bir geri döndürme yöntemini bildirmek için bir yöntem içeren bir alt tip olabilir IEnumerable<Point>. Bir sonra olabilir IQuadrilateralkaynaklanmaktadır alt türü IPolygon, IRhombusve IRectanglebunların birinden türetmek ve ISquaretürer IRhombusve IRectangle. Değişebilirlik her şeyi pencereden dışarı atar ve çoklu kalıtım sınıflarla çalışmaz, ancak değişmez arayüzler için iyi olduğunu düşünüyorum.
Supercat

Burada Eric ile etkili bir şekilde aynı fikirdeyim (-1 için yeterli değil!). Tüm bu ilişkiler (muhtemelen), supercat'ın da belirttiği gibi; Bu sadece bir YAGNI sorunudur: İhtiyacınız olana kadar uygulamıyorsunuz.
Mark Hurd

Çok iyi cevap! Çok daha yüksek olmalı.
andrew.fox

1
@MarkHurd - bu bir YAGNI meselesi değil: önerilen kalıtım temelli hiyerarşi, tarif edilen taksonomi şeklindedir, ancak onu tanımlayan ilişkileri garanti etmek için yazılamaz. Nasıl yok IRhombushepsi garanti Pointdöndü Enumerable<Point>tarafından tanımlanan IPolygoneşit uzunlukta kenarlara tekabül? IRhombusArayüzün tek başına uygulanması somut bir nesnenin eşkenar dörtgen olduğunu garanti etmediğinden, miras cevap olamaz.
A. Rager

14

Matematiksel açıdan bakıldığında, kare bir dikdörtgendir. Bir matematikçi kareyi değiştirir, böylece artık kare sözleşmeye uymaz, bir dikdörtgene dönüşür.

Ancak OO tasarımında bu bir problemdir. Bir nesne, ne olduğu ve bu durumun yanı sıra davranışları da içerir . Kare bir nesneyi tutarsam, ancak bir başkası onu dikdörtgen olacak şekilde değiştirirse, karenin sözleşmesini ihlal etmemden başka bir şey yapmaz. Bu, her türlü kötü şeyin olmasına neden olur.

Buradaki anahtar faktör değişkenliktir . Bir şekil kurulduktan sonra değişebilir mi?

  • Değişebilir: Şekiller oluşturulduktan sonra değişmelerine izin verilirse, kare, dikdörtgen ile is-a ilişkisine sahip olamaz. Bir dikdörtgenin kontratı, karşıt tarafların eşit uzunlukta olması gerektiği, ancak bitişik tarafların olması gerekmemesi sınırlamasını içerir. Kare dört eşit tarafa sahip olmalıdır. Bir kareyi bir dikdörtgen arabirim aracılığıyla değiştirmek kare sözleşmesini ihlal edebilir.

  • Değiştirilemez: şekiller bir kez oluşturulduktan sonra değişemezse, kare bir nesnenin de her zaman dikdörtgen sözleşmesini yerine getirmesi gerekir. Bir karenin dikdörtgenle bir ilişkisi olabilir.

Her iki durumda da, bir ya da daha fazla değişiklikle kendi durumuna göre yeni bir şekil üretmesi için bir kare istemek mümkündür. Örneğin, biri "bu kareye dayalı yeni bir dikdörtgen oluştur, zıt tarafların A ve C'nin iki katı olması dışında" diyebilir. Yeni bir nesne inşa edildiğinden, orijinal kare sözleşmelerine uymaya devam ediyor.


1
This is one of those cases where the real world is not able to be modeled in a computer 100%. Neden öyle? Fonksiyonel bir kare ve dikdörtgen modelimiz olabilir. Tek sonuç, bu iki nesne üzerinde soyutlamak için daha basit bir yapı aramamız gerektiğidir.
Simon Bergot

6
Dikdörtgenler ve kareler arasında bundan daha yaygın olanı vardır. Sorun, bir dikdörtgenin kimliğinin ve bir karenin kimliğinin yan uzunlukları (ve her kesişme noktasındaki açı) olmasıdır. Buradaki en iyi çözüm, karelerin dikdörtgenlerden miras alınması, ancak her ikisinin de değişmez hale getirilmesidir.
Stephen

3
@Stephen Anladım. Aslında, onları değişmez kılmak, altyazı problemlerinden bağımsız olarak yapılacak mantıklı şeydir. Bunları değişken hale getirmek için hiçbir neden yok - yeni bir kare veya dikdörtgen oluşturmak için birini değiştirmek zor değil, o zaman neden solucanlar olabilir? Artık takma / yan etkiler konusunda endişelenmenize gerek yok ve gerektiğinde bunları harita / dikte etmek için anahtar olarak kullanabilirsiniz. Bazıları, sıcak noktanın şekil kodunda olduğunu ölçüp kanıtlayana kadar "erken optimizasyon" diyeceğim "performans" diyecek.
Doval

Üzgünüm çocuklar, geç oldu ve cevabı yazarken çok yorgundum. İşin aslı ne demek istediğimi söylemek için yeniden yazdım, zekası değişkenliktir.

13

Ve neden bir dikdörtgen beklediğiniz her yerde Kare kullanılabilir?

Çünkü bu bir alt tip olmanın bir parçası (ayrıca bakınız: Liskov ikame prensibi). Yapabilirsin, bunu yapabilmelisin:

Square s = new Square(5);
Rect r = s;
doSomethingWith(r); // written assuming a Rect, actually calls Square methods

Bunu gerçekten OOP kullanırken her zaman (bazen daha da dolaylı olarak) yaparsınız.

ve Kare için SetWidth ve SetHeight yöntemlerini aşarsak neden herhangi bir sorun çıkacak?

Çünkü bunları duyarlı bir şekilde geçersiz kılamazsınız Square. Bir kare Çünkü olamaz "Herhangi dikdörtgen gibi yeniden boyutlandırılması". Bir dikdörtgenin yüksekliği değiştiğinde, genişlik aynı kalır. Ancak bir karenin yüksekliği değiştiğinde, genişliğin buna göre değişmesi gerekir. Sorun sadece yeniden boyutlandırmakla kalmıyor, her iki boyutta bağımsız olarak yeniden boyutlandırılıyor.


Pek çok dilde, hatta hatta ihtiyacınız yok Rect r = s;, sadece yapabilirsiniz doSomethingWith(s)ve çalışma zamanı sherhangi bir sanal Squareyöntemi çözmek için herhangi bir çağrıyı kullanır .
Patrick M,

1
@PatrickM Alt mantıklı herhangi bir mantıklı dilde buna ihtiyacınız yoktur. Açıklığa kavuşturmak için bu çizgiyi dahil ettim.

Yani geçersiz kıl setWidthve setHeighthem genişliği hem de yüksekliği değiştir.
Yaklaşan yaklaşanlıkFish

@ValekHalfHeart Bu tam olarak düşündüğüm seçenek.

7
@ValekHalfHeart: Bu, tam olarak size düşecek ve kodun nasıl çalışması gerektiğini unuttuğunuzda iki yıl sonra garip bir hata bulmaya çalışırken uykusuz birkaç gece geçirmenizi sağlayacak olan Liskov Değiştirme İlkesi'nin ihlalidir.
Jan Hudec

9

Tanımladığınız şey, Liskov Değiştirme Prensibi olarak adlandırılan şeyden kaynaklanıyor . LSP'nin temel fikri, ne zaman belirli bir sınıfın bir örneğini kullanırsanız, her zaman, bu sınıfa ait herhangi bir alt sınıfın örneğinde, herhangi bir hata vermeden takas edebilmeniz gerektiğidir .

Dikdörtgen-Kare sorunu, Liskov'u tanıtmanın gerçekten iyi bir yolu değil. Aslında oldukça ince olan bir örneği kullanarak geniş bir prensibi açıklamaya çalışır ve tüm matematiğin en yaygın sezgisel tanımlarından birini oluşturur. Bazıları bu yüzden Elips Çemberi problemi olarak adlandırıyor, ancak bu sadece mümkün olduğu kadar biraz daha iyi. Paralelkenar-Dikdörtgen problemi dediğim şeyi kullanarak, biraz geri adım atmak daha iyi bir yaklaşım olacaktır. Bu, anlaşılması çok daha kolay şeyler yapar.

Bir paralelkenar iki çift paralel tarafı olan bir dörtgendir. Aynı zamanda iki çift eş açıya sahiptir. Paralelkenar nesnesini şu satırlar boyunca hayal etmek zor değil:

class Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

Bir dikdörtgeni düşünmenin yaygın bir yolu, dik açılı bir paralelkenar gibidir. İlk bakışta, bu Dikdörtgen'i Paralelkenar'dan miras almak için iyi bir aday yapıyor gibi görünebilir , böylece tüm bu nefis kodu yeniden kullanabilirsiniz. Ancak:

class Rectangle extends Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* BUG: Liskov violations ahead */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

Bu iki işlev neden Rectangle'da hatalar ortaya çıkarmaktadır? Sorun şu ki, bir dikdörtgendeki açıları değiştiremezsiniz : her zaman 90 derece olarak tanımlanırlar ve bu yüzden bu arabirim Paralelkenar'dan miras kalan Dikdörtgen için gerçekten çalışmaz. Bir Dikdörtgeni Paralelkenar bekleyen bir kodla değiştirirsem ve bu kod açıyı değiştirmeye çalışırsa, neredeyse kesinlikle hatalar olacaktır. Alt sınıfta yazılabilir bir şey aldık ve salt okunur hale getirdik, bu bir Liskov ihlali.

Şimdi, bunu Kareler ve Dikdörtgenler için nasıl uygular?

Bir değer belirleyebileceğinizi söylediğimizde, genellikle sadece bir değer yazabilmekten biraz daha güçlü bir şey ifade ediyoruz. Belli bir münhasırlık derecesine işaret ediyoruz: bir değer belirlerseniz, o zaman bazı olağanüstü durumları yasaklarsanız, yeniden belirleyene kadar bu değerde kalacaktır. Yazılabilecek ancak ayarlanmayan değerler için birçok kullanım alanı vardır, ancak bir kez ayarladığınız yerde kaldığınız değere bağlı olan birçok durum vardır. Ve orası başka bir sorunla karşılaştığımız yer.

class Square extends Rectangle {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};

    /* BUG: More Liskov violations */
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* Liskov violations inherited from Rectangle */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

Square sınıfımız, Rectangle’dan gelen böcekleri devraldı, ancak bazı yenileri var. SetSideA ve setSideB ile ilgili sorun, bunların hiçbirinin artık gerçekten ayarlanamamasıdır: ikisinden birine yine de bir değer yazabilirsiniz, ancak diğeri yazılırsa alttan değişecektir. Bunu bir Paralelkenar için, birbirlerinden bağımsız olarak yanlar koymaya bağlı olan bir kodla değiştirirsem çıldırır.

Mesele budur ve bu nedenle Listangle’ya giriş olarak Dikdörtgen-Kare’yi kullanmakta bir sorun var. Dikdörtgen-Kare , bir şeye yazabilmek ile onu ayarlayabilmek arasındaki farka bağlıdır ve bu, salt okunur olmasına karşı bir şeyi ayarlayabilmekten çok daha ince bir farktır. Dikdörtgen-Kare hala göz kulak gereken bir oldukça yaygın gotcha belgeler, çünkü örnek olarak değeri vardır, ancak bir şekilde kullanılmamalıdır tanıtım örnek. Öğrencinin önce temelleri biraz temel almasına izin verin, sonra da onlara daha sert bir şeyler atayın.


8

Alt yazı davranışla ilgilidir.

BYazmanın bir alt tür olması için, yazının aynı anlambilimde Adesteklediği her işlemi A("davranış" için süslü konuşma) desteklemesi gerekir. Gerekçesini kullanarak her B A gelmez değil davranış uyumluluğu son söz - çalışırlar. “B bir tür A” dır, çoğu zaman “B, A gibi davranır”, ancak her zaman değil .

Bir örnek:

Gerçek sayılar kümesini düşünün. Herhangi bir dilde, biz onları operasyonları desteklemek için bekleyebilirsiniz +, -, *, ve /. Şimdi pozitif tamsayılar kümesini düşünün ({1, 2, 3, ...}). Açıkçası, her pozitif tamsayı da gerçek bir sayıdır. Ancak, pozitif tamsayıların türü, gerçek sayı türlerinin bir alt tipi midir? Dört işleme bakalım ve pozitif tamsayıların gerçek sayılarla aynı şekilde davranıp davranmadığını görelim:

  • +: Sorunsuz tam sayılar ekleyebiliriz.
  • -: Pozitif tamsayıların tüm çıkarmaları pozitif tamsayılarla sonuçlanmaz. Örn 3 - 5.
  • *: Olumlu tamsayıları problemsizce çarpabiliriz.
  • /: Her zaman pozitif tamsayıları bölemez ve pozitif bir tamsayı elde edemeyiz. Örn 5 / 3.

Bu yüzden pozitif tamsayılar gerçek sayıların alt kümesi olmasına rağmen, bunlar bir alt tip değildir. Benzer bir argüman sonlu büyüklükte tamsayılar için yapılabilir. Açıkçası, her 32 bit tam sayı da 64 bit tam sayıdır, ancak 32_BIT_MAX + 1her tür için size farklı sonuçlar verecektir. Eğer size bir program verirsem ve her 32-bit tamsayı değişkeninin türünü 64-bit tamsayı olarak değiştirdiyseniz, programın farklı davranması (neredeyse her zaman yanlış anlamına gelir ) için iyi bir şans var .

Tabii ki, +sonuç 64-bit bir tamsayı olacak şekilde 32 bitlik girişler için tanımlayabilirsiniz , ancak şimdi her iki 32 bitlik sayı eklediğinizde 64 bitlik alan ayırmanız gerekir. Bellek gereksinimlerinize bağlı olarak bu sizin için kabul edilebilir olabilir veya olmayabilir.

Bu neden önemli?

Programların doğru olması önemlidir. Bir program için sahip olunabilecek en önemli özellik tartışmalı. Bir program bazı tür için doğru ise A, tek yolu bazı alt tipi için bu programın doğru olmaya devam edecektir garanti Beğer olduğunu Bgibi davranır Aher şekilde.

Yani Rectanglesözellikleri taraflarının bağımsız olarak değiştirilebileceğini söyleyen bir türünüz var . RectanglesUygulamayı kullanan ve uygulamanın şartnameye uyduğunu varsayan bazı programlar yazdınız. Sonra adında bir alt türü tanıtıldı Squareolan taraf olamaz bağımsız yeniden boyutlandırılabilir. Sonuç olarak, dikdörtgenleri yeniden boyutlandıran çoğu program şimdi yanlış olacaktır.


6

Bir Kare bir Dikdörtgen türüdür, neden bir Kare Dikdörtgenden miras alamaz? Ya da neden kötü bir tasarım?

İlk önce, bir karenin neden bir dikdörtgen olduğunu düşündüğünüzü kendinize sorun .

Elbette çoğu insan bunu ilkokulda öğrendi ve çok açık gözüküyordu. Bir dikdörtgen, 90 derece açılı 4 taraflı bir şekildir ve bir kare tüm bu özellikleri yerine getirir. Yani bir kare bir dikdörtgen değil mi?

Yine de, nesnelerin gruplandırılmasında ilk kriterinizin ne olduğuna, bu nesnelere hangi bağlamda baktığınıza bağlı. Geometride, şekiller nokta, çizgi ve meleklerin özelliklerine göre sınıflandırılır.

Demek ki "kare bir tür dikdörtgendir" demeden önce kendinize sormanız gereken şey, bu benim umrumda olan kriterlere dayanıyor .

Çoğu durumda, umursadığınız şey bu olmayacak. GUI'ler, grafikler ve video oyunları gibi şekilleri modelleyen sistemlerin çoğu, öncelikle bir nesnenin geometrik gruplandırması ile ilgilenmez, ancak davranıştır. Hiç bir karenin geometrik anlamda bir dikdörtgen olması önemli olduğu bir sistem üzerinde çalıştınız mı? Bunun 4 tarafı ve 90 derecelik açıları olduğunu bilerek size ne verirdi?

Statik bir sistemi modellenmiyorsunuz, şeylerin olacağı dinamik bir sistemi modelleniyorsunuz (şekiller yaratılacak, tahrip edilmiş, değiştirilmiş, çizilmiş vb.). Bu bağlamda, nesneler arasındaki paylaşılan davranışları önemsiyorsunuz, çünkü birincil endişeniz bir şekille yapabilecekleriniz, hala tutarlı bir sisteme sahip olmak için hangi kuralların sürdürülmesi gerekiyor.

Bu bağlamda bir kare kesinlikle bir dikdörtgen değildir , çünkü karenin nasıl değiştirilebileceğini yöneten kurallar dikdörtgen ile aynı değildir. Yani aynı şey değiller.

Bu durumda, onları bu şekilde modellemeyin. Neden sen Bu size gereksiz bir kısıtlamadan başka bir şey kazandırmaz.

Yalnızca Square nesnesini oluşturursak ve Square için SetWidth ve SetHeight yöntemlerini geçersiz kılarsak neden herhangi bir sorun çıkacaksa kullanılabilir.

Bunu yaparsanız pratikte kodda aynı şey olmadığını belirten olsa. Kodunuz bir karenin bu şekilde davrandığını ve bir dikdörtgenin bu şekilde davrandığını söylüyor, ama yine de aynı.

İki farklı davranış tanımladığınız için önemsediğiniz bağlamda açıkça aynı değillerdir. Öyleyse neden sadece umursamadığınız bir bağlamda benzerlerse aynı gibi davranıyorlar?

Bu, geliştiriciler modellemek istedikleri bir alana geldiklerinde önemli bir sorunu vurgular. Etki alanındaki nesneler hakkında düşünmeye başlamadan önce hangi bağlamla ilgilendiğinizi açıklığa kavuşturmak çok önemlidir. Hangi yönüyle ilgileniyorsunuz? Binlerce yıl önce Yunanlılar, çizgilerin ve şekillerin meleklerinin ortak özelliklerini önemsemiş ve bunları temel alarak gruplandırmıştır. Bu, umrunda değil (eğer yazılımdaki modelleme zamanının% 99'unda umursamayacağınız) değilse, o gruba devam etmek zorunda olduğunuz anlamına gelmez.

Bu sorunun cevaplarının çoğu, alt grup yazmaya gruplama davranışı ile ilgili olmaya odaklanır 'çünkü onlara kurallar getirilir .

Ancak bunu sadece kurallara uymak için yapmadığınızı anlamak çok önemlidir. Bunu yapıyorsunuz, çünkü çoğu durumda bu aslında sizin de neye değer verdiğinizdir. Bir kare ve dikdörtgenin aynı iç melekleri paylaşması umrunda değil. Hala kare ve dikdörtgen olurken neler yapabildiklerini umursuyorsun. Nesnelerin davranışını önemsersiniz, çünkü nesnelerin davranış kurallarına göre sistemi değiştirmeye odaklanan bir sistemi modelliyorsunuzdur.


Eğer tür değişkenleri Rectangleyalnızca değerleri temsil etmek için kullanılıyorsa , bir sınıfın sözleşmesinden Squaremiras kalan Rectangleve tam olarak uyması mümkün olabilir . Ne yazık ki, birçok dil, değerleri içeren değişkenler ile varlıkları tanımlayanlar arasında herhangi bir ayrım yapmamaktadır.
Supercat,

Muhtemelen, ama sonra ilk etapta neden rahatsız. Dikdörtgen / kare probleminin amacı, "kare bir dikdörtgendir" ilişkisinin nasıl çalışacağını bulmak değil, nesnelerin kullandığınız bağlamda ilişkinin gerçekten varolmadığının farkına varmaktır. (davranışsal olarak) ve etki alanınıza alakasız ilişkiler kurmama konusunda bir uyarı olarak.
Cormac Mulhall

Ya da başka bir deyişle: Kaşığı bükmeye çalışmayın. Bu imkansız. Bunun yerine sadece gerçeği anlamaya çalışın, kaşık yoktur. :-)
Cormac Mulhall

1
Bir değişmez Squaretipten miras kalan değişmez bir Rectnagletipe sahip olmak, sadece kareler üzerinde yapılabilecek bazı işlem türleri olsaydı faydalı olabilirdi. Konseptin gerçekçi bir örneği olarak, bir ReadableMatrixtür [temel tip, seyrek de dahil olmak üzere çeşitli şekillerde saklanabilecek dikdörtgen bir dizi] ve bir ComputeDeterminantyöntem düşünün . Bu sahip olmak mantıklı olabilir ComputeDeterminantancak birlikte çalışmayı ReadableSquareMatrixtüretilmiş oluyor tip ReadableMatrixbir örneğidir olarak düşündüğünüz, Squarebir gelen türetme Rectangle.
supercat

5

Bir Kare bir Dikdörtgen türüdür, neden bir Kare Dikdörtgenden miras alamaz?

Sorun, işler gerçekte bir şekilde ilişkiliyse, modellemeden sonra da aynı şekilde ilişkili olmaları gerektiği düşüncesinde yatmaktadır.

Modellemede en önemli şey, ortak özellikleri ve ortak davranışları tanımlamak, bunları temel sınıfta tanımlamak ve alt sınıflara ek özellikler eklemektir.

Örnekteki problem, tamamen soyut. Kimse bilmediği sürece, bu sınıfları ne için kullanmayı planlıyorsunuz, hangi tasarımı yapmanız gerektiğini tahmin etmek zor. Ancak gerçekten yalnızca yükseklik, genişlik ve yeniden boyutlandırma olmasını istiyorsanız, daha mantıklı olacaktır:

  • Square'i temel sınıf olarak tanımlayın , verilen faktörle widthparametreyi ve resize(double factor)genişliği yeniden boyutlandırın
  • Rectangle sınıfını ve Square alt sınıfını tanımlayın, çünkü başka bir öznitelik ekler ve yüksekliği, belirtilen faktör tarafından yüksekliği çağıran ve yeniden boyutlandıran işlevini heightgeçersiz kılarresizesuper.resize

Programlama bakış açısından, Kare içinde, Dikdörtgen'in sahip olmadığı hiçbir şey yoktur. Dikdörtgenin alt sınıfı olarak Kare yapmanın anlamı yok.


+1 Bir kare matematikte özel bir rektiftir, OO'da aynı olduğu anlamına gelmez.
Lovis

1
Bir kare bir kare ve bir dikdörtgen bir dikdörtgendir. Aralarındaki ilişkiler modellemede de geçerli olmalı, yoksa oldukça zayıf bir modeliniz var. Asıl meseleler şunlardır: 1) onları değişken kılarsanız, artık kareleri ve dikdörtgenleri modellemiyorsunuz; 2) sadece bazı "bir" ilişki olduğu için iki tür nesne arasında olduğu varsayılırsa, birini ayırt etmeksizin yerine diğerinin yerini alabilir.
Doval

4

Zira LSP ile ikisi ile baştan sona devralma arasında bir miras ilişkisi oluşturmak setWidthve setHeightkareyi sağlamak hem kafa karıştırıcı hem de sezgisel olmayan davranışlar getirmiştir. Diyelim ki bir kodumuz var:

Rectangle r = createRectangle(); // create rectangle or square here
r.setWidth(10);
r.setHeight(20);
print(r.getWidth()); // expect to print 10
print(r.getHeight()); // expect to print 20

Fakat eğer yöntem createRectanglegeri döndü Square, çünkü Squaremiras almanız sayesinde mümkün Rectange. Sonra beklentiler bozuluyor. Burada, bu kodla, genişlik veya yükseklik ayarının yalnızca genişlik veya yükseklik değerinin değişmesine neden olacağını umuyoruz. OOP'nin amacı, üst sınıfla çalışırken, altındaki herhangi bir alt sınıf hakkında hiçbir bilginiz olmadığıdır. Ve eğer alt sınıf davranışı değiştirir, böylece üst sınıfla ilgili beklentilerimize uymazsa, böcek oluşması olasılığı yüksektir. Ve bu tür böceklerin hem hata ayıklaması hem de düzeltilmesi zordur.

OOP hakkındaki ana fikirlerden biri miras alınan veriler değil (aynı zamanda IMO'nun ana yanılgılarından biri) davranış olduğu. Ve kareye ve dikdörtgene bakarsanız, kalıtım ilişkisi içinde ilişki kurabileceğimiz davranışları yoktur.


2

LSP'nin söylediği şey, miras alan her şeyin Rectanglebir olması gerektiğidir Rectangle. Yani, ne Rectangleyaparsa yapmalı .

Muhtemelen için belgeler Rectangle, bir Rectangleismin davranışının şöyle olduğunu söylemek için yazılmıştır r:

r.setWidth(10);
r.setHeight(20);
print(r.getWidth());  // prints 10

Eğer kareniz aynı davranışa sahip değilse, a gibi davranmaz Rectangle. LSP miras almaması gerektiğini söylüyor Rectangle. Dil bu kuralı uygulayamaz, çünkü bir yöntem geçersiz kılmasında yanlış bir şey yapmanızı durduramaz, ancak bu "dil tamam, çünkü yöntemleri geçersiz kılmama izin veriyor" anlamına gelmez!

Şimdi, dokümantasyonu yukarıdaki kodun 10 basılacağı anlamına gelmeyecek şekilde yazmak mümkün olacaktır Rectangle, bu durumda belki sizin Squareolabilir Rectangle. "Bu, X yapar. Bunun yanında, bu sınıftaki uygulama Y" şeklinde bir belge de olabilir. Öyleyse, sınıftan bir arayüz ayıklamak ve arayüzün neyi garanti ettiğini ve buna ek olarak sınıfın neyi garanti ettiğini ayırt etmek için iyi bir vakanız var. Ancak insanlar "değişebilen bir kare değişmez bir dikdörtgen değil, değişmez bir kare değişmez bir dikdörtgendir" dediğinde, temelde yukarıdakilerin gerçekten de değişken bir dikdörtgenin makul tanımının bir parçası olduğunu varsayıyorlar.



@gnat: Diğer cevabı yaklaşık olarak bu kısalıktan aşağıya düzenlememi tercih eder misiniz? ;-) Soruyu cevaplamak için gerekli olduğunu düşündüğüm başka cevaplayıcıların gerekli olduğunu düşündüğü noktaları çıkarmadan yapamayacağımı sanmıyorum.
Steve Jessop


1

Alt tipler ve uzantı olarak, OO programlaması genellikle Liskov Değiştirme Prensibi'ne dayanır; A <= B ise bir B'nin gerekli olduğu yerlerde A tipi herhangi bir değerin kullanılabileceği, yani OO mimarisinde bir aksiyom budur. tüm alt sınıfların bu özelliğe sahip olacağı varsayılmaktadır (ve değilse, alt türlerin arabası bozuktur ve düzeltilmesi gerekir).

Bununla birlikte, bu ilkenin ya kodun gerçekçi olmadığını / temsil etmemesini ya da gerçekten önemsiz durumlarda (önemsiz durumlarda) ortaya çıktığını ortaya koyuyor! Kare-dikdörtgen problemi veya daire-elips problemi ( http://en.wikipedia.org/wiki/Circle-ellipse_problem ) olarak bilinen bu problem, ne kadar zor olduğu konusunda ünlü bir örnektir.

Biz unutmayın olabilir daha-ve-daha gözlemsel olarak eşdeğer kareler ve dikdörtgenler, ama tek fark işe yaramaz kadar, daha fazla fonksiyonellik uzağa atarak uygularlar.

Örnek olarak, http://okmij.org/ftp/Computation/Subtyping/ adresini ziyaret edin.

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.