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


18

Più imparo sui diversi paradigmi di programmazione, come la programmazione funzionale, più comincio a mettere in discussione la saggezza dei concetti di OOP come eredità e polimorfismo. Ho imparato a conoscere l'eredità e il polimorfismo a scuola, e all'epoca il polimorfismo sembrava un modo meraviglioso di scrivere un codice generico che permettesse una facile estensibilità.

Ma di fronte alla tipizzazione anatra (sia dinamica che statica) e caratteristiche 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 alla base del polimorfismo è che scrivi una funzione una volta, e successivamente puoi aggiungere nuove funzionalità al tuo programma senza cambiare 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 tipizzazione duck, sia che si tratti di un linguaggio dinamico come Python, sia di un linguaggio statico come 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 differenza principale, ovviamente, è che la versione Java richiede la creazione di un'interfaccia o di una gerarchia ereditaria prima che funzioni. La versione Java quindi richiede più lavoro ed è meno flessibile. Inoltre, trovo che la maggior parte delle gerarchie ereditarie del mondo reale siano alquanto instabili. Abbiamo visto tutti gli esempi inventati di forme e animali, ma nel mondo reale, poiché i requisiti aziendali cambiano e vengono aggiunte nuove funzionalità, è difficile svolgere qualsiasi lavoro prima di dover veramente allungare la relazione "is-a" tra sottoclassi, oppure rimodellare / riformattare la gerarchia per includere ulteriori classi base o interfacce al fine di soddisfare i nuovi requisiti. Con la tipizzazione anatra, non devi preoccuparti di modellare nulla: ti preoccupi solo della funzionalità di cui hai bisogno.

Tuttavia, ereditarietà e polimorfismo sono così popolari che dubito che sarebbe esagerato chiamarli la strategia dominante per estensibilità e riutilizzo del codice. Allora perché l'ereditarietà e il polimorfismo hanno così tanto successo? Sto trascurando alcuni seri vantaggi che l'ereditarietà / il polimorfismo hanno sulla dattilografia?


Non sono un esperto di Python, quindi devo chiedere: cosa succederebbe se objnon avesse un doSomethingmetodo? Viene sollevata un'eccezione? Non succede niente?
FrustratedWithFormsDesigner

6
@Frustrated: viene sollevata un'eccezione molto chiara e specifica.
dsimcha,

11
La tipizzazione delle anatre non è solo polimorfismo portato fino alle undici? La mia ipotesi è che non sei frustrato con OOP ma con le sue incarnazioni tipicamente statiche. Posso capirlo, davvero, ma ciò non rende OOP nel suo insieme una cattiva idea.

3
Prima leggi "ampiamente" come "selvaggiamente" ...

Risposte:


22

Sono principalmente d'accordo con te, ma per divertimento giocherò a Devil's Advocate. Le interfacce esplicite offrono un unico posto per cercare un contratto esplicitamente, formalmente specificato, che ti dice cosa dovrebbe fare un tipo. 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 tipizzazione duck. Una chiamata di funzione virtuale ha appena più sovraccarico di una normale chiamata di funzione, tranne per il fatto che non può essere integrata. La digitazione dell'anatra ha un notevole sovraccarico. La tipizzazione strutturale in stile C ++ (utilizzando i modelli) può generare enormi quantità di gonfiore del file oggetto (poiché ogni istanza è indipendente a livello del file oggetto) e non funziona quando è necessario il polimorfismo in fase di esecuzione, non in fase di compilazione.

In conclusione: sono d'accordo sul fatto che l'ereditarietà e il polimorfismo in stile Java possano essere una PITA e che le alternative debbano essere usate più spesso, ma ha ancora i suoi vantaggi.


29

Ereditarietà e polimorfismo sono ampiamente utilizzati perché funzionano , per alcuni tipi di problemi di programmazione.

Non è che siano ampiamente insegnati nelle scuole, questo è all'indietro: sono ampiamente insegnati nelle scuole perché le persone (alias il mercato) hanno scoperto di lavorare meglio dei vecchi strumenti, e quindi le scuole hanno iniziato a insegnare loro. [Aneddoto: quando stavo imparando OOP per la prima volta, era estremamente difficile trovare un college che insegnasse qualsiasi lingua OOP. Dieci anni dopo, è stato difficile trovare un college che non insegnasse una lingua OOP.]

Tu hai detto:

L'idea generale alla base del polimorfismo è che scrivi una funzione una volta, e successivamente puoi aggiungere nuove funzionalità al tuo programma senza cambiare la funzione originale - tutto ciò che devi fare è creare un'altra classe derivata che implementa i metodi necessari

Dico:

No, non lo è

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

Fai un passo indietro: 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 abilita) il polimorfismo

L'essenza della tua domanda sembra non essere che non capisci OOP o che non ti piaccia , ma che non ti piace definire le interfacce . Va bene, e finché stai attento le cose funzioneranno bene. Lo svantaggio è che se hai commesso un errore, ad esempio un metodo omesso, non lo scoprirai fino al runtime.

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


2
+1 per "scrivere l'anatra è polimorfismo". Il polimorfismo e l'eredità sono stati inconsciamente incatenati insieme per troppo tempo e la tipizzazione statica rigorosa è l'unica vera ragione per cui siano mai stati.
cHao,

+1. Penso che la distinzione statica vs dinamica dovrebbe essere più di una nota a margine: è al centro della distinzione tra tipizzazione delle anatre e polimorfismo in stile java.
Sava B.

8

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.

Perché?

L'ereditarietà (con o senza la tipizzazione delle anatre) assicura il riutilizzo di una caratteristica comune. Se è comune, puoi assicurarti che viene riutilizzato in modo coerente nelle sottoclassi.

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

Il polimorfismo, allo stesso modo, è ciò che significa "scrivere l'anatra". Stessi metodi. Molte classi con un'interfaccia identica, ma implementazioni diverse.

Non è una restrizione. È una semplificazione.


7

L'ereditarietà viene abusata, ma lo è anche la dattilografia. Entrambi possono causare problemi.

Con una digitazione forte si ottengono molti "test unitari" eseguiti in fase di compilazione. Con la tipizzazione delle anatre devi spesso scriverle.


3
Il tuo commento sui test si applica solo alla digitazione dinamica delle anatre. La digitazione anatra statica in stile C ++ (con modelli) ti dà errori in fase di compilazione. (Sebbene, gli errori del modello C ++ concessi siano piuttosto difficili da decifrare, ma questo è un problema completamente diverso.)
Channel72

2

La cosa buona di imparare cose a scuola è che le impari. La cosa non così buona è che potresti accettarli un po 'troppo dogmaticamente, senza capire quando sono utili e quando no.

Quindi, se l'hai imparato dogmaticamente, in seguito potresti "ribellarti" altrettanto dogmaticamente nell'altra direzione. Neanche questo va bene.

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


2

Sì, la digitazione statica e le interfacce sono restrizioni. Ma tutto da quando è stata inventata la programmazione strutturata (ovvero "considerato dannoso") è stato per vincolarci. Zio Bob ha una visione eccellente di questo nel suo video blog .

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

Rilasciare i vincoli introducendo (ri) la digitazione dinamica e persino l'accesso diretto alla memoria è un concetto molto potente, ma può anche essere più difficile da gestire per molti programmatori. Soprattutto i programmatori erano soliti fare affidamento sul compilatore e digitare la sicurezza per gran parte del loro lavoro.


1

L'ereditarietà è una relazione molto forte tra due classi. Non penso che Java abbia qualcosa di più forte. Pertanto, dovresti usarlo solo quando lo intendi. L'eredità pubblica è una relazione "is-a", non un "solitamente is-a". È davvero, davvero facile abusare dell'eredità e finire con un casino. In molti casi, l'ereditarietà viene utilizzata per rappresentare "has-a" o "prende-funzionalità-da-a", e in genere è meglio farlo dalla composizione.

Il polimorfismo è una conseguenza diretta della relazione "is-a". Se Derivata eredita da Base, allora ogni "è-a" Base derivata, e quindi è possibile utilizzare una Derivata ovunque si utilizzi una Base. Se questo non ha senso in una gerarchia ereditaria, allora la gerarchia è sbagliata e probabilmente c'è troppa eredità in corso.

La digitazione delle anatre è una funzionalità accurata, ma il compilatore non ti avviserà se la utilizzerai in modo improprio. Se non si desidera gestire le eccezioni in fase di esecuzione, è necessario assicurarsi che si ottengano sempre i risultati giusti. Potrebbe essere più semplice definire una gerarchia di ereditarietà statica.

Non sono un vero fan della tipizzazione statica (la 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 la digitazione dinamica e la digitazione anatra meglio della tipizzazione statica e delle gerarchie ereditarie definite, va bene. Tuttavia, il modo Java ha i suoi vantaggi.


0

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

Mentre di solito puoi usare le chiusure per fare la maggior parte di ciò che faresti con l'eredità, può anche diventare brutto.

Fondamentalmente è una situazione giusta per il lavoro: la tradizionale OOP può funzionare davvero bene quando si dispone di un modello adatto e le chiusure possono funzionare davvero bene quando non lo si fa.


-1

La verità sta da qualche parte nel mezzo. Mi piace come C # 4.0 sia il linguaggio tipizzato staticamente che supporti la "digitazione anatra" con la parola chiave "dinamica".


-1

L'ereditarietà, anche quando la ragione dal punto di vista del FP, è un grande concetto; non solo fa risparmiare molto tempo, ma dà significato alla relazione tra determinati oggetti nel 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 GoldenRetrieverha la stessa Soundcome Dogper gratuiti di Grazie a eredità.

Scriverò lo stesso esempio con il mio livello di Haskell per farti 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 si evita di dover specificare soundper GoldenRetriever. La cosa più semplice in generale sarebbe

sound GoldenRetriever = sound Dog

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

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


5
Lo giuro, Animalè l'anti-tutorial di OOP.
Telastyn,

@Telastyn ho imparato l'esempio da Derek Banas su Youtube. Grande insegnante. Secondo me è molto facile da visualizzare e ragionare. Sei libero di modificare il post con un esempio migliore.
Cristian Garcia,

questo non sembra aggiungere nulla di sostanziale rispetto alle precedenti 10 risposte
moscerino

@gnat Mentre la "sostanza" è già là fuori, una persona nuova all'argomento potrebbe trovare un esempio più facile da comprendere.
Cristian Garcia,

2
Animali e forme sono davvero cattive applicazioni del polimorfismo per diverse ragioni che sono state discusse fino alla nausea in questo sito. Fondamentalmente, dire che un gatto "è un" animale non ha senso, dal momento che non esiste un "animale" concreto. Ciò che intendi veramente modellare è il comportamento, non l'identità. Definire un'interfaccia "CanSpeak" che potrebbe essere implementata come un miagolio o una corteccia ha senso. Definire un'interfaccia "HasArea" che un quadrato e un cerchio possono implementare ha senso, ma non una forma generica.
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.