Limitazioni del polimorfismo in lingue tipizzate staticamente

5

Io programma principalmente in lingue tipizzate staticamente, come C ++ e Java. Una strategia comune impiegata in linguaggi come questi per gestire la gestione di collezioni di oggetti che sono correlati , ma che hanno bisogno di impiegare comportamenti specifici, è usare il polimorfismo e le gerarchie di ereditarietà. Ma il Polymorphism funziona solo quando la classe Base ha un metodo (funzione virtuale) che può essere sovrascritto da ogni classe derivata. Ma ci sono spesso casi in cui una classe derivata deve avere un metodo, e non ha senso posizionare un metodo con lo stesso nome nella classe Base, perché è molto specifico della classe derivata.

Consentitemi di dare un esempio concreto che probabilmente molti di noi potrebbero riguardare: il DOM HTML.

I programmatori Javascript sono abituati a fare cose come iterare su raccolte di nodi DOM. Danno per scontata la natura tipizzata in modo dinamico di Javascript quando fanno qualcosa del tipo:

node = document.getElementById("foo");
for (var i = 0; i < node.childNodes.length; ++i) 
    doSomething(node.childNodes[i]);

Qualcosa del genere sarebbe molto complicato da fare in un linguaggio tipizzato staticamente come Java, perché il polimorfismo basato sull'ereditarietà non si presta bene alle situazioni in cui le classi derivate hanno metodi che non esistono nella classe Base. Ad esempio, durante l'iterazione dei nodi HTML precedenti, potremmo trovare un nodo DIV, che ha determinati metodi, e un nodo di testo, che ha determinati metodi diversi, e un nodo TABLE, che ha i suoi metodi unici - e non sarebbe Ho molto senso che TUTTI questi vari metodi esistano in una classe base comune "Nodo", perché sono altamente specifici per ogni tipo derivato. (Ad esempio, un nodo DIV non ha bisogno di un metodo "celle", ma un nodo TABLE lo fa.)

Quindi, in un linguaggio tipizzato staticamente, dovresti ricorrere a qualcosa come Type Enums e un sacco di down-casting per verificare quale tipo di Node hai. Quindi in pratica si finisce con codice disordinato come:

Node node = document.getElementById("foo");
for (int i = 0; i < node.childNodes().length(); ++i) 
{
   Node n = node.childNodes().elementAt(i);
   if (n.type() == Node.DIV_NODE) doSomethingWithDiv(((DivNode)(n)));
   else if (n.type() == Node.TABLE_NODE) doSomethingWithTable(((TableNode)(n)));
   /* etc... */
}

Questo è simile a ciò che fanno i programmatori C, ed è apparentemente il tipo di cosa che le funzioni virtuali sono state progettate per evitare . Eppure, quando ci si trova in una situazione in cui ci sono vari metodi in una classe derivata per cui non ha alcun senso collocare in una classe base comune, non si può fare ricorso, ma ricorrere a qualcosa come Type Enums .

Quindi, in che modo questo problema viene solitamente affrontato in lingue tipizzate staticamente? Esiste un modello di progettazione migliore rispetto a Enum di tipo?

    
posta Channel72 05.08.2012 - 17:19
fonte

13 risposte

12

Questa non è una limitazione nei linguaggi tipizzati staticamente, esiste una soluzione a questo problema che consiste nel dichiarare un'interfaccia comune tra le varie classi per le operazioni che ti interessano. Ciò ti consente di fare tutto ciò che devi fare, pur continuando a digitare sicuro.

    
risposta data 05.08.2012 - 18:20
fonte
6

La soluzione migliore sarebbe probabilmente Modello visitatore .

E se hai bisogno di fare cose come questa, allora dovresti ripensare al tuo modello di oggetto, perché avere un comportamento, che funziona solo su un tipo specifico dovrebbe essere parte di quel tipo.

    
risposta data 05.08.2012 - 17:51
fonte
4

Ecco a cosa servono le interfacce . Non dovresti usare una classe base per questo. Le classi base segnalano una relazione del tipo A is a B ; le interfacce dicono A can do X .

Utilizzando l'esempio tradizionale degli animali: un Bee è un Insect , che è un Animal . Un Sparrow è un Bird , che è anche un Animal . Sia le api che i passeri possono volare, ma non tutti gli animali possono volare, e quel che è peggio, nemmeno tutti gli insetti e gli uccelli possono volare. Allora, dove va il metodo fly() ? %codice%? O dovremmo ristrutturare la nostra gerarchia di classi in modo che Animal e Bee abbiano un antenato comune? Ma se lo facciamo, non possiamo avere un antenato comune per Sparrow e Penguin che fornisce un becco senza fare volare i pinguini o equipaggiare le api con i becchi (disegna le gerarchie di classe su carta se non la ottieni) .

La soluzione è di avere un'interfaccia Sparrow , che fornisce la firma per FlyingAnimal ; ogni volta che hai bisogno di una collezione di animali volanti, dichiarala come fly() .

Utilizzando l'ereditarietà dell'interfaccia, puoi raggruppare le interfacce in "superinterfacce", ad es. puoi avere un Collection<FlyingAnimal> che eredita FlyingAndWalkingAnimal da fly() e FlyingAnimal da walk() .

    
risposta data 06.08.2012 - 08:20
fonte
4

Per me sembra che il tuo problema sia un difetto nella tua comprensione o un difetto nel tuo design. O entrambi. :)

Ciò che i tuoi tentativi di esempio fanno (e fa in JS) è di eseguire un metodo con lo stesso nome su ognuno di un gruppo di oggetti di tipi non correlati . Questo è uno di quegli hack che fai quando fai saltare in aria uno strumento rapido in qualche linguaggio di scripting, e ha il suo posto lì.

Ma le lingue statiche sono tipizzate staticamente in modo che il compilatore possa rilevare errori semplici come chiamare un metodo inesistente in fase di compilazione. Quando stai facendo lo sviluppo in un tale linguaggio, l'assunzione è naturalmente che intendi affari, che il tuo programma deve essere eseguito da alcuni clienti e che vuoi eliminare il maggior numero possibile di bug il prima possibile.

Una delle cose che richiede un linguaggio statico per poter tenere la mano è che la esprimi nel tuo progetto se un gruppo di tipi supporta lo stesso operazione. Lo fai derivandoli dalla stessa classe base o dall'interfaccia, che dichiara che questa operazione è disponibile in tutte le classi derivate . Se non lo fai, il tuo codice dovrà inventare numerosi hack per raggiungere lo stesso obiettivo. (Un uomo saggio una volta disse che un cambio di tipo - che è quello che il tuo if(n.type() == Node.SOME_TYPE) ... else if(n.type() == Node.SOME_OTHER_TYPE) ... equivale a dire - è solo una paura irragionevole della funzione virtuale.)

Quindi: Se quei tipi supportano la stessa operazione, e se vuoi eseguire quell'operazione su oggetti di uno di questi tipi senza preoccuparti veramente di quale tipo concreto sono " Di conseguenza, è necessario dichiarare tali operazioni disponibili in alcune classi di base comuni o interfacce . (Nel caso dell'angolo si ha solo un numero relativamente piccolo di tipi, ma un numero di operazioni non correlate, che ingrassare irragionevolmente un'interfaccia di una classe base, ci sono idiomi come il Pattern ospite che impiegano dispacci multipli per risolvere il problema.)

    
risposta data 14.08.2012 - 09:11
fonte
3

Il fatto che tu abbia bisogno di questo è un fallimento nel tuo progetto. Esistono sottotipi che fungono da raffinamento del tuo tipo di base. Se hai bisogno di comportamenti specifici, allora lavora con il tipo specifico.

Il modello migliore è quello di lasciare che la collezione fornisca la raccolta di tipi specifici di cui hai bisogno (selettori CSS / jquery).

    
risposta data 05.08.2012 - 17:52
fonte
2

C'è sempre un controllo dei tipi sia in fase di compilazione che in fase di esecuzione, ma non chiamarlo così. Ad esempio, il frammento JavaScript sembra molto impressionante, ma nasconde i dettagli su doSomething. Sì, può accettare qualsiasi oggetto e qualsiasi oggetto può avere un insieme di metodi molto specifico, ma questa funzione dovrebbe accedere a metodi e proprietà, quindi si presuppone che i metodi di cui ha bisogno siano lì, altrimenti ti darà un errore di runtime. Quindi, in precedenza, puoi filtrare gli oggetti per il passaggio in modo sicuro per DoSomething o lo farà all'interno. I linguaggi tipizzati staticamente hanno il lusso (o l'onere, a seconda del POV) di fare gran parte di questo lavoro in fase di compilazione, lasciando la parte in run-time con un piccolo aiuto di RTTI

    
risposta data 05.08.2012 - 20:00
fonte
1

Tutti i linguaggi tipizzati staticamente hanno un metodo integrato che è abbastanza equivalente, come instanceof o dynamic_cast . Gli elementi DOM hanno alcune interfacce condivise che potresti inserire in una classe base. Tuttavia, esistono strumenti tipicamente statici che non sono ereditari, ad esempio boost::variant e boost::any che possono fornire una digitazione dinamica tra elementi non correlati per ereditarietà.

    
risposta data 05.08.2012 - 22:22
fonte
1

Ci sono molti modi per gestirlo in lingue tipizzate staticamente. Probabilmente il più elegante è il pattern matching. Ecco Haskell:

doSomething (DivNode n)     = does something with a div node
doSomething (TableNode n)   = does something with a table node
doSomething _               = do nothing

Gli stessi meccanismi, o simili, esistono in altri linguaggi tipizzati staticamente (F #, Scala per quanto ne so). In C ++, lo stesso può essere emulato (anche se più verbosamente) con Boost.Variant e il visitatore statico:

struct visit_node : static_visitor<void> {
    void operator ()(DivNode const& n) {
        // Do something with a div node.
    }

    void operator ()(TableNode const& n) {
        // Do something with a table node.
    }

    template <typename T>
    void operator ()(T const& n) {
        // Do nothing.
    }
}

(Vale la pena notare che questa sembra una risoluzione di sovraccarico in fase di compilazione ma, grazie alla magia di Boost.Variant, funzionerà in fase di runtime e sarà completamente sicuro.)

Più in generale, un modello di visitatore può essere usato per implementarlo, ma sono ancora più prolissi di quelli di Boost static_visitor helper e (che sostanzialmente astrae il cruft) che li rende molto poco reattivo alle modifiche nella gerarchia delle classi.

E sì, puoi ovviamente codificare la spedizione tramite lanci dinamici come hanno mostrato altre risposte. Questa è facilmente la peggiore soluzione possibile, però. Come i metodi precedenti, codifica la gerarchia di tipi, ma lo fa in modo particolarmente dettagliato.

    
risposta data 14.08.2012 - 12:13
fonte
0

In Java, è possibile utilizzare la riflessione per determinare se un oggetto ha un metodo particolare su di esso. Ma è meglio raccogliere i metodi dell'interfaccia in un'interfaccia, e se hai bisogno di vedere se un oggetto fornisce quell'interfaccia, è solo un cast di distanza.

Allo stesso modo, mentre C ++ non fornisce la reflection, fornisce RTTI e dynamic_cast (o, meglio ancora, stai usando shared_ptr e dynamic_ptr_cast ) che puoi usare per interrogare un oggetto C ++ per un'interfaccia (che è un modello espresso tramite ereditarietà multipla, piuttosto che una caratteristica linguistica specifica).

    
risposta data 05.08.2012 - 21:04
fonte
0

Non vedo dove hai diversi problemi nelle lingue tipizzate in modo statico rispetto a quelle tipizzate dinamicamente. Perché l'unica differenza nel linguaggio digitato dinamicamente è che non devi dichiarare l'interfaccia e che un errore viene rilevato solo in fase di runtime.

Tuttavia in entrambi i casi devi:

  • Verifica il tipo effettivo dell'istanza e chiama solo il metodo che conosci. L'unica differenza è che il linguaggio tipizzato staticamente avrà bisogno di un cast.
  • Hai tutti i metodi in tutti i tipi, anche quelli in cui non hanno senso.
  • Utilizza un Pattern visitatori , che è abbastanza semplice in linguaggi tipizzati staticamente, mentre richiede l'introspezione nella maggior parte dei linguaggi dinamici.

Ci sono una manciata di lingue con dispatch multipli dinamici, ma IIRC ci sono sia quelli dinamici che quelli statici che fanno (e nessuno è ampiamente usato).

    
risposta data 14.08.2012 - 12:30
fonte
0

Il tuo esempio pone la domanda: come fai a sapere che stai iterando su un set di DOM? Come sai che qualcos'altro non è entrato di nascosto? Come fai a sapere che la nuova assunzione da PoliticallyCorrectUniversity ha letto abbastanza attentamente la documentazione e non ha definito la tua routine con l'elenco di cose sbagliato?

Vuoi che il compilatore catturi queste cose per te. Questo è ciò che sono forti tipizzazione e tipizzazione statica. (Richiama la risposta del comitato ALGOL alla proposta di Wirth per le regole di tipo predefinite per ALGOL.)

Si può dire al compilatore cosa si sta facendo in due modi: inserire un metodo virtuale nella classe base (o in un'interfaccia), con un errore barf per il caso predefinito che tutti ECCEDONO che la classe derivata privilegiata erediterà , o, come hai detto, "ripensa a qualcosa come Type Enums e un sacco di downy casting per verificare quale tipo di Nodo hai".

    
risposta data 14.08.2012 - 14:08
fonte
0

Ecco alcune regole sull'utilizzo dell'ereditarietà:

  1. Separa il tuo codice in due parti: interfaccia e implementazione
  2. L'interfaccia
  3. deve pubblicare tutti dati tramite l'interfaccia
  4. l'implementazione può implementare solo le funzioni disponibili nell'interfaccia. Non sono consentite altre implementazioni.
  5. I parametri di costruzione
  6. della classe derivata fanno parte della "interfaccia" di un oggetto - l'interfaccia è divisa in due parti: classe base comune e parametri costruttore della classe derivata
  7. Tutto il comportamento specifico viene gestito tramite i parametri del costruttore
risposta data 18.08.2012 - 11:51
fonte
-1

Per prima cosa, se vuoi qualcosa di simile a quello che avresti scritto in javascript, le istruzioni if probabilmente andrebbero nel corpo del metodo doSomething piuttosto che all'esterno. Non è più pulito, solo più vicino al tuo esempio di javascript.

Detto questo, puoi fare esattamente la stessa cosa in un linguaggio statico usando il metodo Overloading. In realtà, è ancora meglio: rimuoveresti tutte le istruzioni if tutte insieme!

Supponiamo che sia DivNode che TableNode ereditino da un tipo Node .

Quindi, se scrivi le 3 funzioni seguenti, non hai bisogno di loop o casting:

private void doSomething(Node node) {
   throw new RuntimeException ("This method should never be called !");
}

private void doSomething (DivNode divNode) {
   // function body
}

private void doSomething (TableNode tableNode) {
   // function body
}
    
risposta data 06.08.2012 - 06:51
fonte

Leggi altre domande sui tag