Molte classi di piccole dimensioni rispetto all'eredità logica (ma) complessa


24

Mi chiedo cosa c'è di meglio in termini di buona progettazione di OOP, codice pulito, flessibilità ed evitare odori di codice in futuro. Situazione dell'immagine, in cui hai molti oggetti molto simili che devi rappresentare come classi. Queste classi sono prive di funzionalità specifiche, solo classi di dati e sono diverse solo per nome (e contesto) Esempio:

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;
}

Sarebbe meglio tenerli in classi separate, ottenere una leggibilità "migliore", ma con molte ripetizioni di codice, o sarebbe meglio usare l'ereditarietà per essere più ASCIUTTI?

Usando l'ereditarietà, finirai con qualcosa del genere, ma i nomi delle classi perderanno il significato contestuale (a causa di is-a, non has-a):

Class A
{
  String name;
  String description;
}

Class B : A
{
  String count;
}

Class C : B
{
  String imageUrl;
}

Class D : C
{
  String age;
}

So che l'ereditarietà non è e non dovrebbe essere utilizzata per il riutilizzo del codice, ma in questo caso non vedo altri modi per ridurre la ripetizione del codice.

Risposte:


58

La regola generale recita "Preferire la delega sull'ereditarietà", non "evitare ogni eredità". Se gli oggetti hanno una relazione logica e una B può essere utilizzata ovunque sia prevista una A, è buona norma utilizzare l'ereditarietà. Tuttavia, se gli oggetti hanno solo campi con lo stesso nome e non hanno relazioni di dominio, non utilizzare l'ereditarietà.

Abbastanza spesso, vale la pena porre la domanda di "ragione del cambiamento" nel processo di progettazione: se la classe A ottiene un campo in più, ti aspetteresti che anche la classe B lo ottenga? Se questo è vero, gli oggetti condividono una relazione e l'ereditarietà è una buona idea. In caso contrario, subisci la piccola ripetizione per mantenere concetti distinti in un codice distinto.


Certo, la ragione del cambiamento è un buon punto, ma cosa succede nella situazione, dove queste classi sono e saranno sempre costanti?
user969153,

20
@ user969153: quando le classi saranno veramente costanti, non importa. Tutta la buona ingegneria del software è per il futuro manutentore, che deve apportare modifiche. Se non ci saranno cambiamenti futuri, tutto andrà bene, ma fidati di me, avrai sempre delle modifiche a meno che non programmi per il cestino.
thiton

@ user969153: Detto questo, la "ragione del cambiamento" è euristica. Ti aiuta a decidere, niente di più.
thiton

2
Buona risposta. I motivi per cambiare è S nell'acronimo SOLID di zio bob che aiuterà a progettare un buon software.
Klee,

4
@ user969153, Nella mia esperienza, "dove sono queste classi e saranno sempre costanti?" non è un caso d'uso valido :) Devo ancora lavorare su un'applicazione di dimensioni significative che non ha subito cambiamenti inaspettati.
cdkScegli il

18

Usando l'ereditarietà, finirai con qualcosa del genere, ma i nomi delle classi perderanno il significato contestuale (a causa di is-a, non has-a):

Esattamente. Guarda questo, fuori dal contesto:

Class D : C
{
    String age;
}

Quali proprietà ha una D? Puoi ricordare? E se complicassi ulteriormente la gerarchia:

Class E : B
{
    int numberOfWheels;
}

Class F : E
{
    String favouriteColour;
}

E poi se, orrore sugli orrori, vuoi aggiungere un campo imageUrl a F ma non E? Non puoi derivarlo da C ed E.

Ho visto una situazione reale in cui molti tipi di componenti diversi avevano tutti un nome, un ID e una descrizione. Ma questo non era per natura del loro essere componenti, era solo una coincidenza. Ognuno aveva un ID perché erano memorizzati in un database. Il nome e la descrizione erano per la visualizzazione.

I prodotti avevano anche un ID, nome e descrizione, per motivi simili. Ma non erano componenti, né componenti prodotti. Non vedresti mai le due cose insieme.

Ma alcuni sviluppatori hanno letto di DRY e hanno deciso che avrebbe rimosso ogni accenno di duplicazione dal codice. Quindi ha definito tutto un prodotto e ha eliminato la necessità di definire quei campi. Sono stati definiti in Prodotto e tutto ciò che necessitava di quei campi potrebbe derivare dal Prodotto.

Non posso dirlo abbastanza forte: questa non è una buona idea.

A SECCO non si tratta di rimuovere la necessità di proprietà comuni. Se un oggetto non è un prodotto, non chiamarlo un prodotto. Se un prodotto NON RICHIEDE una descrizione, per la natura stessa del suo essere un prodotto, non definire la descrizione come proprietà del prodotto.

Alla fine è successo che avevamo Componenti senza descrizioni. Quindi abbiamo iniziato ad avere descrizioni nulle in quegli oggetti. Non nullable. Nullo. Sempre. Per qualsiasi prodotto di tipo X ... o successivo Y ... e N.

Questa è una grave violazione del principio di sostituzione di Liskov.

DRY significa non metterti mai in una posizione in cui potresti correggere un bug in un posto e dimenticare di farlo altrove. Questo non accadrà perché hai due oggetti con la stessa proprietà. Mai. Quindi non preoccuparti.

Se il contesto delle classi rende ovvio che dovresti, il che sarà molto raro, allora e solo allora dovresti considerare una classe genitore comune. Ma, se si tratta di proprietà, considera perché non stai utilizzando un'interfaccia.


4

L'ereditarietà è un modo per ottenere un comportamento polimorfico. Se le tue lezioni non hanno alcun comportamento, l'eredità è inutile. In questo caso, non li considererei nemmeno oggetti / classi, ma strutture.

Se vuoi essere in grado di mettere strutture diverse in diverse parti del tuo comportamento, considera le interfacce con metodi o proprietà get / set, come IHasName, IHasAge, ecc. Ciò renderà il design molto più pulito e consentirà una migliore composizione di questi tipo di hiearchies.


3

Non sono queste le interfacce? Classi non correlate con funzioni diverse ma con proprietà simili.

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;

1

La tua domanda sembra essere inquadrata nel contesto di una lingua che non supporta l'ereditarietà multipla ma non specifica questo. Se stai lavorando con un linguaggio che supporta l'ereditarietà multipla, questo diventa banalmente facile da risolvere con un solo livello di eredità.

Ecco un esempio di come risolvere questo problema utilizzando l'ereditarietà multipla.

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;
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.