Wiele małych klas a logiczne (ale) zawiłe dziedziczenie

24

Zastanawiam się, co jest lepsze pod względem dobrego projektowania OOP, czystego kodu, elastyczności i unikania zapachów kodu w przyszłości. Sytuacja obrazowa, w której masz wiele bardzo podobnych obiektów, które musisz reprezentować jako klasy. Klasy te nie mają żadnej konkretnej funkcjonalności, tylko klasy danych i różnią się tylko nazwą (i kontekstem) Przykład:

Class A
{
  String name;
  string description;
}

Class B
{
  String name;
  String count;
  String description;
}

Class C
{
  String name;
  String count;
  String description;
  String imageUrl;
}

Class D
{
  String name;
  String count;
}

Class E
{
  String name;
  String count;
  String imageUrl;
  String age;
}

Czy lepiej byłoby przechowywać je w osobnych klasach, aby uzyskać „lepszą” czytelność, ale przy wielu powtórzeniach kodu, czy lepiej byłoby zastosować dziedziczenie, aby uzyskać większą SUSZENIE?

Używając dziedziczenia, uzyskasz coś takiego, ale nazwy klas utracą znaczenie kontekstowe (z powodu a-a, a nie-a):

Class A
{
  String name;
  String description;
}

Class B : A
{
  String count;
}

Class C : B
{
  String imageUrl;
}

Class D : C
{
  String age;
}

Wiem, że dziedziczenie nie jest i nie powinno być używane do ponownego użycia kodu, ale nie widzę żadnego innego możliwego sposobu na ograniczenie powtarzania kodu w takim przypadku.

użytkownik969153
źródło

Odpowiedzi:

58

Ogólna zasada brzmi: „Preferuj przekazanie zamiast dziedziczenia”, a nie „unikaj wszelkiego dziedziczenia”. Jeśli obiekty mają logiczną relację, a B można stosować wszędzie tam, gdzie spodziewane jest A, dobrą praktyką jest stosowanie dziedziczenia. Jeśli jednak obiekty po prostu mają pola o tej samej nazwie i nie mają związku z domeną, nie używaj dziedziczenia.

Dość często opłaca się zadać pytanie o „przyczynę zmiany” w procesie projektowania: jeśli klasa A otrzyma dodatkowe pole, czy spodziewałbyś się, że klasa B również je otrzyma? Jeśli to prawda, obiekty mają związek, a dziedziczenie jest dobrym pomysłem. Jeśli nie, cierpię z powodu niewielkiej liczby powtórzeń, aby zachować różne pojęcia w odrębnym kodzie.

Thiton
źródło
Oczywiście, powód zmiany jest słuszny, ale co w sytuacji, w której klasy te są i zawsze będą stałe?
user969153
20
@ user969153: Gdy klasy będą naprawdę stałe, nie ma to znaczenia. Cała dobra inżynieria oprogramowania jest przeznaczona dla przyszłego opiekuna, który musi wprowadzać zmiany. Jeśli nie będzie żadnych zmian w przyszłości, wszystko pójdzie, ale zaufaj mi, zawsze będziesz mieć zmiany, chyba że zaprogramujesz kosz na śmieci.
thiton
@ user969153: To powiedziawszy, „powód zmiany” jest heurystyczny. Pomaga ci zdecydować, nic więcej.
thiton
2
Dobra odpowiedź. Powodem zmiany jest S w SOLIDNYM akronimie wuja boba, który pomoże w zaprojektowaniu dobrego oprogramowania.
Klee
4
@ user969153, Z mojego doświadczenia wynika, „gdzie te klasy są i zawsze będą stałe?” nie jest prawidłowym przypadkiem użycia :) Muszę jeszcze pracować nad aplikacją o znacznych rozmiarach, która nie doświadczyła całkowicie nieoczekiwanych zmian.
cdkMoose
18

Używając dziedziczenia, uzyskasz coś takiego, ale nazwy klas utracą znaczenie kontekstowe (z powodu a-a, a nie-a):

Dokładnie. Spójrz na to poza kontekstem:

Class D : C
{
    String age;
}

Jakie właściwości ma litera D? Pamiętasz? Co się stanie, jeśli jeszcze bardziej skomplikujesz hierarchię:

Class E : B
{
    int numberOfWheels;
}

Class F : E
{
    String favouriteColour;
}

A co jeśli horror po horrorze chcesz dodać pole imageUrl do F, ale nie do E? Nie możesz wyprowadzić go z C i E.

Widziałem rzeczywistą sytuację, w której wiele różnych typów komponentów miało nazwę, identyfikator i opis. Ale nie było to z natury ich składników, to był tylko zbieg okoliczności. Każdy z nich miał identyfikator, ponieważ były przechowywane w bazie danych. Nazwa i opis były do ​​wyświetlenia.

Produkty miały również identyfikator, nazwę i opis z podobnych powodów. Ale to nie były komponenty, ani też komponenty. Nigdy nie zobaczyłbyś tych dwóch rzeczy razem.

Ale jakiś programista przeczytał o DRY i zdecydował, że usunie wszelkie ślady duplikacji z kodu. Nazwał więc wszystko produktem i zrezygnował z konieczności definiowania tych pól. Zostały one zdefiniowane w produkcie i wszystko, co wymagało tych pól, mogło pochodzić z produktu.

Nie mogę powiedzieć wystarczająco mocno: To nie jest dobry pomysł.

DRY nie polega na usuwaniu potrzeby posiadania wspólnych właściwości. Jeśli obiekt nie jest Produktem, nie nazywaj go Produktem. Jeśli Produkt nie WYMAGA Opisu, ze względu na jego charakter jest nim, nie definiuj Opisu jako Własności Produktu.

W końcu zdarzyło się, że mieliśmy Komponenty bez opisów. Zaczęliśmy więc mieć puste opisy w tych obiektach. Nie ma wartości zerowej. Zero. Zawsze. Dla każdego produktu typu X ... lub później Y ... i N.

Jest to rażące naruszenie zasady substytucji Liskowa.

DRY polega na tym, aby nigdy nie stawić się w sytuacji, w której możesz naprawić błąd w jednym miejscu i zapomnieć o zrobieniu tego gdzie indziej. Tak się nie stanie, ponieważ masz dwa obiekty o tej samej właściwości. Nigdy. Więc nie martw się o to.

Jeśli kontekst klas pokazuje, że powinieneś, co będzie bardzo rzadkie, wtedy i tylko wtedy powinieneś rozważyć wspólną klasę nadrzędną. Ale jeśli są to właściwości, zastanów się, dlaczego nie używasz interfejsu.

pdr
źródło
4

Dziedziczenie jest sposobem na osiągnięcie zachowania polimorficznego. Jeśli twoje klasy nie zachowują się, dziedziczenie jest bezużyteczne. W tym przypadku nie wziąłbym nawet pod uwagę obiektów / klas, lecz struktury.

Jeśli chcesz mieć możliwość umieszczania różnych struktur w różnych częściach swojego zachowania, rozważ interfejsy z metodami lub właściwościami get / set, takimi jak IHasName, IHasAge itp. Dzięki temu projekt będzie znacznie czystszy i pozwoli na lepsze ich zestawienie rodzaj hierarchii.

Euforyk
źródło
3

Czy nie do tego służą interfejsy? Niepowiązane klasy o różnych funkcjach, ale o podobnej właściwości.

IName = Interface(IInterface)
  function Getname : String;
  property Name : String read Getname
end;

Class Dog(InterfacedObject, IName)
private
  FName : String;
  Function GetName : String;
Public
  Name : Read Getname;
end;

Class Movie(InterfacedObject, IName)
private
  FCount;
  FName : String;
  Function GetName : String;
Public
  Name : String Read Getname;
  Count : read FCount; 
end;
Pieter B.
źródło
1

Twoje pytanie wydaje się być sformułowane w kontekście języka, który nie obsługuje wielokrotnego dziedziczenia, ale nie określa tego konkretnie. Jeśli pracujesz w języku, który obsługuje wielokrotne dziedziczenie, staje się to trywialnie łatwe do rozwiązania z tylko jednym poziomem dziedziczenia.

Oto przykład, jak można to rozwiązać za pomocą wielokrotnego dziedziczenia.

trait Nameable { String name; }
trait Describable { String description; }
trait Countable { Integer count; }
trait HasImageUrl { String imageUrl; }
trait Living { String age; }

class A : Nameable, Describable;
class B : Nameable, Describable, Countable;
class C : Nameable, Describable, Countable, HasImageUrl;
class D : Nameable, Countable;
class E : Nameable, Countable, HasImageUrl, Living;
pgraham
źródło