L'ereditarietà limita ciò che possiamo fare con la generalizzazione?

4

Come regola generale, la generalizzazione viene utilizzata solo in circostanze specifiche. Ad esempio, quando possiamo dire che X è letteralmente una sottoclasse di Y. Quindi, possiamo tranquillamente dire che un cavallo è una sottoclasse di mammiferi. Sono sempre stato portato a credere che dovremmo usare la generalizzazione e l'ereditarietà qui. Se non abbiamo questa stretta corrispondenza tra due oggetti, non dovremmo.

Un cavallo è un mammifero. Tuttavia, è anche letteralmente una modalità di trasporto. Quindi cosa succede in un mondo di eredità singola in cui il cavallo non può ereditare da entrambi? Sottoponiamo quindi il fatto che un cavallo è letteralmente una modalità di trasporto per la realizzazione, cioè un'interfaccia come IT trasferibile?

    
posta CarneyCode 09.04.2011 - 15:19
fonte

9 risposte

9

La chiave qui è che l'ereditarietà (singola) è una risorsa scarsa. Hai solo una superclasse. Considerare il tuo dominio ti aiuterà a prendere la decisione su come allocarlo al meglio. Stai scrivendo un programma di tassonomia? Allora probabilmente vorrai sottoclasse Mammal. Stai scrivendo un programma di trasporto? Quindi probabilmente vorrai sottoclasse Veicolo (o qualcosa del genere).

Solo perché un cavallo è sia un tipo di mammifero che un tipo di dispositivo di trasporto non significa che entrambi i concetti siano ugualmente validi nel tuo dominio.

Considera anche la massima "favorisci la composizione sull'eredità" dal libro GoF. Probabilmente la cosa migliore da uscire da quel libro.

    
risposta data 09.04.2011 - 23:01
fonte
6

Pensa a cosa significa in realtà "un mammifero". Da google:

A warm-blooded vertebrate animal of a class that is distinguished by the possession of hair or fur, the secretion of milk by females, and (typically) the birth of live young

Quindi, nel contesto della programmazione, potrebbe avere un metodo GrowHair (per ora dimentichiamo il sistema riproduttivo, è troppo complesso). Ma c'è molto poco nell'implementazione di GrowHair che è comune tra diversi tipi di Mammal.

La classe Universe potrebbe chiamare GrowHair su ogni Mammifero su un ciclo di timer, ma non deve preoccuparsi dell'implementazione, i diversi modi in cui Umani e Cani crescono capelli. Lascia che le classi Human e Dog si occupino di questo. Vuole solo sapere che ogni Mammifero ha la proprietà della crescita dei capelli, che può essere attivata attraverso il metodo GrowHair.

All'improvviso, questa è un'interfaccia (un contratto tra il Mammifero e l'Universo che il Mammifero eseguirà determinate funzioni, indipendentemente dall'implementazione) piuttosto che una classe base di per sé.

Questa è la direzione in cui OO si è spostato. L'ereditarietà multipla ha causato molti problemi e, in quasi tutti i casi, tutte le classi base tranne una potrebbero esistere senza implementazione (specialmente dove seguiamo l'approccio "preferisci la composizione sull'ereditarietà"). E così le interfacce sono nate come una sostituzione.

    
risposta data 09.04.2011 - 16:30
fonte
5

In parole semplici: le interfacce sono per contratti, classi per implementazioni.
L'ereditarietà dell'interfaccia è per le gerarchie di tipi, l'ereditarietà della classe (e la composizione) è per il riutilizzo del codice.

In generale, dovresti usare le interfacce per esprimere le relazioni di tipo.

    
risposta data 09.04.2011 - 15:47
fonte
1

Molteplici: le relazioni causano problemi con l'ereditarietà singola, ma in molti casi la relazione non è così difficile da risolvere. Ad esempio, utilizzerei un'interfaccia ITransportation, perché le diverse forme di trasporto non si comportano in nulla allo stesso modo. Lo farei anche se fosse disponibile un'eredità multipla; non esiste una classe di "forma di trasporto" nel mondo reale, è naturalmente un'interfaccia.

La maggior parte delle volte direi che l'ereditarietà multipla è un segnale che i tuoi oggetti stanno facendo troppo, ma è qualcosa che deve essere pensato caso per caso. È fondamentale evitare di utilizzare l'ereditarietà multipla per trascinare un sacco di comportamenti in una classe che è poi responsabile di troppe cose nella progettazione.

Se vuoi davvero che una classe esegua un'ereditarietà multipla nel mondo dell'ereditarietà, hai ancora la possibilità di avere la classe implementare le interfacce e delegare l'implementazione a un oggetto interno.

    
risposta data 09.04.2011 - 15:38
fonte
1

Parliamo di un concetto più generale: similarità.

Dire che B è una sottoclasse di A significa che B è simile a A (ma non che A è simile a B - quindi la somiglianza nel contesto della sottotipizzazione è non simmetrica).

Se B è simile a A allora ovunque posso usare A Posso usare B .

L'ereditarietà multipla significa che B è simile a A e B è simile a C . Questo non è un problema finché A e C non sono dissimili a vicenda. Se sono dissimili, in pratica sto dicendo che B è vicino a due cose che non sono vicine l'una all'altra, una contraddizione.

Quindi, affinché funzioni l'ereditarietà multipla, A e C devono essere uguali tra loro o devono essere mutuamente ortogonali (cioè vivono in spazi diversi non hanno nulla a che fare l'uno con l'altro).

Bene, sappiamo che non possono essere reciprocamente simili perché la sottotipizzazione non è simmetrica ( A e C non può ereditare l'uno dall'altro).

Ciò significa che devono essere reciprocamente ortogonali, il che significa che non hanno spazio in comune. Ma questo è fragile perché anche se iniziano come ortogonali, questo si interromperà se ognuno di loro ottiene un metodo con lo stesso nome e firma (cioè finiscono nello stesso spazio nominale - cioè lo spazio dei nomi). Può anche rompersi se a loro volta, a un certo punto, finiscono per ereditare dalla stessa classe base (il problema dei diamanti).

Quindi è, in effetti, l'ereditarietà singola che vale nel caso generale.

    
risposta data 09.04.2011 - 20:23
fonte
1

Getterò il mio contributo anche se la domanda è già stata risolta abbastanza bene. Il fatto è che i programmatori tendono a pensare all'architettura in maniera tassonomica piuttosto che architettonicamente. Vale a dire, i programmatori tenderanno a cercare classificazioni di oggetti piuttosto che strutture di oggetti. Questo può probabilmente essere attribuito alla vita reale: quando incontri una persona, le pensi come

  1. Una donna
  2. Una donna bianca
  3. Una bella donna bianca
  4. Una bella donna bianca in un bel vestito.

Questo si innesca in circa un secondo quando il nostro cervello elabora la nostra visione della donna. Si finirebbe con una gerarchia di classi:

class Woman {}
class WhiteWoman extends Woman {}
class PrettyWhiteWoman extends WhiteWoman {}
class NicelyDressedPrettyWhiteWoman extends PrettyWhiteWoman {}

Ora, è abbastanza ovvio che questa architettura è molto ingenua, ma probabilmente commettiamo questi errori di progettazione laddove la gerarchia non è così facilmente definita.

Per scrivere più codice riutilizzabile, dobbiamo combattere il desiderio del nostro cervello di classificare e invece tentare di comporre. "Composizione sull'ereditarietà" come si dice.

Nel tuo esempio, stai pensando che un cavallo "è un" mammifero, e un cavallo "è un" mezzo di trasporto (cioè un'estensione). In effetti, è più corretto pensare che un cavallo "abbia una" biologia dei mammiferi e un cavallo "abbia una capacità di agire come un trasporto. Pertanto, un cavallo dovrebbe implementare i contratti di interfaccia Mammalable e Transporter .

Ora arriviamo a un punto di contesa su come alcune gerarchie dovrebbero essere strutturate. Ad esempio, diciamo che nella nostra farm abbiamo un sacco di FourLeggedAnimal s, ognuno dei quali presenta un comportamento identico ai loro fratelli a quattro zampe. Nella nostra fattoria, abbiamo anche onnivori. Anche gli onnivori mostrano comportamenti simili. Il problema è che alcuni animali non sono più onnivori di quelli a quattro zampe. Non ha senso che FourLeggedAnimal si estenda da Omnivore o viceversa. Un esempio di questa contesa è se abbiamo sia umani che maiali nella nostra fattoria.

Invece, dobbiamo ripensare la nostra architettura. Dovremmo pensare ai nostri oggetti dall'interno piuttosto che dall'alto in basso. Invece di pensare "ciò che definisce un cavallo", dovremmo pensare "ciò che compone un cavallo". Nel contesto della nostra fattoria, questa definizione di cavallo può avere più senso (pseudo-codice basato su php):

interface Mammal {
   function growHair();
}
interface Transporter {
   function loadUp(bulk);
   function moveOut(destination);
}
class FourLeggedAnimal {
   public function walk() {
      echo "Going for a walk";
   }
}
class Omnivore {
   public function eat(Food food) {
      echo "Enjoying some food";
   }
}
class Herbivore {
   public function eat(Meat food) {
      echo "Blech!";
   }
   public function eat(Veggies food) {
      echo "Nom nom nom";
   }
}

class Horse implements Mammal, Transporter {
   private FourLeggedAnimal;
   private Herbivore;
   //Mammal/Transporter methods omitted
   public function __construct(FourLeggedAnimal fla, Herbivore h) {
      this.FourLeggedAnimal = fla;
      this.Herbivore = h;
   }
   public function eat(Food food) {
      this.Herbivore.eat(food);
   }
   public function walk() {
      this.FourLeggedAnimal.walk();
   }
}

Questo esempio è molto ingenuo e potrebbe probabilmente usare qualche miglioramento, ma spero che il concetto sia lì. Poiché ci siamo allontanati dalla mentalità "è una", la nostra architettura è più SOLIDA. Le dipendenze dei cavalli sono invertite, non dobbiamo preoccuparci delle violazioni di sostituzione di Liskov, ecc.

L'unico lato negativo di questa architettura, ed è grande, è il codice duplicato. Dobbiamo davvero avere un intero metodo walk() per Horse solo per mostrare il suo comportamento FourLeggedAnimal ? Se riusciamo a trovare un modo per aggirare questo problema inerente, non ci fermeremo.

    
risposta data 21.07.2011 - 20:49
fonte
0

Mettere da parte i problemi con la progettazione dell'ereditarietà (che si applica sia all'ereditarietà dell'interfaccia sia all'IM "completo").

La risposta è 'no', cosa dovresti essere in grado di fare con MI, puoi fare con la composizione SI e Interface. La differenza è che SI + II richiede di scrivere molto più codice per fare la stessa cosa e gestire la gestione di questa duplicazione del codice.

Se volevi un cavallo che fosse sia un 'ITransporter' che un 'IStockKeepingUnit' allora MI ti avrebbe permesso di ereditare e di essere fatto con esso, SI richiede di creare una classe che implementa l'altra interfaccia e agganciare fino ai metodi di interfaccia che vengono implementati dalla tua classe di cavalli. Finché non impazzisci ereditando molti livelli in profondità e hai diverse classi che ereditano dalla stessa classe, allora MI è la scelta migliore.

Se devi implementare i tuoi metodi specifici per un'interfaccia e non puoi semplicemente ri-implementare i metodi della classe base, allora II è migliore (più semplice).

Ho lavorato con alcuni seri analisti in passato scrivendo sistemi di scala molto lareg. Hanno usato MI molto mentre decomposero i progetti del sistema, quindi posso vedere i vantaggi in più interfacce riutilizzate. Vedo anche la necessità di mantenere tali interfacce a un livello di profondità o di iniziare a diventare troppo complesso.

    
risposta data 21.07.2011 - 18:11
fonte
0

So what happens in a world of single inheritance where horse cannot inherit from both?

Si finisce con un sacco di codice rumoroso per aggirare la mancanza di supporto per l'ereditarietà multipla. Invece di

 class A { private: *data* ; 
           public: void doA() { ... } }
 class B { private: *data* ; 
           public: void doB1() { ... } 
                   void doB2() { ... };
         }

 public class MI: public A, public B { ... }

hai bisogno di:      classe pubblica A {...; }      interfaccia pubblica B {...; }      strumenti BImpl di classe pubblica B {...; }

 public class SI extends A implements B {
    private BImpl bImpl;
    public void doB1() { bImpl.doB1(); }
    public void doB2() { bImpl.doB2(); }
 }

Tutto questo problema per aggirare un possibile conflitto nei nomi dei metodi tra le due classi base.

A proposito, è un errore tentare di creare classi OO per imitare le categorie nel dominio. Creare classi OO solo quando necessario per implementare casi d'uso e utilizzare l'ereditarietà per migliorare la struttura del programma. Non mi preoccuperei di Animal and Transporter fino a quando non ho avuto anche una Cow and a Car, e un caso d'uso che richiedeva di allevare i cavalli e le mucche e di viaggiare a cavallo o in auto. A quel punto potrei desiderare una lingua che supporta l'ereditarietà multipla.

    
risposta data 22.07.2011 - 06:00
fonte
0

Sì e no.

Da un lato, non hai bisogno di classi - se la tua lingua è completa di Turing e ha tutte le capacità di I / O di cui hai bisogno, hai tutti gli strumenti di cui hai bisogno. Ma questa è un'interpretazione estrema del "bisogno".

D'altra parte, il supporto per l'ereditarietà multipla integrata nella lingua può semplificare e chiarire alcune attività. Potresti comunque ottenere gli stessi requisiti con un'unica ereditarietà, ma avresti bisogno di strutturare le cose in modo diverso, aggiungendo un po 'più di confusione.

Un problema simile si verifica con la spedizione singola rispetto alla spedizione multipla. Ero solito affermare che il dispiegamento multiplo rende ridondante il visitatore. Penso che probabilmente sia sbagliato ora perché il dispiegamento multiplo di per sé non separa le responsabilità per i ruoli visitatori e visitee, ma porta a una variante molto più semplice e probabilmente più flessibile del modello di visitatore. È abbastanza convincente che per le lingue che non supportano la distribuzione multipla (la maggior parte delle lingue OOP popolari), alcune persone hanno scritto linguaggi specifici per il dominio. Compreso me stesso; -)

Immagino che ci siano alcuni schemi di progettazione che potrebbero essere semplificati o eliminati con l'uso dell'ereditarietà multipla, ma non posso dirlo con certezza.

Come spesso accade, è un problema di equilibrio. Potrebbe sembrare più facile avere lo strumento perfetto per ogni lavoro, ma poi hai bisogno di un numero infinito di strumenti. Un kit di strumenti minimalista significa che hai più lavoro manuale da fare - ma più esperienza con gli strumenti specifici che hai a disposizione. I programmatori probabilmente spareranno e si bombarderanno a vicenda fino alla fine dei tempi su quanto dovrebbe essere grande il toolkit, e precisamente quali strumenti dovrebbero essere inclusi.

Un punto degno di nota - l'ereditarietà multipla porta con sé più complessità di quanto possa essere immediatamente ovvio. Il "modello di ereditarietà dei diamanti" viene spesso menzionato in questo contesto. Considera questo schema ...

    A
   / \
  /   \
 B     C
  \   /
   \ /
    D

D eredita una singola copia di A o due copie indipendenti di A ? Questa domanda può verificarsi solo perché D eredita sia B che C . In C ++, è una scelta - e indirettamente espressa. Se sia B che C ereditano A usando l'ereditarietà virtuale , D eredita una singola copia condivisa di A - altrimenti ci sono due copie indipendenti di A .

Ma per quanto riguarda questo caso ...

    A
   /|\
  / | \
 B  C  D
  \ | /
   \|/
    E

E se B e C utilizzano entrambi l'ereditarietà virtuale, ma D no?

Penso che la risposta corretta sia che E erediterà due copie di A - una condivisa da B e C , e una copia separata per D . Non in linea di principio, ma nel codice, le informazioni rilevanti sono sparse in giro.

    
risposta data 22.07.2011 - 09:09
fonte

Leggi altre domande sui tag