Po Circle
rozszerzeniuEllipse
łamie zasadę substytucji Liskowa , ponieważ modyfikuje ona warunek końcowy : mianowicie możesz ustawić X i Y niezależnie, aby narysować elipsę, ale X musi zawsze być równe Y dla okręgów.
Ale czy problem nie jest spowodowany tym, że Circle jest podtypem elipsy? Czy nie moglibyśmy odwrócić relacji?
Tak więc Circle jest nadtypem - ma jedną metodę setRadius
.
Następnie Ellipse rozszerza Circle, dodając setX
i setY
. Wywołanie setRadius
Ellipse ustawi zarówno X, jak i Y - co oznacza, że warunek końcowy na setRadius zostanie utrzymany, ale teraz możesz ustawić X i Y niezależnie przez rozszerzony interfejs.
object-oriented
solid
liskov-substitution
HorusKol
źródło
źródło
Odpowiedzi:
Problem z tym (i problemem kwadratu / prostokąta) polega na fałszywym założeniu, że relacja w jednej domenie (geometria) utrzymuje się w innej (zachowanie)
Okrąg i elipsa są powiązane, jeśli patrzysz na nie przez pryzmat teorii geometrycznej. Ale to nie jedyna domena, na którą można spojrzeć.
Projektowanie obiektowe dotyczy zachowania .
Charakterystyczną cechą obiektu jest zachowanie, za które obiekt jest odpowiedzialny. A w dziedzinie zachowania koło i elipsa mają tak różne zachowania, że prawdopodobnie lepiej nie myśleć o nich w ogóle. W tej dziedzinie elipsa i okrąg nie mają istotnego związku.
Lekcja polega na tym, aby wybrać domenę, która jest najbardziej sensowna dla OOD, a nie próbować wyłudzać relacje tylko dlatego, że istnieje ona w innej domenie.
Najczęstszym przykładem tego błędu w świecie rzeczywistym jest założenie, że obiekty są powiązane (lub nawet tej samej klasy), ponieważ mają podobne dane, nawet jeśli ich zachowanie jest bardzo różne. Jest to powszechny problem, gdy zaczynasz konstruować obiekty „najpierw dane”, określając, dokąd dane idą. Możesz skończyć z klasą powiązaną za pomocą danych, które mają zupełnie inne zachowanie. Na przykład zarówno odcinek wypłaty, jak i obiekty pracownika mogą mieć atrybut „wynagrodzenie brutto”, ale pracownik nie jest rodzajem odcinka wypłaty, a odcinek wypłaty nie jest rodzajem pracownika.
źródło
Koła są szczególnym przypadkiem elips, a mianowicie, że obie osie elipsy są takie same. Zasadniczo fałszywe jest w obszarze problemowym (geometrii) stwierdzenie, że elipsy mogą być rodzajem koła. Korzystanie z tego wadliwego modelu naruszyłoby wiele gwarancji okręgu, na przykład „wszystkie punkty na okręgu mają tę samą odległość od środka”. To także byłoby naruszeniem zasady substytucji Liskowa. Jak elipsa miałaby pojedynczy promień? (Nie,
setRadius()
ale co ważniejszegetRadius()
)Podczas gdy modelowanie kół jako podtypu elips nie jest zasadniczo błędne, to wprowadzenie zmienności łamie ten model. Bez
setX()
isetY()
metod, nie ma naruszenia LSP. Jeśli istnieje potrzeba posiadania obiektu o różnych wymiarach, lepszym rozwiązaniem jest utworzenie nowej instancji:źródło
Ellipse
iCircle
(taki jakgetArea
), który zostałby wyodrębniony do typuShape
- mógłbyEllipse
iCircle
osobno podtypShape
i spełniać LSP?Cormac ma naprawdę świetną odpowiedź, ale po prostu chcę trochę wyjaśnić przyczynę zamieszania.
Dziedzictwo w OO jest często nauczane przy użyciu rzeczywistych metafor, takich jak „jabłka i pomarańcze są podklasami owoców”. Niestety prowadzi to do błędnego przekonania, że typy w OO powinny być modelowane zgodnie z pewnymi hierarchiami taksonomicznymi istniejącymi niezależnie od programu.
Ale w projektowaniu oprogramowania typy powinny być modelowane zgodnie z wymaganiami aplikacji. Klasyfikacje w innych domenach są zwykle nieistotne. W rzeczywistej aplikacji z obiektami „Apple” i „Orange” - powiedzmy systemem zarządzania zapasami w supermarkecie - prawdopodobnie nie będą to w ogóle odrębne klasy, a kategorie takie jak „Owoc” będą raczej atrybutami niż nadtypami.
Problemem elipsy koła jest czerwony śledź. W geometrii okrąg jest specjalizacją elipsy, ale klasy w twoim przykładzie nie są figurami geometrycznymi. Co najważniejsze, figury geometryczne nie są zmienne. Można je jednak przekształcić , ale potem koło można przekształcić w elipsę. Model, w którym okręgi mogą zmieniać promień, ale nie mogą zmieniać się w elipsę, nie odpowiada geometrii. Taki model może mieć sens w konkretnej aplikacji (np. Narzędzie do rysowania), ale klasyfikacja geometryczna nie ma znaczenia dla projektowania hierarchii klas.
Czy Circle powinien być podklasą Elipsy czy odwrotnie? Zależy to całkowicie od wymagań konkretnej aplikacji, która korzysta z tych obiektów. Aplikacja do rysowania może mieć różne możliwości traktowania kręgów i elips:
Traktuj koła i elipsy jako odrębne typy kształtów o różnym interfejsie użytkownika (np. Dwa uchwyty zmiany rozmiaru na elipsie, jeden uchwyt na okręgu). Oznacza to, że z perspektywy aplikacji możesz mieć elipsę, która jest geometrycznie kołem, ale nie kołem.
Traktuj wszystkie elipsy, w tym okręgi, tak samo, ale masz opcję „zablokowania” xiy na tej samej wartości.
Elipsy są po prostu okręgami, w których zastosowano transformację skalowania.
Każdy możliwy projekt doprowadzi do innego modelu obiektu -
W pierwszym przypadku Circle i Elipsy będą klasami dla rodzeństwa
W drugim nie będzie w ogóle odrębnej klasy Kręgów
W trzecim nie będzie wyraźnej klasy elipsy. Tak więc tzw. Problem elipsy koła nie wchodzi w obraz w żadnym z nich.
Aby odpowiedzieć na postawione pytanie: Czy okrąg powinien rozciągać się na elipsie? Odpowiedź brzmi: to zależy od tego, co chcesz z tym zrobić. Ale prawdopodobnie nie.
źródło
Błędem jest od początku naleganie na posiadanie klasy „Elipsa” i „Koło”, gdzie jedna jest podklasą drugiej. Masz dwie realistyczne opcje: jedną z nich są osobne klasy. Mogą mieć wspólną nadklasę, np. Kolor, to, czy obiekt jest wypełniony, szerokość linii do rysowania itp.
Druga to mieć tylko jedną klasę o nazwie „Elipsa”. Jeśli masz tę klasę, łatwo jest użyć jej do reprezentowania okręgów (w zależności od szczegółów implementacji mogą istnieć pułapki; elipsa będzie miała pewien kąt, a obliczenie tego kąta nie może powodować problemów dla elipsy w kształcie koła). Mógłbyś mieć nawet wyspecjalizowane metody dla elips okrągłych, ale te „elipsy kołowe” nadal byłyby pełnymi obiektami „Elipsy”.
źródło
Po punktach LSP jednym „właściwym” rozwiązaniem tego problemu jest pojawienie się @HorusKol i @Ixrec - wywodzących oba typy z Shape. Ale to zależy od modelu, z którego pracujesz, więc zawsze powinieneś do tego wrócić.
Nauczono mnie:
Jeśli podtyp nie może zachowywać się tak samo jak nadtyp, relacja nie zachowuje się w przesłance IS-A - należy go zmienić.
Po angielsku:
(Przykład:
Tak działa klasyfikacja (tj. W świecie zwierząt), a przede wszystkim w OO.
Używając tego jako definicji dziedziczenia i polimorfizmu (które zawsze się zapisują razem), jeśli zasada ta zostanie złamana, powinieneś spróbować przemyśleć typy, które próbujesz wymodelować.
Jak wspomniali @HorusKul i @Ixrec, w matematyce masz jasno zdefiniowane typy. Ale w matematyce okrąg jest elipsą, ponieważ jest PODSETEM elipsy. Ale w OOP dziedziczenie nie działa. Klasa powinna dziedziczyć tylko wtedy, gdy jest SUPERSET (rozszerzenie) istniejącej klasy - co oznacza, że nadal JEST klasą podstawową we wszystkich kontekstach.
Na tej podstawie uważam, że należy nieco przeredagować rozwiązanie.
Mają typ kształtu Shape, a następnie RoundedShape (efektywnie koło, ale użyłem tutaj innej nazwy ROZMYŚLNIE ...)
... potem Ellipse.
W ten sposób:
(Ma to teraz sens dla ludzi posługujących się językiem. Mamy już jasno zdefiniowaną koncepcję „koła” w naszym umyśle, a to, co próbujemy tutaj osiągnąć poprzez uogólnienie (agregację), łamie tę koncepcję).
źródło
Z perspektywy EO elipsa rozszerza okrąg, specjalizuje się w niej, dodając pewne właściwości. Istniejące właściwości koła nadal utrzymują się w elipsie, stają się bardziej złożone i bardziej szczegółowe. W tym przypadku nie widzę żadnych problemów z zachowaniem, podobnie jak Cormac, kształty nie zachowują się. Jedyny problem polega na tym, że w sensie liguistycznym lub matematycznym nie jest właściwe mówić „elipsa jest kołem”. Ponieważ cały punkt ćwiczenia, który nie został wymieniony, ale jest jednak domyślny, polegał na klasyfikacji kształtów geometrycznych. To może być dobry powód, aby uważać okrąg i elipsę za rówieśników, a nie łączyć je przez dziedziczenie i zaakceptować, że akurat mają one te same właściwości i NIE pozwól, by twój pokręcony umysł OO miał swoją drogę do tej obserwacji.
źródło