Dziedziczenie a dodatkowa właściwość o wartości null

12

Czy w przypadku klas z polami opcjonalnymi lepiej jest użyć dziedziczenia lub właściwości zerowalnej? Rozważ ten przykład:

class Book {
    private String name;
}
class BookWithColor extends Book {
    private String color;
}

lub

class Book {
    private String name;
    private String color; 
    //when this is null then it is "Book" otherwise "BookWithColor"
}

lub

class Book {
    private String name;
    private Optional<String> color;
    //when isPresent() is false it is "Book" otherwise "BookWithColor"
}

Kod zależny od tych 3 opcji to:

if (book instanceof BookWithColor) { ((BookWithColor)book).getColor(); }

lub

if (book.getColor() != null) { book.getColor(); }

lub

if (book.getColor().isPresent()) { book.getColor(); }

Pierwsze podejście wydaje mi się bardziej naturalne, ale może jest mniej czytelne z powodu konieczności castingu. Czy jest jakiś inny sposób na osiągnięcie tego zachowania?

Bojan VukasovicTest
źródło
1
Obawiam się, że rozwiązanie sprowadza się do twojej definicji reguł biznesowych. Mam na myśli, że jeśli wszystkie twoje książki mogą mieć kolor, ale kolor jest opcjonalny, to dziedziczenie jest przesadne i faktycznie niepotrzebne, ponieważ chcesz, aby wszystkie książki wiedziały, że właściwość koloru istnieje. Z drugiej strony, jeśli możesz mieć zarówno książki, które nic nie wiedzą o kolorze, jak i książki, które mają kolor, tworzenie specjalistycznych zajęć nie jest złe. Nawet wtedy prawdopodobnie wybrałbym kompozycję zamiast dziedziczenia.
Andy
Aby było bardziej szczegółowe - istnieją 2 rodzaje książek - jedna z kolorem, a druga bez. Dlatego nie wszystkie książki mogą mieć kolor.
Bojan VukasovicTest
1
Jeśli naprawdę jest przypadek, w którym książka w ogóle nie ma koloru, w twoim przypadku dziedziczenie jest najprostszym sposobem na rozszerzenie właściwości kolorowej książki. W tym przypadku nie wygląda na to, że coś byś zepsuł, dziedzicząc klasę podstawowej książki i dodając do niej właściwość color. Możesz przeczytać o zasadzie podstawienia Liskowa, aby dowiedzieć się więcej o przypadkach, w których rozszerzenie klasy jest dozwolone, a kiedy nie.
Andy
4
Czy potrafisz zidentyfikować pożądane zachowanie na podstawie książek z kolorowaniem? Czy potrafisz znaleźć wspólne zachowanie wśród książek z kolorowaniem i bez, w których książki z kolorowaniem powinny zachowywać się nieco inaczej? Jeśli tak, to jest to dobry przypadek dla OOP z różnymi typami, a pomysłem byłoby przeniesienie zachowań do klas, zamiast sprawdzania obecności i wartości właściwości na zewnątrz.
Erik Eidt
1
Jeśli możliwe kolory książek są znane z wyprzedzeniem, co powiesz na pole Enum dla BookColor z opcją BookColor.NoColor?
Graham

Odpowiedzi:

7

To zależy od okoliczności. Konkretny przykład jest nierealny, ponieważ nie miałbyś podklasy wywoływanej BookWithColorw prawdziwym programie.

Ogólnie jednak właściwość, która ma sens tylko dla niektórych podklas, powinna istnieć tylko w tych podklasach.

Na przykład jeśli Bookma PhysicalBooki DigialBookjak potomkowie, to PhysicalBookmoże mieć weightwłasności, a DigitalBookna sizeInKbwłasność. Ale DigitalBooknie będzie weighti na odwrót. Booknie będzie miał żadnej właściwości, ponieważ klasa powinna mieć właściwości wspólne dla wszystkich potomków.

Lepszym przykładem jest przyglądanie się klasom z prawdziwego oprogramowania. JSliderSkładnik ma pole majorTickSpacing. Ponieważ tylko suwak ma „tyknięcia”, to pole ma sens tylko dla JSliderjego potomków. Byłoby bardzo mylące, gdyby inne elementy rodzeństwa, takie jak pole, JButtonmiały majorTickSpacingpole.

JacquesB
źródło
lub możesz zrzucić ciężar, aby móc odzwierciedlić oba, ale to chyba za dużo.
Walfrat
3
@Walfrat: Byłoby naprawdę złym pomysłem, aby to samo pole reprezentowało zupełnie różne rzeczy w różnych podklasach.
JacquesB
Co jeśli książka ma zarówno wydania fizyczne, jak i cyfrowe? Będziesz musiał stworzyć inną klasę dla każdej permutacji posiadania lub nie posiadania każdego opcjonalnego pola. Myślę, że najlepszym rozwiązaniem byłoby pominięcie niektórych pól lub ich brak w zależności od książki. Przytoczyłeś zupełnie inną sytuację, w której obie klasy zachowywałyby się inaczej funkcjonalnie. To nie oznacza tego pytania. Muszą tylko modelować strukturę danych.
4castle,
@ 4castle: Potrzebne byłyby wtedy oddzielne zajęcia na wydanie, ponieważ każda edycja może mieć inną cenę. Nie możesz tego rozwiązać za pomocą opcjonalnych pól. Ale nie wiemy z tego przykładu, czy operacja chce innego zachowania dla książek z kolorowymi lub „bezbarwnymi książkami”, więc nie można wiedzieć, jakie jest prawidłowe modelowanie w tym przypadku.
JacquesB
3
zgadzam się z @JacquesB. nie możesz podać uniwersalnie poprawnego rozwiązania dla „modelowania książek”, ponieważ zależy to od tego, jaki problem próbujesz rozwiązać i jaki jest Twój biznes. Myślę, że dość bezpiecznie jest powiedzieć, że definiowanie hierarchii klas, w których narusza się Liskov, jest kategorycznie Złe (tm) (tak, tak, twoja sprawa jest prawdopodobnie bardzo wyjątkowa i uzasadnia to (chociaż wątpię))
sara
5

Ważna kwestia, o której najwyraźniej nie wspomniano: W większości języków instancje klasy nie mogą zmienić klasy, której są instancjami. Jeśli więc masz książkę bez koloru i chcesz dodać kolor, musisz utworzyć nowy obiekt, jeśli używasz różnych klas. I wtedy prawdopodobnie będziesz musiał zastąpić wszystkie odniesienia do starego obiektu odniesieniami do nowego obiektu.

Jeśli „książki bez koloru” i „książki z kolorem” są instancjami tej samej klasy, wówczas dodanie koloru lub usunięcie koloru będzie znacznie mniejszym problemem. (Jeśli interfejs użytkownika wyświetla listę „książek z kolorem” i „książek bez koloru”, interfejs ten musiałby się oczywiście zmienić, ale przypuszczam, że i tak należy się tym zająć, podobnie jak „lista czerwonych” książki ”i„ lista zielonych książek ”).

gnasher729
źródło
To naprawdę ważny punkt! Określa, czy należy rozważyć zbiór opcjonalnych właściwości zamiast atrybutów klasy.
chromanoid
1

Pomyśl o JavaBean (jak twój Book) jako rekordzie w bazie danych. Opcjonalne kolumny są, nullgdy nie mają żadnej wartości, ale jest to całkowicie legalne. Dlatego twoja druga opcja:

class Book {
    private String name;
    private String color; // null when not applicable
}

Jest najbardziej rozsądny. 1

Uważaj jednak na sposób korzystania z Optionalklasy. Na przykład tak nie jest Serializable, co zwykle jest cechą JavaBean. Oto kilka wskazówek od Stephena Colebourne'a :

  1. Nie deklaruj żadnej zmiennej typu Optional.
  2. Służy nulldo wskazywania opcjonalnych danych w prywatnym zakresie klasy.
  3. Użyj Optionaldla pobierających, którzy uzyskują dostęp do pola opcjonalnego.
  4. Nie używaj Optionalw seterach lub konstruktorach.
  5. Użyj Optionaljako typu zwrotu dla wszelkich innych metod logiki biznesowej, które dają wynik opcjonalny.

W związku z tym, w ciągu swojej klasie należy użyć nulldo reprezentowania, że pole nie jest obecny, ale kiedy color odchodzi się Book(jako typ zwracany) powinny być opakowane ze związkiem Optional.

return Optional.ofNullable(color); // inside the class

book.getColor().orElse("No color"); // outside the class

Zapewnia to przejrzysty wygląd i czytelniejszy kod.


1 Jeśli masz zamiar dotyczący BookWithColoraby upakować całą „klasa” książek, które wyspecjalizowane funkcje w stosunku do innych książek, wtedy byłoby sensu używać dziedziczenia.

4castle
źródło
1
Kto powiedział coś o bazie danych? Gdyby wszystkie wzorce OO pasowały do ​​paradygmatu relacyjnej bazy danych, musielibyśmy zmienić wiele wzorców OO.
alexanderbird
Argumentowałbym, że dodanie nieużywanych właściwości „na wszelki wypadek” jest najmniej przejrzystą opcją. Jak powiedzieli inni, zależy to od przypadku użycia, ale najbardziej pożądaną sytuacją byłoby nie przenoszenie właściwości, w których zewnętrzna aplikacja musi wiedzieć, czy faktycznie istnieje używana właściwość.
Volker Kueffel
@Volker Zgódźmy się nie zgadzać, ponieważ mogę zacytować kilka osób, które odegrały pewną rolę, dodając Optionalklasę do Javy właśnie w tym celu. To może nie być OO, ale z pewnością jest to ważna opinia.
4castle
@Alexanderbird W szczególności zajmowałem się komponentem JavaBean, który powinien być możliwy do serializacji. Jeśli nie mogłem poruszyć tematu JavaBeans, to był to mój błąd.
4castle
@Alexanderbird Nie oczekuję, że wszystkie wzorce będą pasować do relacyjnej bazy danych, ale spodziewam się, że Java Bean to
4castle
0

Powinieneś albo użyć interfejsu (a nie dziedziczenia), albo jakiegoś mechanika własności (może nawet czegoś, co działa jak struktura opisu zasobów).

Więc albo

interface Colored {
  Color getColor();
}
class ColoredBook extends Book implements Colored {
  ...
}

lub

class PropertiesHolder {
  <T> extends Property<?>> Optional<T> getProperty( Class<T> propertyClass ) { ... }
  <V, T extends Property<V>> Optional<V> getPropertyValue( Class<T> propertyClass ) { ... }
}
interface Property<T> {
  String getName();
  T getValue();
}
class ColorProperty implements Property<Color> {
  public Color getValue() { ... }
  public String getName() { return "color"; }
}
class Book extends PropertiesHolder {
}

Wyjaśnienie (edycja):

Po prostu dodając opcjonalne pola w klasie encji

Zwłaszcza w przypadku Opcjonalnego opakowania (edycja: patrz odpowiedź 4castle ) Myślę, że to (dodanie pól w oryginalnej encji) jest realnym sposobem na dodanie nowych właściwości na małą skalę. Największym problemem związanym z tym podejściem jest to, że może on działać przeciwko niskiemu sprzężeniu.

Wyobraź sobie, że twoja klasa książek jest zdefiniowana w dedykowanym projekcie dla twojego modelu domeny. Teraz dodajesz kolejny projekt, który używa modelu domeny do wykonania specjalnego zadania. To zadanie wymaga dodatkowej właściwości w klasie książek. Albo skończysz z dziedziczeniem (patrz poniżej), albo musisz zmienić wspólny model domeny, aby umożliwić nowe zadanie. W tym drugim przypadku możesz skończyć z wieloma projektami, które wszystkie zależą od ich własnych właściwości dodanych do klasy book, podczas gdy sama klasa book w pewien sposób zależy od tych projektów, ponieważ nie jesteś w stanie zrozumieć klasy book bez dodatkowe projekty.

Dlaczego dziedziczenie jest problematyczne, jeśli chodzi o sposób zapewnienia dodatkowych właściwości?

Kiedy widzę przykładową klasę „Book”, myślę o obiekcie domeny, który często ma wiele opcjonalnych pól i podtypów. Wyobraź sobie, że chcesz dodać właściwość do książek zawierających dysk CD. Istnieją teraz cztery rodzaje książek: książki, książki z kolorami, książki z CD, książki z kolorami i CD. Nie można opisać tej sytuacji za pomocą dziedziczenia w Javie.

Dzięki interfejsom można obejść ten problem. Możesz łatwo skomponować właściwości określonej klasy książki za pomocą interfejsów. Delegacja i skład ułatwią ci zdobycie dokładnie takiej klasy, jakiej chcesz. W przypadku dziedziczenia zazwyczaj uzyskuje się pewne opcjonalne właściwości w klasie, które są dostępne tylko dlatego, że potrzebuje ich klasa rodzeństwa.

Czytaj dalej, dlaczego dziedziczenie jest często problematycznym pomysłem:

Dlaczego dziedziczenie jest ogólnie postrzegane przez zwolenników OOP jako coś złego

JavaWorld: Dlaczego rozszerzenie jest złem

Problem z implementacją zestawu interfejsów w celu skomponowania zestawu właściwości

Gdy używasz interfejsów do rozszerzenia, wszystko jest w porządku, o ile masz ich tylko niewielki zestaw. Zwłaszcza, gdy Twój model obiektowy jest używany i rozszerzany przez innych programistów, np. W Twojej firmie, liczba interfejsów wzrośnie. I w końcu tworzysz nowy oficjalny „interfejs własności”, który dodaje metodę, którą koledzy już zastosowali w swoim projekcie dla klienta X dla zupełnie niezwiązanego przypadku użycia - Ugh.

edycja: Kolejny ważny aspekt wspomniany jest przez gnasher729 . Często chcesz dynamicznie dodawać opcjonalne właściwości do istniejącego obiektu. W przypadku dziedziczenia lub interfejsów musiałbyś odtworzyć cały obiekt z inną klasą, co jest dalekie od opcjonalnego.

Kiedy spodziewasz się rozszerzeń swojego modelu obiektowego w takim stopniu, lepiej skorzystasz z jawnego modelowania możliwości dynamicznego rozszerzenia. Proponuję coś takiego jak powyżej, w którym każde „rozszerzenie” (w tym przypadku właściwość) ma swoją własną klasę. Klasa działa jako przestrzeń nazw i identyfikator rozszerzenia. Gdy odpowiednio ustawisz konwencje nazewnictwa pakietów, w ten sposób zezwolisz na nieskończoną liczbę rozszerzeń bez zanieczyszczania przestrzeni nazw metodami w oryginalnej klasie encji.

Podczas tworzenia gry często napotykasz sytuacje, w których chcesz komponować zachowania i dane w wielu odmianach. Właśnie dlatego wzorzec architektury jednostka-komponent-system stał się bardzo popularny w kręgach twórców gier. Jest to również interesujące podejście, na które warto spojrzeć, gdy oczekujesz wielu rozszerzeń swojego modelu obiektowego.

chromanoid
źródło
I nie odpowiedział na pytanie „jak decydować między tymi opcjami”, to tylko inna opcja. Dlaczego jest to zawsze lepsze niż wszystkie opcje przedstawione przez PO?
alexanderbird
Masz rację, poprawię odpowiedź.
chromanoid
@ 4castle Interfejsy Java nie są dziedziczone! Implementacja interfejsów jest sposobem na zachowanie zgodności z umową, podczas gdy dziedziczenie klasy zasadniczo rozszerza jej zachowanie. Można zaimplementować wiele interfejsów, a jednocześnie nie można rozszerzyć wielu klas w jednej klasie. W moim przykładzie korzystasz z interfejsów podczas dziedziczenia, aby odziedziczyć zachowanie oryginalnej encji.
chromanoid
Ma sens. W twoim przykładzie PropertiesHolderprawdopodobnie byłaby to klasa abstrakcyjna. Ale co sugerowałbyś jako implementację dla getPropertylub getPropertyValue? Dane nadal muszą być gdzieś przechowywane w jakimś polu instancji. A może użyłbyś a Collection<Property>do przechowywania właściwości zamiast pól?
4castle,
1
Zrobiłem Bookprzedłużanie PropertiesHolderze względu na przejrzystość. PropertiesHolderZwykle powinien być zarejestrowany we wszystkich klasach, które potrzebują tej funkcji. Implementacja będzie zawierać Map<Class<? extends Property<?>>,Property<?>>coś w tym rodzaju. Jest to brzydka część implementacji, ale zapewnia naprawdę fajną platformę, która działa nawet z JAXB i JPA.
chromanoid
-1

Jeśli Bookklasa jest pochodna bez właściwości koloru, jak poniżej

class E_Book extends Book{
   private String type;
}

to dziedziczenie jest uzasadnione.

Adem Caglin
źródło
Nie jestem pewien, czy rozumiem twój przykład? Jeśli colorjest prywatny, to żadna klasa pochodna i tak nie powinna się tym przejmować color. Po prostu dodaj kilka konstruktorów do Booktego ignorowania, colora domyślnie będzie to null.
4castle