Jeśli kwadrat jest rodzajem prostokąta, to dlaczego kwadrat nie może odziedziczyć po prostokącie? Lub dlaczego jest to zły projekt?
Słyszałem, jak ludzie mówią:
Jeśli sprawiłeś, że Kwadrat pochodzi z prostokąta, kwadrat powinien być użyteczny wszędzie tam, gdzie oczekujesz prostokąta
Jaki jest tutaj problem? I dlaczego Square miałby być użyteczny wszędzie tam, gdzie oczekujesz prostokąta? Byłoby to użyteczne tylko wtedy, gdy utworzymy obiekt Square, a jeśli zastąpimy metody SetWidth i SetHeight dla Square, to dlaczego miałby wystąpić jakiś problem?
Jeśli posiadałeś metody SetWidth i SetHeight w swojej klasie bazowej Rectangle i jeśli twoja referencja Rectangle wskazywała na Square, to SetWidth i SetHeight nie mają sensu, ponieważ ustawienie jednej zmieniłoby drugą, aby ją dopasować. W tym przypadku Square nie przejdzie testu podstawienia Liskowa prostokątem, a abstrakcja dziedziczenia kwadratu po prostokącie jest zła.
Czy ktoś może wyjaśnić powyższe argumenty? Ponownie, jeśli przekroczymy metody SetWidth i SetHeight w Square, czy nie rozwiąże tego problemu?
Słyszałem także / czytałem:
Prawdziwy problem polega na tym, że nie modelujemy prostokątów, lecz „prostokąty, które można zmieniać”, tj. Prostokąty, których szerokość lub wysokość można modyfikować po utworzeniu (i nadal uważamy, że jest to ten sam obiekt). Jeśli spojrzymy na klasę prostokąta w ten sposób, jasne jest, że kwadrat nie jest „prostokątem przekształcalnym”, ponieważ kwadrat nie może zostać przekształcony i nadal być kwadratem (ogólnie). Matematycznie nie widzimy problemu, ponieważ zmienność nie ma nawet sensu w kontekście matematycznym
Tutaj uważam, że „zmiana rozmiaru” jest właściwym terminem. Prostokąty mają „możliwość zmiany rozmiaru”, podobnie jak kwadraty. Czy brakuje mi czegoś w powyższym argumencie? Kwadrat można zmienić tak jak dowolny prostokąt.
źródło
Why do we even need Square
? To tak, jakby mieć dwa długopisy. Jeden długopis niebieski i jeden czerwony długopis niebieski, żółty lub zielony. Niebieski długopis jest zbędny - tym bardziej w przypadku kwadratu, ponieważ nie ma żadnych korzyści finansowych.Odpowiedzi:
Zasadniczo chcemy, aby rzeczy zachowywały się rozsądnie.
Rozważ następujący problem:
Dano mi grupę prostokątów i chcę zwiększyć ich powierzchnię o 10%. Więc to, co robię, to ustawiam długość prostokąta na 1,1 razy większą niż wcześniej.
Teraz w tym przypadku wszystkie moje prostokąty mają teraz długość zwiększoną o 10%, co zwiększy ich powierzchnię o 10%. Niestety, ktoś faktycznie przekazał mi mieszaninę kwadratów i prostokątów, a kiedy zmieniła się długość prostokąta, zmieniła się również szerokość.
Moje testy jednostkowe zdały, ponieważ napisałem wszystkie moje testy jednostkowe, aby użyć zbioru prostokątów. Wprowadziłem teraz do mojej aplikacji subtelny błąd, który może pozostać niezauważony przez wiele miesięcy.
Co gorsza, Jim z księgowości widzi moją metodę i pisze inny kod, który wykorzystuje fakt, że jeśli przekazuje kwadraty do mojej metody, uzyskuje bardzo ładny wzrost wielkości o 21%. Jim jest szczęśliwy i nikt nie jest mądrzejszy.
Jim zostaje awansowany za doskonałą pracę do innego oddziału. Alfred dołącza do firmy jako junior. W swoim pierwszym raporcie o błędach Jill z działu reklamy podał, że przekazanie kwadratów tej metodzie powoduje wzrost o 21% i chce, aby błąd został naprawiony. Alfred widzi, że kwadraty i prostokąty są używane wszędzie w kodzie, i zdaje sobie sprawę, że przerwanie łańcucha dziedziczenia jest niemożliwe. Nie ma również dostępu do kodu źródłowego Księgowości. Więc Alfred naprawia błąd w ten sposób:
Alfred jest zadowolony ze swoich umiejętności hakerskich, a Jill sygnalizuje, że błąd został naprawiony.
W przyszłym miesiącu nikt nie otrzyma zapłaty, ponieważ księgowość była zależna od możliwości przekazania kwadratów do
IncreaseRectangleSizeByTenPercent
metody i zwiększenia powierzchni o 21%. Cała firma przechodzi w tryb „poprawki błędu 1”, aby wyśledzić źródło problemu. Śledzą problem do rozwiązania Alfreda. Wiedzą, że muszą zadowolić zarówno księgowość, jak i reklamę. Rozwiązują więc problem, identyfikując użytkownika za pomocą wywołania metody w następujący sposób:I tak dalej i tak dalej.
Ta anegdota oparta jest na rzeczywistych sytuacjach, z którymi codziennie spotykają się programiści. Naruszenie zasady substytucji Liskowa może wprowadzić bardzo subtelne błędy, które zostaną wykryte dopiero po latach od ich napisania, do tego czasu naprawienie naruszenia spowoduje uszkodzenie wielu rzeczy, a nie naprawienie go rozwścieczy twojego największego klienta.
Istnieją dwa realistyczne sposoby rozwiązania tego problemu.
Pierwszym sposobem jest uczynienie Rectangle niezmiennym. Jeśli użytkownik Prostokąta nie może zmienić właściwości Długość i Szerokość, problem zniknie. Jeśli chcesz mieć prostokąt o innej długości i szerokości, utwórz nowy. Kwadraty z radością dziedziczą po prostokątach.
Drugim sposobem jest przerwanie łańcucha spadkowego między kwadratami i prostokątami. Jeżeli kwadrat jest definiowana jako posiadające jedną
SideLength
nieruchomość i prostokąty mająLength
iWidth
mienia i nie ma dziedziczenia, to niemożliwe, aby przypadkowo złamać rzeczy oczekując prostokąt i coraz kwadrat. W języku C # możeszseal
zdefiniować klasę prostokąta, co gwarantuje, że wszystkie prostokąty, które kiedykolwiek otrzymasz, są w rzeczywistości prostokątami.W tym przypadku podoba mi się sposób „rozwiązania problemu”. Tożsamość prostokąta to jego długość i szerokość. Ma sens, że gdy chcesz zmienić tożsamość obiektu, tak naprawdę chcesz nowego obiektu. Jeśli stracisz starego klienta i zyskasz nowego, nie zmienisz
Customer.Id
pola ze starego klienta na nowego, tworzysz nowegoCustomer
.Naruszenia zasady substytucji Liskowa są powszechne w świecie rzeczywistym, głównie dlatego, że wiele kodu jest napisanych przez ludzi, którzy są niekompetentni / pod presją czasu / nie dbają / popełniają błędy. Może i prowadzi do bardzo nieprzyjemnych problemów. W większości przypadków zamiast tego preferujesz kompozycję zamiast dziedziczenia .
źródło
Jeśli wszystkie twoje obiekty są niezmienne, nie ma problemu. Każdy kwadrat jest również prostokątem. Wszystkie właściwości prostokąta są również właściwościami kwadratu.
Problem zaczyna się, gdy dodasz możliwość modyfikowania obiektów. A tak naprawdę - kiedy zaczynasz przekazywać argumenty do obiektu, a nie tylko czytasz gettery właściwości.
Istnieją modyfikacje, które można wykonać w prostokącie, które zachowują wszystkie niezmienniki klasy Rectangle, ale nie wszystkie niezmienniki kwadratowe - na przykład zmieniając szerokość lub wysokość. Nagle zachowanie się prostokąta to nie tylko jego właściwości, ale także możliwe modyfikacje. To nie tylko to, co dostajesz z Prostokąta, ale także to, co możesz włożyć .
Jeśli twój prostokąt ma metodę
setWidth
udokumentowaną jako zmiana szerokości i nie modyfikująca wysokości, wówczas Square nie może mieć zgodnej metody. Jeśli zmienisz szerokość, a nie wysokość, wynik nie będzie już prawidłowym kwadratem. Jeśli zdecydujesz się zmodyfikować zarówno szerokość, jak i wysokość Kwadratu podczas używaniasetWidth
, nie wdrażasz specyfikacji ProstokątasetWidth
. Po prostu nie możesz wygrać.Kiedy spojrzysz na to, co możesz „umieścić” w prostokącie i kwadracie, jakie wiadomości możesz do nich wysłać, prawdopodobnie zauważysz, że każdą wiadomość, którą możesz poprawnie wysłać na kwadrat, możesz również wysłać do prostokąta.
Jest to kwestia współwariancji kontra kontra wariancji.
Metody odpowiedniej podklasy, takie, w których instancje mogą być używane we wszystkich przypadkach, w których oczekuje się nadklasy, wymagają, aby każda metoda:
Wróćmy więc do prostokąta i kwadratu: to, czy kwadrat może być podklasą prostokąta, zależy całkowicie od tego, jakie metody ma prostokąt.
Jeśli prostokąt ma indywidualne ustawienia szerokości i wysokości, Square nie będzie dobrą podklasą.
Podobnie, jeśli sprawisz, że niektóre metody będą alternatywne w argumentach, jak
compareTo(Rectangle)
na przykład Prostokąt icompareTo(Square)
Kwadrat, będziesz miał problem z użyciem Kwadratu jako prostokąta.Jeśli zaprojektujesz swój Kwadrat i Prostokąt, aby były kompatybilne, prawdopodobnie będzie działać, ale należy je opracować razem, inaczej założę się, że to nie zadziała.
źródło
Jest tu wiele dobrych odpowiedzi; W szczególności odpowiedź Stephena dobrze ilustruje, dlaczego naruszenia zasady substytucji prowadzą do rzeczywistych konfliktów między zespołami.
Pomyślałem, że mógłbym krótko porozmawiać o konkretnym problemie prostokątów i kwadratów, zamiast używać go jako metafory innych naruszeń LSP.
Istnieje dodatkowy problem z kwadratem-specjalnym rodzajem prostokąta, o którym rzadko się wspomina, a mianowicie: dlaczego zatrzymujemy się za pomocą kwadratów i prostokątów ? Jeśli chcemy powiedzieć, że kwadrat jest szczególnym rodzajem prostokąta, z pewnością powinniśmy również powiedzieć:
Co u licha powinny tu być wszystkie relacje? Języki oparte na dziedziczeniu klas, takie jak C # lub Java, nie zostały zaprojektowane do reprezentowania tego rodzaju złożonych relacji z wieloma różnymi rodzajami ograniczeń. Najlepiej po prostu całkowicie uniknąć pytania, nie próbując reprezentować tych wszystkich rzeczy jako klas z relacjami podtypów.
źródło
IShape
typ, który zawiera ramkę ograniczającą, i można go rysować, skalować i serializować oraz miećIPolygon
podtyp z metodą raportowania liczby wierzchołków i metodą zwracania znakuIEnumerable<Point>
. Można wtedyIQuadrilateral
podtyp wynikającemu zIPolygon
,IRhombus
iIRectangle
, wywodzą się z tym, iISquare
wywodzą się zIRhombus
iIRectangle
. Zmienność wyrzuca wszystko przez okno, a wielokrotne dziedziczenie nie działa z klasami, ale myślę, że w przypadku niezmiennych interfejsów jest w porządku.IRhombus
gwarancja, że wszystkiePoint
zwracane odEnumerable<Point>
zdefiniowanego przezIPolygon
odpowiadają krawędziom o równej długości? Ponieważ sama implementacjaIRhombus
interfejsu nie gwarantuje, że konkretny obiekt to romb, dziedziczenie nie może być odpowiedzią.Z matematycznego punktu widzenia kwadrat jest prostokątem. Jeśli matematyk modyfikuje kwadrat, aby przestał przestrzegać kontraktu kwadratowego, zmienia się w prostokąt.
Ale w projektowaniu OO jest to problem. Obiekt jest tym, czym jest, i obejmuje to zarówno zachowania, jak i stan. Jeśli trzymam kwadratowy obiekt, ale ktoś inny modyfikuje go tak, aby był prostokątem, narusza to kontrakt kwadratu bez mojej winy. To powoduje, że dzieją się różnego rodzaju złe rzeczy.
Kluczowym czynnikiem jest tutaj zmienność . Czy kształt może się zmienić po jego zbudowaniu?
Zmienny: jeśli kształty mogą się zmieniać po zbudowaniu, kwadrat nie może mieć związku z prostokątem. Kontrakt prostokąta zawiera ograniczenie, że przeciwległe boki muszą być równej długości, ale sąsiednie boki nie muszą. Kwadrat musi mieć cztery równe boki. Modyfikacja kwadratu za pomocą interfejsu prostokąta może naruszać kwadratową umowę.
Niezmienny: jeśli kształty nie mogą się zmienić po zbudowaniu, to kwadratowy obiekt musi również zawsze wypełniać kontrakt prostokątny. Kwadrat może mieć związek z prostokątem.
W obu przypadkach można poprosić kwadrat o utworzenie nowego kształtu w oparciu o jego stan z jedną lub większą liczbą zmian. Na przykład można powiedzieć „utwórz nowy prostokąt na podstawie tego kwadratu, z tym wyjątkiem, że przeciwne boki A i C są dwa razy dłuższe”. Ponieważ budowany jest nowy obiekt, oryginalny kwadrat nadal przestrzega swoich umów.
źródło
This is one of those cases where the real world is not able to be modeled in a computer 100%
. Dlaczego tak? Nadal możemy mieć funkcjonalny model kwadratu i prostokąta. Jedyną konsekwencją jest to, że musimy poszukać prostszej konstrukcji do abstrakcji na tych dwóch obiektach.Ponieważ jest to część tego, co oznacza podtyp (patrz też: zasada podstawienia Liskowa). Możesz to zrobić, musisz być w stanie to zrobić:
Robisz to cały czas (czasem nawet bardziej niejawnie), używając OOP.
Ponieważ nie można rozsądnie zastąpić tych
Square
. Ponieważ kwadratu nie można „zmienić rozmiaru jak jakikolwiek prostokąt”. Gdy zmienia się wysokość prostokąta, szerokość pozostaje taka sama. Ale gdy zmienia się wysokość kwadratu, szerokość musi się odpowiednio zmieniać. Problemem jest nie tylko zmiana rozmiaru, ale także zmiana rozmiaru w obu wymiarach niezależnie.źródło
Rect r = s;
linii, możesz po prostu,doSomethingWith(s)
a środowisko wykonawcze użyje wszelkich wywołań ws
celu rozwiązania dowolnychSquare
metod wirtualnych .setWidth
isetHeight
zmień zarówno szerokość, jak i wysokość.To, co opisujesz, narusza tak zwaną Zasadę Zastępstwa Liskowa . Podstawową ideą LSP jest to, że za każdym razem, gdy używasz instancji określonej klasy, zawsze powinieneś mieć możliwość zamiany w instancji dowolnej podklasy tej klasy, bez wprowadzania błędów.
Problem Prostokąta-kwadratu nie jest tak naprawdę dobrym sposobem na przedstawienie Liskova. Próbuje wyjaśnić ogólną zasadę na przykładzie, który jest dość subtelny, i działa z obojętnością na jedną z najczęstszych intuicyjnych definicji w całej matematyce. Niektórzy nazywają go z tego powodu problemem koła elipsy, ale w tym przypadku jest tylko nieco lepszy. Lepszym podejściem jest cofnięcie się o krok, używając czegoś, co nazywam problemem równoległościogram-prostokąt. To znacznie ułatwia zrozumienie.
Równoległobok jest czworokątny z dwiema równoległymi bokami. Ma również dwie pary przystających kątów. Nie jest trudno wyobrazić sobie obiekt równoległoboku wzdłuż tych linii:
Jednym z powszechnych sposobów myślenia o prostokącie jest równoległobok z kątami prostymi. Na pierwszy rzut oka może wydawać się, że Rectangle jest dobrym kandydatem do dziedziczenia po Parallelogramie , dzięki czemu można ponownie wykorzystać cały ten pyszny kod. Jednak:
Dlaczego te dwie funkcje wprowadzają błędy w Rectangle? Problem polega na tym, że nie można zmieniać kątów w prostokącie : są one zdefiniowane jako zawsze 90 stopni, a zatem ten interfejs nie działa tak naprawdę dla Rectangle dziedziczącego z Parallelogram. Jeśli zamienię prostokąt na kod, który oczekuje równoległoboku, a ten kod spróbuje zmienić kąt, prawie na pewno wystąpią błędy. Wzięliśmy coś, co można było zapisać w podklasie, i uczyniło to tylko do odczytu, a to naruszenie Liskowa.
Jak to się ma do kwadratów i prostokątów?
Kiedy mówimy, że możesz ustawić wartość, zwykle mamy na myśli coś nieco silniejszego niż po prostu możliwość zapisania wartości. Zakładamy pewien stopień wyłączności: jeśli ustalisz wartość, a następnie wykluczysz jakieś nadzwyczajne okoliczności, pozostanie na tej wartości, dopóki jej nie ustawisz ponownie. Istnieje wiele zastosowań wartości, które można zapisać, ale nie pozostają ustawione, ale istnieje również wiele przypadków, które zależą od wartości pozostającej tam, gdzie jest, po jej ustawieniu. I tutaj napotykamy inny problem.
Nasza klasa Square odziedziczyła błędy po Rectangle, ale ma kilka nowych. Problem z setSideA i setSideB polega na tym, że żadnego z nich nie da się tak naprawdę ustawić: nadal możesz zapisać wartość w jednym z nich, ale zmieni się ono pod tobą, jeśli drugi zostanie napisany. Jeśli zamienię to na Parallelogram w kodzie, który zależy od możliwości ustawiania stron niezależnie od siebie, to wystraszy mnie.
Na tym polega problem i dlatego istnieje problem z użyciem Rectangle-Square jako wstępu do Liskova. Prostokąt-Kwadrat zależy od różnicy między zdolnością do pisania a zdolnością do ustawiania tego, i to jest o wiele bardziej subtelna różnica niż umiejętność ustawiania czegoś w porównaniu z ustawieniem tylko do odczytu. Prostokąt-kwadrat wciąż ma wartość jako przykład, ponieważ dokumentuje dość powszechną gotcha, na którą należy uważać, ale nie należy jej używać jako przykładu wprowadzającego . Pozwól uczniowi najpierw zdobyć trochę podstaw w podstawach, a następnie rzucić w niego czymś trudniejszym.
źródło
Subtyping dotyczy zachowania.
Aby typ
B
był podtypem typuA
, musi obsługiwać każdą operację obsługiwaną przez ten typA
przy użyciu tej samej semantyki (wymyślne określenie „zachowanie”). Posługiwanie się uzasadnieniem, że każde B jest literą A , nie działa - ostateczna ocena ma zgodność z zachowaniem. Przez większość czasu „B jest rodzajem A” pokrywa się z „B zachowuje się jak A”, ale nie zawsze .Przykład:
Rozważ zestaw liczb rzeczywistych. W dowolnym języku, możemy spodziewać się ich do wspierania operacji
+
,-
,*
, i/
. Rozważmy teraz zbiór dodatnich liczb całkowitych ({1, 2, 3, ...}). Oczywiście każda dodatnia liczba całkowita jest również liczbą rzeczywistą. Ale czy typ liczb całkowitych dodatnich jest podtypem liczb rzeczywistych? Spójrzmy na cztery operacje i zobaczmy, czy dodatnie liczby całkowite zachowują się tak samo jak liczby rzeczywiste:+
: Możemy bez problemu dodawać liczby całkowite.-
: Nie wszystkie odejmowania dodatnich liczb całkowitych dają dodatnie liczby całkowite. Np3 - 5
.*
: Możemy pomnożyć dodatnie liczby całkowite bez problemów./
: Nie zawsze możemy podzielić dodatnie liczby całkowite i uzyskać dodatnią liczbę całkowitą. Np5 / 3
.Tak więc pomimo dodatnich liczb całkowitych będących podzbiorem liczb rzeczywistych, nie są one podtypem. Podobny argument można podać dla liczb całkowitych o skończonym rozmiarze. Oczywiście każda 32-bitowa liczba całkowita jest również 64-bitową liczbą całkowitą, ale
32_BIT_MAX + 1
daje różne wyniki dla każdego typu. Więc jeśli dałem ci jakiś program i zmieniłeś typ każdej 32-bitowej zmiennej całkowitej na 64-bitową liczbę całkowitą, istnieje duża szansa, że program będzie się zachowywał inaczej (co prawie zawsze oznacza źle ).Oczywiście możesz zdefiniować
+
dla 32-bitowych liczb całkowitych, aby wynik był 64-bitową liczbą całkowitą, ale teraz będziesz musiał zarezerwować 64 bity miejsca za każdym razem, gdy dodasz dwie liczby 32-bitowe. To może, ale nie musi być do zaakceptowania, w zależności od potrzeb pamięci.Dlaczego to ma znaczenie?
Ważne jest, aby programy były poprawne. Jest to prawdopodobnie najważniejsza właściwość programu. Jeśli program jest poprawny dla jakiegoś typu
A
, jedynym sposobem na zagwarantowanie, że program będzie nadal poprawny dla niektórych podtypów,B
jestB
zachowanie sięA
pod każdym względem.Masz więc typ
Rectangles
, którego specyfikacja mówi, że jego boki można zmieniać niezależnie. Napisałeś kilka programów, które używająRectangles
i zakładają, że implementacja jest zgodna ze specyfikacją. Następnie wprowadziłeś podtyp o nazwie,Square
którego boków nie można niezależnie zmienić rozmiaru. W rezultacie większość programów zmieniających rozmiar prostokątów będzie teraz niepoprawna.źródło
Po pierwsze, zadaj sobie pytanie, dlaczego uważasz, że kwadrat jest prostokątem.
Oczywiście większość ludzi nauczyła się tego w szkole podstawowej i wydaje się to oczywiste. Prostokąt ma kształt 4-stronny z kątami 90 stopni, a kwadrat spełnia wszystkie te właściwości. Czy kwadrat nie jest prostokątem?
Rzecz w tym, że wszystko zależy od tego, jakie są twoje początkowe kryteria grupowania obiektów, w jakim kontekście patrzysz na te obiekty. W geometrii kształty są klasyfikowane na podstawie właściwości ich punktów, linii i aniołów.
Zanim więc powiesz „kwadrat jest rodzajem prostokąta”, musisz najpierw zadać sobie pytanie, czy jest to oparte na kryteriach, na których mi zależy .
W zdecydowanej większości przypadków to nie będzie to, na czym ci zależy. Większość systemów modelujących kształty, takich jak GUI, grafika i gry wideo, nie dotyczy przede wszystkim geometrycznego grupowania obiektu, ale zachowanie. Czy kiedykolwiek pracowałeś nad systemem, który miał znaczenie, że kwadrat był rodzajem prostokąta w sensie geometrycznym. Co by ci to dało, wiedząc, że ma 4 boki i kąty 90 stopni?
Nie modelujesz systemu statycznego, modelujesz system dynamiczny, w którym coś się stanie (kształty będą tworzone, niszczone, zmieniane, rysowane itp.). W tym kontekście zależy Ci na wspólnym zachowaniu między obiektami, ponieważ najważniejsze jest to, co możesz zrobić z kształtem, jakie reguły należy zachować, aby nadal mieć spójny system.
W tym kontekście kwadrat zdecydowanie nie jest prostokątem , ponieważ zasady rządzące sposobem zmiany kwadratu nie są takie same jak prostokąt. Więc nie są to tego samego rodzaju rzeczy.
W takim przypadku nie modeluj ich jako takich. Dlaczego miałbyś? Nie zyskuje nic poza niepotrzebnymi ograniczeniami.
Jeśli to zrobisz, to praktycznie stwierdzasz w kodzie, że to nie to samo. Twój kod mówi, że kwadrat zachowuje się w ten sposób, a prostokąt zachowuje się w ten sposób, ale nadal są takie same.
Oczywiście nie są one takie same w kontekście, na którym ci zależy, ponieważ właśnie zdefiniowałeś dwa różne zachowania. Dlaczego więc udawać, że są takie same, jeśli są podobne tylko w kontekście, na którym ci nie zależy?
To podkreśla poważny problem, gdy programiści wchodzą do domeny, którą chcą modelować. Bardzo ważne jest, aby wyjaśnić, jakim kontekstem jesteś zainteresowany, zanim zaczniesz myśleć o obiektach w domenie. Jakim aspektem jesteś zainteresowany. Tysiące lat temu Grecy dbali o wspólne właściwości linii i aniołów kształtów i na tej podstawie pogrupowali je. Nie oznacza to, że musisz kontynuować to grupowanie, jeśli nie jest to dla ciebie ważne (co w 99% przypadków modelujesz w oprogramowaniu, na którym ci nie zależy).
Wiele odpowiedzi na to pytanie koncentruje się na podtypach dotyczących zachowania grupującego , ponieważ są one regułami .
Ale bardzo ważne jest, aby zrozumieć, że nie robisz tego tylko po to, aby przestrzegać zasad. Robisz to, ponieważ w zdecydowanej większości przypadków tak naprawdę zależy ci na tym. Nie obchodzi cię, czy kwadrat i prostokąt mają te same wewnętrzne anioły. Dbasz o to, co mogą zrobić, będąc wciąż kwadratami i prostokątami. Dbasz o zachowanie obiektów, ponieważ modelujesz system, który koncentruje się na zmianie systemu w oparciu o reguły zachowania obiektów.
źródło
Rectangle
są używane tylko do reprezentowania wartości , klasaSquare
może odziedziczyćRectangle
i w pełni przestrzegać swojej umowy. Niestety, wiele języków nie czyni rozróżnienia między zmiennymi, które zawierają wartości, a tymi, które identyfikują jednostki.Square
typu, który dziedziczy po niezmiennymRectnagle
typie, może być przydatne, jeśli istnieją pewne rodzaje operacji, które można wykonać tylko na kwadratach. Jako realistyczny przykład tej koncepcji, rozważReadableMatrix
typ [typ podstawowy prostokątnej tablicy, która może być przechowywana na różne sposoby, w tym rzadko], orazComputeDeterminant
metodę. Może mieć sensComputeDeterminant
praca tylko zReadableSquareMatrix
typem, z którego pochodziReadableMatrix
, co uważam za przykładSquare
pochodzący odRectangle
.Problem polega na myśleniu, że jeśli rzeczy są w jakiś sposób powiązane w rzeczywistości, muszą być powiązane w dokładnie taki sam sposób po modelowaniu.
Najważniejszą rzeczą w modelowaniu jest identyfikacja wspólnych atrybutów i typowych zachowań, zdefiniowanie ich w klasie podstawowej i dodanie dodatkowych atrybutów w klasach potomnych.
Problem z twoim przykładem jest całkowicie abstrakcyjny. Dopóki nikt nie wie, do czego zamierzasz użyć tych klas, trudno zgadnąć, jaki projekt powinieneś wykonać. Ale jeśli naprawdę chcesz mieć tylko wysokość, szerokość i rozmiar, logiczne byłoby:
width
parametrem iresize(double factor)
zmieniając szerokość o podany współczynnikheight
i przesłania jegoresize
funkcję, która wywołuje,super.resize
a następnie zmienia rozmiar o podany współczynnikZ punktu widzenia programowania w Square nie ma nic, czego nie ma Rectangle. Nie ma sensu tworzyć kwadratu jako podklasy prostokąta.
źródło
Ponieważ przez LSP tworzenie relacji dziedziczenia między nimi a nadpisywaniem
setWidth
orazsetHeight
zapewnienie, że kwadrat ma to samo, wprowadza zachowanie mylące i nieintuicyjne. Powiedzmy, że mamy kod:Ale jeśli metoda
createRectangle
zwróciłaSquare
, ponieważ jest to możliwe dziękiSquare
dziedziczeniuRectange
. Potem oczekiwania się łamią. W przypadku tego kodu oczekujemy, że ustawienie szerokości lub wysokości spowoduje jedynie zmianę odpowiednio szerokości lub wysokości. Chodzi o to, że podczas pracy z nadklasą nie masz żadnej wiedzy o żadnej podklasie pod nią. A jeśli podklasa zmieni zachowanie tak, aby było sprzeczne z naszymi oczekiwaniami dotyczącymi nadklasy, istnieje duże prawdopodobieństwo wystąpienia błędów. Tego rodzaju błędy są trudne do debugowania i naprawy.Jedną z głównych idei dotyczących OOP jest to, że to zachowanie, a nie dane, które są dziedziczone (co jest również jednym z głównych nieporozumień IMO). A jeśli spojrzysz na kwadrat i prostokąt, same nie mają zachowania, które moglibyśmy odnosić w relacji dziedziczenia.
źródło
LSP mówi, że wszystko, co dziedziczy,
Rectangle
musi byćRectangle
. Oznacza to, że powinien zrobić cokolwiekRectangle
.Prawdopodobnie
Rectangle
napisano w dokumentacji , że zachowanieRectangle
nazwanegor
wygląda następująco:Jeśli Twój Plac nie ma takiego samego zachowania, to nie zachowuje się jak
Rectangle
. Więc LSP mówi, że nie może dziedziczyćRectangle
. Język nie może egzekwować tej reguły, ponieważ nie może powstrzymać cię przed zrobieniem czegoś złego w zastępowaniu metod, ale to nie znaczy, że „jest OK, ponieważ język pozwala mi zastąpić metody” jest przekonującym argumentem przemawiającym za tym!Teraz byłoby to możliwe , aby napisać dokumentację
Rectangle
w taki sposób, że nie oznacza to, że powyższy kod drukuje 10, w którym to przypadku może twójSquare
może byćRectangle
. Możesz zobaczyć dokumentację, która mówi coś w stylu „robi to X. Ponadto implementacja w tej klasie robi Y”. Jeśli tak, to masz dobry powód do wyodrębnienia interfejsu z klasy i rozróżnienia między tym, co gwarantuje interfejs, a tym, co gwarantuje klasa. Ale kiedy ludzie mówią, że „zmienny kwadrat nie jest zmiennym prostokątem, podczas gdy niezmienny kwadrat jest niezmiennym prostokątem”, w zasadzie zakładają, że powyższe jest rzeczywiście częścią rozsądnej definicji modyfikowalnego prostokąta.źródło
Podtypy, a co za tym idzie programowanie OO, często opierają się na zasadzie podstawienia Liskowa, że dowolna wartość typu A może być użyta tam, gdzie wymagane jest B, jeśli A <= B. Jest to właściwie aksjomat w architekturze OO, tj. zakłada się, że wszystkie podklasy będą miały tę właściwość (a jeśli nie, podtypy są błędne i wymagają naprawy).
Okazuje się jednak, że zasada ta jest albo nierealistyczna / niereprezentatywna dla większości kodów, albo wręcz niemożliwa do spełnienia (w nietrywialnych przypadkach)! Problem ten, znany jako problem kwadratu-prostokąta lub problem elipsy-koła ( http://en.wikipedia.org/wiki/Circle-ellipse_problem ), jest jednym znanym przykładem tego, jak trudno jest go wypełnić.
Zauważ, że moglibyśmy zaimplementować coraz więcej równoważnych obserwacyjnie Kwadratów i Prostokątów, ale tylko poprzez wyrzucanie coraz większej liczby funkcji, dopóki rozróżnienie nie będzie bezużyteczne.
Jako przykład zobacz http://okmij.org/ftp/Computation/Subtyping/
źródło