Perché l'ereditarietà e il polimorfismo sono così diffusi?

18

Più apprendo su diversi paradigmi di programmazione, come la programmazione funzionale, più inizio a mettere in discussione la saggezza dei concetti OOP come l'ereditarietà e il polimorfismo. Ho imparato per la prima volta l'ereditarietà e il polimorfismo a scuola, e al momento il polimorfismo sembrava un modo meraviglioso per scrivere codice generico che consentisse una facile estensibilità.

Ma di fronte alla digitazione anatra (sia dinamica che statica) e alle funzioni funzionali come le funzioni di ordine superiore, ho iniziato a considerare l'ereditarietà e il polimorfismo come l'imposizione di una restrizione non necessaria basata su un fragile insieme di relazioni tra oggetti . L'idea generale dietro al polimorfismo è che scrivi una funzione una volta, e dopo puoi aggiungere nuove funzionalità al tuo programma senza modificare la funzione originale: tutto ciò che devi fare è creare un'altra classe derivata che implementa i metodi necessari.

Ma questo è molto più semplice da ottenere attraverso la digitazione anatra, sia che si tratti di un linguaggio dinamico come Python, sia di un linguaggio statico come il C ++.

Ad esempio, considera la seguente funzione Python, seguita dal suo equivalente C ++ statico:

def foo(obj):
   obj.doSomething()

template <class Obj>
void foo(Obj& obj)
{
   obj.doSomething();
}

L'equivalente OOP sarebbe simile al seguente codice Java:

public void foo(DoSomethingable obj)
{
  obj.doSomething();
}

La principale differenza, ovviamente, è che la versione Java richiede la creazione di un'interfaccia o di una gerarchia di ereditarietà prima che funzioni. La versione Java comporta quindi più lavoro ed è meno flessibile. Inoltre, trovo che la maggior parte delle gerarchie di ereditarietà del mondo reale siano alquanto instabili. Abbiamo visto tutti gli esempi inventati di Shapes and Animals, ma nel mondo reale, quando cambiano le esigenze del business e vengono aggiunte nuove funzionalità, è difficile eseguire qualsiasi lavoro prima che sia necessario allungare la relazione "is-a" tra sottoclassi, oppure rimodellare / refactare la propria gerarchia per includere ulteriori classi base o interfacce al fine di soddisfare nuovi requisiti. Con la digitazione anatra, non devi preoccuparti di modellare nulla: ti preoccupi solo della funzionalità di cui hai bisogno.

Tuttavia, eredità e polimorfismo sono così popolari che dubito che sarebbe molto esagerato definirli la strategia dominante per l'estensibilità e il riutilizzo del codice. Allora perché ereditari e polimorfismi hanno così successo? Sto trascurando alcuni seri vantaggi che l'ereditarietà e il polimorfismo hanno una digitazione eccessiva?

    
posta Channel72 14.04.2011 - 21:45
fonte

10 risposte

22

Sono per lo più d'accordo con te, ma per gioco giocherò con Devil's Advocate. Le interfacce esplicite offrono un posto unico in cui cercare un contratto specifico esplicitamente, formalmente , che ti dice che cosa si suppone che un tipo faccia. Questo può essere importante quando non sei l'unico sviluppatore di un progetto.

Inoltre, queste interfacce esplicite possono essere implementate in modo più efficiente rispetto alla digitazione anatra. Una chiamata di funzione virtuale ha a malapena più di una normale chiamata di funzione, tranne che non può essere inline. La tipizzazione di anatra ha un sovraccarico sostanziale. La tipizzazione strutturale in stile C ++ (con i modelli) può generare enormi quantità di file oggetto gonfiati (poiché ogni istanza è indipendente a livello di file oggetto) e non funziona quando è necessario il polimorfismo in fase di runtime, non il tempo di compilazione.

Conclusione: concordo sul fatto che l'ereditarietà e il polimorfismo in stile Java possano essere una PITA e che le alternative dovrebbero essere utilizzate più spesso, ma ha ancora i suoi vantaggi.

    
risposta data 14.04.2011 - 21:52
fonte
29

L'ereditarietà e il polimorfismo sono ampiamente usati perché funzionano , per determinati tipi di problemi di programmazione.

Non è che siano ampiamente insegnati nelle scuole, questo è indietro: sono ampiamente insegnati nelle scuole perché le persone (ovvero il mercato) hanno scoperto che hanno lavorato meglio dei vecchi strumenti, e così le scuole hanno iniziato ad insegnarli. [Aneddoto: quando ho iniziato a studiare OOP per la prima volta, è stato estremamente difficile trovare un college che insegnasse un linguaggio OOP. Dieci anni dopo, era difficile trovare un college che non insegnasse una lingua OOP.]

Hai detto:

The general idea behind polymorphism is that you write a function once, and later you can add new functionality to your program without changing the original function - all you need to do is create another derived class which implements the necessary methods

Dico:

No, non è

Ciò che descrivi non è il polimorfismo, ma l'ereditarietà. Non c'è da stupirsi che tu abbia problemi di OOP! ; -)

Eseguire il backup di un passaggio: il polimorfismo è un vantaggio del passaggio di messaggi; significa semplicemente che ogni oggetto è libero di rispondere a un messaggio a modo suo.

Quindi ... Duck Typing è (o meglio, attiva) il polimorfismo

L'essenza della tua domanda sembra essere che non capisci OOP o che non ti piace, ma che non ti piace definire le interfacce . Va bene, e finché stai attento le cose andranno bene. Lo svantaggio è che se hai fatto un errore, ad esempio omesso un metodo, non lo scoprirai fino all'ora di esecuzione.

Questa è una cosa statica vs dinamica, che è una discussione vecchia come Lisp, e non limitata a OOP.

    
risposta data 14.04.2011 - 23:31
fonte
8

I've begun to look at inheritance and polymorphism as imposing an unnecessary restriction based on a fragile set of relationships between objects.

Perché?

L'ereditarietà (con o senza digitazione anatra) assicura il riutilizzo di una caratteristica comune. Se è comune, puoi assicurarti che venga riutilizzato in modo coerente nelle sottoclassi.

Questo è tutto. Non ci sono "restrizioni inutili". È una semplificazione.

Il polimorfismo, analogamente, è ciò che significa "digitazione anatra". Stessi metodi Un sacco di classi con un'interfaccia identica, ma diverse implementazioni.

Non è una restrizione. È una semplificazione.

    
risposta data 14.04.2011 - 21:59
fonte
7

L'ereditarietà è abusata, ma lo è anche la digitazione anatra. Entrambi possono portare a problemi.

Con una strong digitazione si ottengono molti "test unitari" fatti in fase di compilazione. Con la digitazione anatra spesso devi scriverli.

    
risposta data 14.04.2011 - 22:00
fonte
2

Il bello dell'apprendimento delle cose a scuola è che tu fai le impari. La cosa meno buona è che puoi accettarle un po 'troppo dogmaticamente, senza capire quando sono utili e quando no.

Quindi, se lo hai imparato dogmaticamente, puoi in seguito ribellarti altrettanto dogmaticamente nella direzione opposta. Neanche questo va bene.

Come per qualsiasi idea del genere, è meglio adottare un approccio pragmatico. Sviluppa una comprensione di dove si adattano e dove no. E ignora tutti i modi in cui sono stati ipervenduti.

    
risposta data 14.04.2011 - 22:19
fonte
2

Sì, la tipizzazione statica e le interfacce sono restrizioni. Ma tutto è stato inventato dal momento che la programmazione strutturata è stata inventata (vale a dire "goto considerato dannoso"). Lo zio Bob ha una visione eccellente di questo in il suo blog di video .

Ora, si può sostenere che la costrizione è cattiva, ma d'altra parte porta ordine, controllo e familiarità a un argomento altrimenti molto complesso.

Limitare i vincoli mediante (ri) introdurre la digitazione dinamica e persino accedere direttamente alla memoria è un concetto molto potente, ma può anche rendere più difficile per molti programmatori affrontare. Soprattutto i programmatori erano soliti affidarsi al compilatore e digitare la sicurezza per gran parte del loro lavoro.

    
risposta data 14.04.2011 - 22:22
fonte
1

L'ereditarietà è una relazione molto strong tra due classi. Non credo che Java sia più strong. Pertanto, dovresti usarlo solo quando lo intendi. L'ereditarietà pubblica è una relazione "è-a", non un "di solito è-a". È davvero, davvero facile abusare dell'eredità e finire con un casino. In molti casi, l'ereditarietà viene utilizzata per rappresentare "ha-a" o "prende-funzionalità-da-a", e in genere è meglio fatto per composizione.

Il polimorfismo è una conseguenza diretta della relazione "è-a". Se Derivato eredita dalla Base, allora ogni Base "è-a" Derivata, e quindi puoi usare una Derivata ovunque tu usi una Base. Se questo non ha senso in una gerarchia di ereditarietà, la gerarchia è sbagliata e probabilmente c'è troppa eredità in corso.

La tipizzazione di anatra è una funzione accurata, ma il compilatore non ti avviserà se lo utilizzerai in modo improprio. Se non si desidera gestire eccezioni in fase di esecuzione, è necessario assicurarsi che si ottengano sempre i risultati corretti. Potrebbe essere più semplice definire una gerarchia di ereditarietà statica.

Non sono un vero fan della tipizzazione statica (lo considero spesso una forma di ottimizzazione prematura), ma elimina alcune classi di errori e molte persone pensano che valga la pena eliminarle.

Se ti piace digitazione dinamica e digitazione anatra meglio della tipizzazione statica e delle gerarchie di ereditarietà definite, va bene. Tuttavia, il modo Java ha i suoi vantaggi.

    
risposta data 14.04.2011 - 23:10
fonte
0

Ho notato che più utilizzo le chiusure di C #, meno sto facendo OOP tradizionale. L'ereditarietà era l'unico modo per condividere facilmente l'implementazione, quindi penso che fosse spesso sovrautilizzato e che i limiti del concepimento fossero spinti troppo lontano.

Mentre puoi solitamente usi chiusure per fare la maggior parte di ciò che faresti con l'ereditarietà, può anche diventare brutto.

Fondamentalmente si tratta di uno strumento giusto per la situazione lavorativa: l'OOP tradizionale può funzionare molto bene se si dispone di un modello adatto e le chiusure possono funzionare molto bene quando non lo si fa.

    
risposta data 15.04.2011 - 02:27
fonte
-1

La verità sta da qualche parte nel mezzo. Mi piace il modo in cui C # 4.0 è un linguaggio tipizzato staticamente e supporta la "digitazione anatra" con la parola chiave "dinamica".

    
risposta data 15.04.2011 - 00:13
fonte
-1

L'ereditarietà, anche quando la ragione da una prospettiva FP, è un grande concetto; non solo risparmia molto tempo, ma dà un senso alla relazione tra certi oggetti del tuo programma.

public class Animal {
    public virtual string Sound () {
        return "Some Sound";
    }
}

public class Dog : Animal {
    public override string Sound () {
        return "Woof";
    }
}

public class Cat : Animal {
    public override string Sound () {
        return "Mew";
    }
}

public class GoldenRetriever : Dog {

}

Qui la classe GoldenRetriever ha lo stesso Sound di Dog per i ringraziamenti gratuiti all'ereditarietà.

Scriverò lo stesso esempio con il mio livello di Haskell affinché tu possa vedere la differenza

data Animal = Animal | Dog | Cat | GoldenRetriever

sound :: Animal -> String
sound Animal = "Some Sound"
sound Dog = "Woof"
sound Cat = "Mew"
sound GoldenRetriever = "Woof"

Qui non ti sfugge dover specificare sound per GoldenRetriever . La cosa più semplice in generale sarebbe quella di

sound GoldenRetriever = sound Dog

ma solo imagen se hai 20 funzioni! Se c'è un esperto Haskell per favore mostraci un modo più semplice.

Detto questo, sarebbe bello avere la corrispondenza dei pattern e l'ereditarietà allo stesso tempo, in cui una funzione sarebbe predefinita per la classe base se l'istanza corrente non ha l'implementazione.

    
risposta data 11.08.2014 - 22:00
fonte

Leggi altre domande sui tag