Jak wdrożyć dziedziczenie RealNumber i ComplexNumber?

11

Mam nadzieję, że nie jest zbyt akademicki ...

Powiedzmy, że potrzebuję prawdziwych i złożonych liczb w mojej bibliotece SW.

Na podstawie relacji is-a (lub tutaj ) liczba rzeczywista jest liczbą zespoloną, gdzie b w urojonej części liczby zespolonej wynosi po prostu 0.

Z drugiej strony, moja implementacja byłaby taka, że ​​dziecko rozszerza element nadrzędny, więc w elemencie RealNumber miałbym prawdziwą część, a element potomny ComplexNumber dodałby sztukę wymyśloną.

Istnieje również opinia, że dziedzictwo jest złe .

Pamiętam, jak wczoraj, kiedy uczyłem się OOP na uniwersytecie, mój profesor powiedział, że to nie jest dobry przykład dziedziczenia, ponieważ wartość bezwzględna tych dwóch jest obliczana inaczej (ale do tego mamy przeciążenie metody / polimorfizm, prawda?) .. .

Z mojego doświadczenia wynika, że ​​często używamy dziedziczenia do rozwiązywania problemu DRY, w wyniku czego często mamy sztuczne abstrakcyjne klasy w hierarchii (często mamy problem ze znalezieniem nazw, ponieważ nie reprezentują one obiektów z prawdziwego świata).

Betlista
źródło
7
wygląda to tak, jak w poprzednim pytaniu: Czy prostokąt powinien dziedziczyć z kwadratu?
komara
1
@gnat O rany, to był kolejny przykład, którego chciałem użyć ... Dzięki!
Betlista
7
... Zwróć uwagę, że zdanie „liczba rzeczywista jest liczbą zespoloną” w sensie matematycznym dotyczy tylko liczb niezmiennych , więc jeśli używasz obiektów niezmiennych, możesz uniknąć naruszenia LSP (to samo dotyczy kwadratów i prostokątów, zobacz to TAK odpowiedź ).
Doc Brown
5
... Uwaga: obliczanie wartości bezwzględnej dla liczb zespolonych działa również dla liczb rzeczywistych, więc nie jestem pewien, co miał na myśli twój profesor. Jeśli poprawnie zaimplementujesz metodę „Abs ()” w niezmiennej liczbie zespolonej i wyprowadzisz z niej „rzeczywistą”, metoda Abs () nadal będzie zapewniać prawidłowe wyniki.
Doc Brown
3
Możliwy duplikat Czy prostokąt powinien dziedziczyć z kwadratu?
BobDalgleish

Odpowiedzi:

17

Nawet jeśli w sensie matematycznym liczba rzeczywista jest liczbą zespoloną, nie jest dobrym pomysłem wyprowadzać rzeczywistą liczbę złożoną. Jest to sprzeczne z zasadą podstawienia Liskowa, mówiącą (między innymi), że klasa pochodna nie powinna ukrywać właściwości klasy podstawowej.

W takim przypadku liczba rzeczywista musiałaby ukryć urojoną część liczby zespolonej. Oczywiste jest, że nie ma sensu przechowywać ukrytej liczby zmiennoprzecinkowej (części urojonej), jeśli potrzebujesz tylko prawdziwej części.

Jest to w zasadzie ten sam problem, co przykład prostokąta / kwadratu wspomniany w komentarzu.

Frank Puffer
źródło
2
Dzisiaj kilkakrotnie widziałem tę „Zasadę Zastępstwa Liskowa”, będę musiał przeczytać o niej więcej, ponieważ tego nie wiem.
Betlista
7
Zupełnie dobrze jest zgłaszać wyimaginowaną część liczby rzeczywistej jako zero, np. Metodą tylko do odczytu. Ale nie ma sensu implementować liczby rzeczywistej jako liczby zespolonej, w której część urojona jest ustawiona na zero. Jest to dokładnie przypadek, w którym dziedziczenie wprowadza w błąd: podczas gdy dziedziczenie interfejsu byłoby prawdopodobnie w porządku, dziedziczenie implementacji spowodowałoby problematyczny projekt.
amon
4
Ma sens dziedziczenie liczb rzeczywistych po liczbach zespolonych, o ile obie są niezmienne. I nie masz nic przeciwko narzutom.
Deduplicator
@Deduplicator: Interesujący punkt. Niezmienność rozwiązuje wiele problemów, ale nie jestem jeszcze w pełni przekonany w tym przypadku. Muszę o tym pomyśleć.
Frank Puffer
3

nie jest to dobry przykład dziedziczenia, ponieważ wartość bezwzględna tych dwóch wartości jest obliczana inaczej

W rzeczywistości nie jest to istotny powód przeciwko wszelkiemu dziedzictwu, tylko proponowany model class RealNumber<-> class ComplexNumber.

Możesz rozsądnie zdefiniować interfejs Number, który zarówno RealNumber i jak i ComplexNumberzaimplementuje.

To może wyglądać

interface Number
{
    Number Add(Number rhs);
    Number Subtract(Number rhs);
    // ... etc
}

Ale wtedy chcesz ograniczyć inne Numberparametry w tych operacjach, aby były tego samego typu pochodnego this, co, do którego możesz się zbliżyć za pomocą

interface Number<T>
{
    Number<T> Add(Number<T> rhs);
    Number<T> Subtract(Number<T> rhs);
    // ... etc
}

Lub zamiast tego użyjesz języka, który dopuszcza polimorfizm strukturalny, zamiast polimorfizmu podtypu. W konkretnym przypadku liczb może być potrzebna tylko możliwość przeciążenia operatorów arytmetycznych.

complex operator + (complex lhs, complex rhs);
complex operator - (complex lhs, complex rhs);
// ... etc

Number frobnicate<Number>(List<Number> foos, Number bar); // uses arithmetic operations
Caleth
źródło
0

Rozwiązanie: Nie masz RealNumberklasy publicznej

Uważałbym, że byłoby całkowicie OK, gdyby ComplexNumberdysponował statyczną metodą fabryczną fromDouble(double), która zwróciłaby liczbę zespoloną z wyimaginowaną wartością zero. Następnie można użyć wszystkich operacji, które można wykonać RealNumberna instancji w tej ComplexNumberinstancji.

Ale mam problem ze zrozumieniem, dlaczego chcesz / musisz mieć RealNumberklasę odziedziczoną publicznie . Zwykle z tych powodów używa się dziedziczenia (z mojej głowy, popraw mnie, jeśli coś przegapiłem)

  • przedłużenie zachowania. RealNumbersnie można wykonać żadnych dodatkowych operacji, których liczba złożona nie może zrobić, więc nie ma sensu tego robić.

  • implementowanie abstrakcyjnych zachowań z konkretną implementacją. Ponieważ ComplexNumbernie powinno to być abstrakcyjne, nie dotyczy to również.

  • ponowne użycie kodu. Jeśli użyjesz ComplexNumberklasy, ponownie wykorzystasz 100% kodu.

  • bardziej konkretna / wydajna / dokładna implementacja dla konkretnego zadania. Można to tutaj zastosować, RealNumbersszybciej zaimplementować niektóre funkcjonalności. Ale wtedy ta podklasa powinna być ukryta za statyczną fromDouble(double)i nie powinna być znana na zewnątrz. W ten sposób nie trzeba ukrywać części wyobrażonej. Na zewnątrz powinny być tylko liczby zespolone (którymi są liczby rzeczywiste). Możesz także zwrócić tę prywatną klasę RealNumber z dowolnych operacji w klasie liczb zespolonych, które dają liczbę rzeczywistą. (Zakłada się, że klasy są niezmienne, jak większość klas liczbowych).

To jest jak zaimplementowanie podklasy Integer o nazwie Zero i zakodować niektóre operacje, ponieważ są one trywialne dla zera. Możesz to zrobić, ponieważ każde zero jest liczbą całkowitą, ale nie upubliczniaj, ukryj to za metodą fabryczną.

findusl
źródło
Nie jestem zaskoczony, że otrzymałem trochę opinii, ponieważ nie mam źródła do udowodnienia. Także jeśli nikt inny nie miał pomysłu, zawsze podejrzewam, że może istnieć jakiś powód. Ale proszę, powiedz mi, dlaczego uważasz, że to źle i jak możesz to poprawić.
findusl
0

Mówienie, że liczba rzeczywista jest liczbą zespoloną, ma większe znaczenie w matematyce, zwłaszcza w teorii mnogości, niż w informatyce.
W matematyce mówimy:

  • Liczba rzeczywista jest liczbą zespoloną, ponieważ zbiór liczb zespolonych obejmuje zbiór liczb rzeczywistych.
  • Liczba wymierna jest liczbą rzeczywistą, ponieważ zbiór liczb rzeczywistych obejmuje zbiór liczb wymiernych (i zbiór liczb niewymiernych).
  • Liczba całkowita jest liczbą wymierną, ponieważ zestaw liczb wymiernych obejmuje zbiór liczb całkowitych.

Nie oznacza to jednak, że musisz, a nawet powinieneś używać dziedziczenia podczas projektowania biblioteki tak, aby zawierała klasy RealNumber i ComplexNumber. In Effective Java, drugie wydanie Joshua Bloch; Punkt 16 to „Preferuj kompozycję nad spadkiem”. Aby uniknąć problemów wymienionych w tym elemencie, po zdefiniowaniu klasy RealNumber można jej użyć w klasie ComplexNumber:

public class ComplexNumber {
    private RealNumber realPart;
    private RealNumber imaginaryPart;

    // Implementation details are for you to write
}

Dzięki temu możesz ponownie wykorzystać swoją klasę RealNumber, aby utrzymać kod w stanie SUCHO, unikając problemów zidentyfikowanych przez Joshua Blocha.

Craig Noah
źródło
0

Istnieją tutaj dwa problemy. Po pierwsze, powszechne jest używanie tych samych terminów dla rodzajów kontenerów i rodzajów ich zawartości, szczególnie w przypadku prymitywnych typów, takich jak liczby. Termin ten jest doublena przykład używany do opisania zarówno zmiennoprzecinkowej podwójnej precyzji, jak i pojemnika, w którym można ją przechowywać.

Druga kwestia polega na tym, że chociaż relacje między kontenerami, z których można odczytać różne typy obiektów, zachowują się tak samo, jak relacje między samymi obiektami, te między kontenerami, w których można umieszczać różne typy obiektów, zachowują się przeciwnie do swoich treści . Każda klatka, o której wiadomo, że zawiera instancję, Catbędzie klatką, która zawiera instancję Animal, ale nie musi być klatką, która zawiera instancję SiameseCat. Z drugiej strony, każda klatka, która może pomieścić wszystkie instancje, Catbędzie klatką, która może pomieścić wszystkie instancje SiameseCat, ale nie musi być klatką, która może pomieścić wszystkie instancje Animal. Jedyny rodzaj klatki, który może pomieścić wszystkie wystąpienia Cati może być zagwarantowany, nigdy nie pomieści niczego innego niż przypadekCat, jest klatką Cat. Każdy inny rodzaj klatki albo nie byłby w stanie zaakceptować niektórych przypadków, Catktóre powinien zaakceptować, albo byłby w stanie zaakceptować rzeczy, które nie są przypadkami Cat.

supercat
źródło