2013-05-29 7 views
6

Przeczytałem kilka artykułów na temat praktyki, która sprawia, że ​​klasa Square klasy dziedziczenia klasy Rectangle jest złą praktyką, mówiąc, że narusza ona LSP (zasada Liskov). Wciąż go nie rozumiem, zrobiłem przykładowy kod w Ruby:Co jest nie tak z dziedziczeniem Square i Rectangle?

class Rectangle 
    attr_accessor :width, :height 
    def initialize(width, height) 
     @width = width 
     @height = height 
    end 
end 

class Square < Rectangle 
    def initialize(length) 
     super(length, length) 
    end 
    def width=(number) 
     super(number) 
     @height = number 
    end 

    def height=(number) 
     super(number) 
     @width = number 
    end 
end 


s = Square.new(100) 

s.width = 50 

puts s.height 

Czy ktoś mógłby mi powiedzieć, co jest z nim nie tak?

+0

Lumpy Space Princess? http://www.youtube.com/watch?v=pJTrD3R5cj0 – paxdiablo

+0

Wow, to interesujące, ale nie mogę tego zrozumieć. – mko

+1

Yozloy, przepraszam, właśnie chciałem wyjaśnić, co masz na myśli LSP, aby ci, którzy nie są tego świadomi, nie musieli szukać. – paxdiablo

Odpowiedz

4

Nie zawsze lubię Liskov, ponieważ wydaje się ograniczać to, co można zrobić z dziedziczeniem w oparciu o zachowanie, a nie "esencję". Moim zdaniem dziedziczenie zawsze miało być relacją "jest", a nie "działa dokładnie tak jak".

Mimo, że the wikipedia article idzie w szczegóły co do dlaczego jest to uważane za złe przez niektóre, używając dokładnie przykład:

Typowym przykładem, który jest niezgodny LSP jest kwadratem klasa, która wywodzi się z prostokąta class, przy założeniu, że istnieją metody getter i setter zarówno dla szerokości, jak i wysokości.

Klasa kwadratowa zawsze zakłada, że ​​szerokość jest równa wysokości. Jeśli obiekt kwadratowy jest używany w kontekście, w którym spodziewany jest prostokąt, może wystąpić nieoczekiwane zachowanie, ponieważ wymiary kwadratu nie mogą (lub raczej nie powinny) być modyfikowane niezależnie.

Ten problem nie może być łatwo naprawiony: jeśli możemy zmodyfikować metody setera w klasie Square, aby zachować niezmienny kwadrat (tj. Zachować wymiary równe), wówczas te metody osłabią (naruszą) warunki dla Ustawniki prostokąta, które określają, że wymiary można modyfikować niezależnie.

Więc patrząc na kodzie obok równoważny kod Rectangle:

s = Square.new(100)   r = Rectangle.new(100,100) 
s.width = 50     r.width = 50 
puts s.height     puts r.height 

wyjście byłoby 50 po lewej stronie i 100 po prawej stronie.

Ale ten jest ważne nieco z artykułu, moim zdaniem:

Naruszenia LSP, jak ten, może lub nie być problemem w praktyce, w zależności od warunki końcowe lub niezmienniki, których rzeczywiście oczekuje kod, który używa klas naruszających LSP.

Innymi słowy, pod warunkiem kod korzystając klas rozumie zachowanie, nie ma problemu.

Konkluzja, kwadrat jest właściwym podzbiorem prostokąta, o luźno na tyle definicji prostokąta :-)

+0

dzięki za szczegółowe wyjaśnienie. Jedna rzecz, której nie dostaję z twojego kodu 'Rectangle' to linia' r = Rectangle.new (100) ', masz na myśli' r = Rectangle.new (100, 100) '? – mko

+0

@ yozloy, tak, przepraszam, to był błąd cut'n'paste, chociaż mogłem twierdzić, że był to pojedynczy argument konstruktora Rectangle, który zrobił kwadrat :-) Naprawiono teraz. – paxdiablo

+0

@paxdiable mam to! mówiąc o wynikach 's.height' i' r.height', myślę, że '50',' 100' są poprawnymi wyjściami, czy mam rację co do tego punktu? – mko

2

Co jest złego w tym z zasady substytucji Liskov (LSP) perspektywa jest, że Rectangle s i Square s są zmienne. Oznacza to, że musisz jawnie ponownie wprowadzić settery w podklasę i stracić korzyści z dziedziczenia. Jeśli utworzysz Rectangle s niezmienny, tj. Jeśli chcesz inny Rectangle tworzysz nowy, a nie zmieniając pomiarów istniejącego, to nie ma problemu z naruszeniem LSP.

class Rectangle 
    attr_reader :width, :height 

    def initialize(width, height) 
    @width = width 
    @height = height 
    end 

    def area 
    @width * @height 
    end 
end 

class Square < Rectangle 
    def initialize(length) 
    super(length, length) 
    end 
end 

Korzystanie attr_reader daje pobierające ale nie ustawiaczy, stąd niezmienność. Dzięki tej implementacji zarówno Rectangles jak i Squares zapewniają widoczność dla height i width, dla kwadratu, który zawsze będzie taki sam, a pojęcie obszaru jest spójne.

+0

Ugh! Widzę, skąd przybywasz i to jest dobre wytłumaczenie. Wygląda jednak na to, że ponowne wykorzystanie obiektu jest trudniejsze. Aby zmienić rozmiar obiektu, musisz stworzyć nowy o zmodyfikowanych atrybutach, a następnie zniszczyć stary. To nie czyni twojej odpowiedzi mniej ważną, tylko że zmniejsza użyteczność LSP w moich oczach. – paxdiablo

+0

@pjs dlaczego mutable tracą korzyść z dziedziczenia? Sądzę, że po prostu ponownie zastosować metody ustawiające, klasa 'Square' może ponownie użyć metod zdefiniowanych w klasie' Rectangle', czy jest to rodzaj korzyści? – mko

+0

@paxdiablo: Nie jestem zwolennikiem LSP, tylko próbuję wyjaśnić (moje rozumienie) tego. Mam jednak tendencję do potwierdzania niezmiennych obiektów. Prostokąt o różnych wymiarach to inny prostokąt! W wielu przypadkach jest to o wiele bezpieczniejsze. Na przykład rozważ umieszczenie pęczku prostokątów w drzewie wyszukiwania binarnego uporządkowanego według ich obszarów. Teraz, jeśli zmienisz wymiar jednego z nich, drzewo zacznie w tajemniczy sposób zawodzić w przypadku przyszłych dostępów - jeden z jego elementów nagle narusza podstawową właściwość porządkowania drzewa. Tego typu błędy mogą być bardzo trudne do wyśledzenia. – pjs

0

Rozważ abstrakcyjną klasę bazową lub interfejs (niezależnie od tego, czy coś jest interfejsem, czy też klasa abstrakcyjna jest szczegółem implementacji, raczej nieistotnym dla LSP) ReadableRectangle; ma właściwości tylko do odczytu Width i Height. Można by na tej podstawie wyprowadzić typ ReadableSquare, który ma takie same właściwości, ale na mocy umowy gwarantuje, że Width i Height będzie zawsze taki sam.

Z ReadableRectangle, można określić konkretny typ ImmutableRectangle (co zajmuje wysokość i szerokość w konstruktora i gwarantuje, że Height i Width właściwości zawsze zwraca same wartości) i MutableRectangle. Można również zdefiniować typ betonu MutableRectangle, który pozwala ustawić wysokość i szerokość w dowolnym momencie.

Po "kwadratowej" stronie, ImmutableSquare powinien być substytutem zarówno dla ImmutableRectangle, jak i ReadableSquare. Argument MutableSquare można jednak zamienić tylko na ReadableSquare [który z kolei można zamienić na ReadableRectangle.] Ponadto zachowanie ImmutableSquare można zastąpić wartością ImmutableRectangle, dlatego wartość uzyskana w wyniku dziedziczenia konkretnego typu ImmutableRectangle będzie ograniczona. Jeśli ImmutableRectangle byłby abstrakcyjnym typem lub interfejsem, klasa ImmutableSquare potrzebowałaby tylko jednego pola zamiast dwóch do przechowywania jego wymiarów (dla klasy z dwoma polami, zapisywanie nie jest wielkim problemem, ale nie jest trudno wyobrazić sobie zajęcia z dużo więcej pól, gdzie oszczędności mogą być znaczące). Jeśli jednak ImmutableRectangle jest typem konkretnym, wówczas każdy typ pochodny musiałby mieć wszystkie pola swojej podstawy.

Niektóre rodzaje kwadratów można zastąpić odpowiednimi typami prostokątów, ale zmienny kwadrat nie może być zastąpiony zmiennym prostokątem.