Czy można rozwiązać problem elipsy koła poprzez odwrócenie relacji?

13

Po CirclerozszerzeniuEllipse ł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 setXi setY. Wywołanie setRadiusEllipse 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.

HorusKol
źródło
1
Czy najpierw zajrzałeś do Wikipedii ( en.wikipedia.org/wiki/Circle-ellipse_problem )?
Doc Brown
1
tak - nawet
łączę
6
I ten właśnie punkt jest omawiany w tym artykule, więc nie jestem pewien, o co pytasz?
Philip Kendall
6
„Niektórzy autorzy sugerowali odwrócenie relacji między kołem a elipsą, uzasadniając to tym, że elipsa jest kołem z dodatkowymi możliwościami. Niestety elipsy nie spełniają wielu niezmienników kół; jeśli okrąg ma promień metody, elipsa będzie miała również to zapewnić ”.
Philip Kendall,
3
To, co uważam za najjaśniejsze wytłumaczenie tego, dlaczego ten problem ma złe przesłanki, znajduje się na samym dole artykułu w Wikipedii: en.wikipedia.org/wiki/… . W zależności od sytuacji, istnieje kilka czystych wzorów, ale to zależy od tego, co trzeba z tych dwóch klas do zrobienia , aby nie być .
Arthur Havlicek,

Odpowiedzi:

38

Ale czy problem nie jest spowodowany tym, że Circle jest podtypem elipsy? Czy nie moglibyśmy odwrócić relacji?

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.

Cormac Mulhall
źródło
Bardzo ważne jest oddzielenie obaw domeny (aplikacji) od możliwości behawioralnych i odpowiedzialności OOD. Na przykład w aplikacji do rysowania być może powinieneś być w stanie przekształcić okrąg w kwadrat, ale nie jest to łatwe do modelowania przy użyciu klas / obiektów w większości języków (ponieważ obiekty zwykle nie mogą zmienić klasy). Zatem domena aplikacji nie zawsze jest dobrze odwzorowana na hierarchię dziedziczenia danego języka OOP i nie powinniśmy próbować narzucać jej; w wielu przypadkach skład jest lepszy.
Erik Eidt,
3
Ta odpowiedź jest zdecydowanie najlepszą rzeczą, jaką widziałem w całym problemie, i tym, jak potencjalne błędy projektowe mogą pojawić się w bardziej ogólnych przypadkach. Dzięki
HorusKol,
1
@ErikEidt Problem zmiany obiektu może zostać rozwiązany w OOD poprzez dekompozycję. Na przykład, jeśli kształt ulegający przemianie zmienia się w okrąg, nie musisz zmieniać klasy. Zamiast tego klasa przyjmuje bieżący obiekt zachowania geometrycznego, który można zamienić na inne zachowanie podczas przekształcania. Ta druga klasa zawiera reguły modelowanego kształtu geometrycznego, a klasa kształtu morfalnego odkłada się do tej klasy w celu zachowania geometrycznego. Jeśli obiekt zmienia się w inną klasę, zmieniasz klasę zachowania na coś innego.
Cormac Mulhall
2
@Cormac, prawda! Generalnie nazwałbym to formą kompozycji, jak już wspomniałem, chociaż można by zidentyfikować, bardziej konkretnie, wzór strategii lub coś takiego. Zasadniczo masz tożsamość, która się nie zmienia, i inne rzeczy, które można następnie zmienić. Podsumowując, dobrze podkreśla różnicę między koncepcjami domen aplikacji i szczegółami OOP danego języka oraz potrzebę mapowania między nimi (tj. Architektury, projektowania i programowania).
Erik Eidt,
1
Ale praca może być wypłatą.
8

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żniejsze getRadius())

Podczas gdy modelowanie kół jako podtypu elips nie jest zasadniczo błędne, to wprowadzenie zmienności łamie ten model. Bez setX()i setY()metod, nie ma naruszenia LSP. Jeśli istnieje potrzeba posiadania obiektu o różnych wymiarach, lepszym rozwiązaniem jest utworzenie nowej instancji:

class Ellipse {
  final double x;
  final double y;
  ...
  Ellipse withX(double newX) {
    return new Ellipse(x: newX, y: y);
  }
}
amon
źródło
1
dobrze - jeśli więc istnieje jakiś wspólny interfejs między Ellipsei Circle(taki jak getArea), który zostałby wyodrębniony do typu Shape- mógłby Ellipsei Circleosobno podtyp Shapei spełniać LSP?
HorusKol,
1
@HorusKol Tak. Dwie klasy dziedziczące interfejs, który obie implementują poprawnie, są w porządku.
Ixrec
7

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:

  1. 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.

  2. Traktuj wszystkie elipsy, w tym okręgi, tak samo, ale masz opcję „zablokowania” xiy na tej samej wartości.

  3. 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.

JacquesB
źródło
1
Bardzo dobra odpowiedź!
Utsav T
6

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”.

gnasher729
źródło
Może istnieć metoda IsCircle, która sprawdza, czy konkretny obiekt klasy Ellipse w rzeczywistości ma obie osie takie same. Zwrócił pan również uwagę na kwestię kąta. Kręgów nie można „obracać”.
3

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ć.

  • Podtyp jest SUPERSETEM typu super.
  • Nadtyp jest PODSETEM podtypu.

Po angielsku:

  • Typ pochodny jest SUPERSET typu podstawowego.
  • Typ podstawowy jest PODSETEM typu pochodnego.

(Przykład:

  • Samochód z wydechem złego chłopca jest nadal samochodem (według niektórych).
  • Samochód bez silnika, kół, drążka kierowniczego, układu napędowego i tylko skorupa pozostała, nie jest „samochodem”, ale tylko skorupą.)

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:

  • RoundedShape to Kształt.
  • Elipsa jest zaokrąglonym kształtem.

(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ę).

Andy Dobrze
źródło
Nasze jasno zdefiniowane koncepcje nie zawsze sprawdzają się w praktyce.
-1

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.

Martin Maat
źródło