Nella progettazione dell'API, quando utilizzare / evitare il polimorfismo ad hoc?

14

Sue sta progettando una libreria JavaScript, Magician.js . Il suo linchpin è una funzione che estrae un Rabbit dall'argomento passato.

Sa che i suoi utenti potrebbero voler estrarre un coniglio da String , Number , Function , forse anche HTMLElement . Con questo in mente, potrebbe progettare la sua API in questo modo:

L'interfaccia rigorosa

Magician.pullRabbitOutOfString = function(str) //...
Magician.pullRabbitOutOfHTMLElement = function(htmlEl) //...

Ogni funzione nell'esempio sopra dovrebbe sapere come gestire l'argomento del tipo specificato nel nome della funzione / nome del parametro.

Oppure, potrebbe progettarlo in questo modo:

L'interfaccia "ad hoc"

Magician.pullRabbit = function(anything) //...

pullRabbit dovrebbe tenere conto della varietà di diversi tipi previsti che l'argomento anything potrebbe essere, oltre a (ovviamente) un tipo inatteso:

Magician.pullRabbit = function(anything) {
  if (anything === undefined) {
    return new Rabbit(); // out of thin air
  } else if (isString(anything)) {
    // more
  } else if (isNumber(anything)) {
    // more
  }
  // etc.
};

Il primo (rigoroso) sembra più esplicito, forse più sicuro e forse più performante, poiché ci sono pochi o nessun overhead per il controllo del tipo o la conversione del tipo. Ma quest'ultimo (ad hoc) sembra più semplice guardarlo dall'esterno, in quanto "funziona" con qualsiasi argomento il consumatore dell'API trova conveniente per passare ad esso.

Per la risposta a questa domanda , mi piacerebbe vedere pro e contro specifici per entrambi gli approcci (o per un approccio completamente diverso, se nessuno dei due è l'ideale), che Sue dovrebbe sapere quale approccio da prendere quando si progetta l'API della sua libreria.

    
posta GladstoneKeep 30.05.2013 - 17:49
fonte

5 risposte

6

Alcuni pro e contro

Professionisti per polimorfici:

  • Un'interfaccia polimorfica più piccola è più facile da leggere. Devo solo ricordare un metodo.
  • Si riferisce al modo in cui si intende utilizzare la lingua: digitazione anatra.
  • Se è chiaro quali oggetti voglio estrarre da un coniglio, non ci dovrebbero comunque essere ambiguità.
  • Fare un sacco di controlli di tipo è considerato negativo anche in linguaggi statici come Java, dove avere un sacco di controlli di tipo per il tipo di oggetto rende un brutto codice, se il mago ha davvero bisogno di distinguere tra il tipo di oggetti che sta tirando coniglio fuori?

Pro ad hoc:

  • È meno esplicito, posso estrarre una stringa da un'istanza Cat ? Funzionerebbe? se no, qual è il comportamento? Se non limito il tipo qui, devo farlo nella documentazione, o nei test che potrebbero peggiorare il contratto.
  • Hai tutta la gestione di tirare un coniglio in un posto, il Mago (alcuni potrebbero considerarlo un problema)
  • I moderni ottimizzatori JS differenziano tra funzioni monomorfe (funziona su un solo tipo) e polimorfiche. Loro sanno come ottimizzare i molto monomorfi meglio così la versione pullRabbitOutOfString rischia di essere molto più veloce nei motori come V8. Guarda questo video per ulteriori informazioni. Modifica: Ho scritto un perf io stesso, si trasforma che in pratica, questo non è sempre il caso .

Alcune soluzioni alternative:

Secondo me, questo tipo di design non è molto "Java-Scripty" per cominciare. JavaScript è una lingua diversa con idiomi diversi da linguaggi come C #, Java o Python. Questi idiomi hanno origine in anni di sviluppatori che cercano di capire le parti deboli e forti del linguaggio, quello che farei è cercare di attenermi a questi idiomi.

Ci sono due belle soluzioni a cui posso pensare:

  • Elevare oggetti, rendere "pulibili" gli oggetti, renderli conformi all'interfaccia in fase di esecuzione, quindi far funzionare il Mago su oggetti pulibili.
  • Uso del modello di strategia, insegnando al mago in modo dinamico come gestire diversi tipi di oggetti.

Soluzione 1: elevazione di oggetti

Una soluzione comune a questo problema è quella di "elevare" gli oggetti con la capacità di far estrarre i conigli da essi.

Cioè, avere una funzione che richiede un certo tipo di oggetto e aggiunge la tiratura fuori da un cappello. Qualcosa come:

function makePullable(obj){
   obj.pullOfHat = function(){
       return new Rabbit(obj.toString());
   }
}

Posso creare tali funzioni makePullable per altri oggetti, potrei creare un makePullableString , ecc. Sto definendo la conversione su ciascun tipo. Tuttavia, dopo aver elevato i miei oggetti, non ho tipo per usarli in modo generico. Un'interfaccia in JavaScript è determinata da una digitazione anatra, se ha un metodo pullOfHat I può tirarlo con il metodo del Mago.

Quindi il mago potrebbe fare:

Magician.pullRabbit = function(pullable) {
    var rabbit = obj.pullOfHat();
    return {rabbit:rabbit,text:"Tada, I pulled a rabbit out of "+pullable};
}

Elevare gli oggetti, usando una sorta di schema di mixin, sembra la cosa più JS da fare. (Nota questo è problematico con i tipi di valore nella lingua che sono stringa, numero, null, indefinito e booleano, ma sono tutti in box)

Ecco un esempio di come potrebbe essere il codice

Soluzione 2: schema strategico

Quando si discute questa domanda nella chat room di JS in StackOverflow il mio amico phenomnomnominal ha suggerito l'uso di Schema di strategia .

Ciò ti consentirebbe di aggiungere le abilità per estrarre conigli da vari oggetti in fase di esecuzione e creerebbe un codice molto JavaScript. Un mago può imparare come estrarre gli oggetti di diverso tipo dai cappelli, e li tira in base a quella conoscenza.

Ecco come potrebbe apparire in CoffeeScript:

class Magician
  constructor: ()-> # A new Magician can't pull anything
     @pullFunctions = {}

  pullRabbit: (obj) -> # Pull a rabbit, handler based on type
    func = pullFunctions[obj.constructor.name]
    if func? then func(obj) else "Don't know how to pull that out of my hat!"

  learnToPull: (obj, handler) -> # Learns to pull a rabbit out of a type
    pullFunctions[obj.constructor.name] = handler

Puoi vedere il codice JS equivalente qui .

In questo modo, trarrai beneficio da entrambi i mondi, l'azione di come tirare non è strettamente accoppiata agli oggetti o al Mago e penso che ciò sia una soluzione molto bella.

L'utilizzo sarebbe qualcosa come:

var m = new Magician();//create a new Magician
//Teach the Magician
m.learnToPull("",function(){
   return "Pulled a rabbit out of a string";
});
m.learnToPull({},function(){
   return "Pulled a rabbit out of a Object";
});

m.pullRabbit(" Str");
    
risposta data 30.05.2013 - 18:07
fonte
4

Il problema è che stai provando ad implementare un tipo di polimorfismo che non esiste in JavaScript. JavaScript è quasi universalmente trattato come un linguaggio tipizzato anatra, anche se supporta alcune facoltà di tipo.

Per creare la migliore API, la risposta è che dovresti implementare entrambi. È un po 'più di digitazione, ma a lungo termine risparmierai molto lavoro agli utenti della tua API.

pullRabbit dovrebbe essere solo un metodo di arbitro che controlla i tipi e chiama la funzione corretta associata a quel tipo di oggetto (ad esempio pullRabbitOutOfHtmlElement ).

In questo modo, mentre gli utenti di prototipazione possono utilizzare pullRabbit , se notano un rallentamento possono implementare il controllo dei tipi alla fine (probabilmente un modo più rapido) e chiamare direttamente pullRabbitOutOfHtmlElement direttamente.

    
risposta data 30.05.2013 - 18:14
fonte
2

Questo è JavaScript. A mano a mano che lo fai meglio, troverai spesso una via di mezzo che aiuta a negare dilemmi come questo. Inoltre, non importa se un "tipo" non supportato viene catturato da qualcosa o si interrompe quando qualcuno tenta di usarlo perché non esiste compilazione rispetto a tempo di esecuzione. Se lo usi male, si rompe. Cercare di nascondere che si è rotto o farlo funzionare a metà strada quando si è rotto non cambia il fatto che qualcosa è rotto.

Quindi mangia la tua torta e mangiala anche tu e impara ad evitare confusione di tipi e inutili rotture mantenendo tutto veramente, davvero ovvio, come in un nome ben fatto e con tutti i dettagli giusti in tutti i posti giusti.

Prima di tutto, incoraggio strongmente a prendere l'abitudine di mettere le anatre in fila prima di dover controllare i tipi. La cosa più snella e più efficiente (ma non sempre la migliore dove siano interessati i costruttori nativi) sarebbe colpire prima i prototipi, quindi il tuo metodo non deve nemmeno preoccuparsi di quale tipo di supporto è in gioco.

String.prototype.pullRabbit = function(){
    //do something string-relevant
}

HTMLElement.prototype.pullRabbit = function(){
    //do something HTMLElement-relevant
}

Magician.pullRabbitFrom = function(someThingy){
    return someThingy.pullRabbit();
}

Nota: è ampiamente considerato come una cattiva forma per fare ciò a Object poiché tutto eredita da Object. Personalmente eviterei anche Function. Alcuni potrebbero sentirsi ansiosi di toccare qualsiasi prototipo di costruttore nativo che potrebbe non essere una cattiva politica ma l'esempio potrebbe ancora servire quando si lavora con i propri costruttori di oggetti.

Non mi preoccuperei di questo approccio per un metodo di uso così specifico che non è in grado di clobare qualcosa da un'altra libreria in un'app meno complicata ma è un buon istinto evitare di fare affermazioni eccessivamente generiche sui metodi nativi in JavaScript se non devi a meno che tu non stia normalizzando i nuovi metodi nei browser non aggiornati.

Fortunatamente, puoi sempre pre-mappare i tipi oi nomi dei costruttori ai metodi (fai attenzione a IE < = 8 che non ha < object > .constructor.name che richiede di analizzarlo dai risultati toString dalla proprietà del costruttore ). In effetti stai ancora controllando il nome del costruttore (typeof è un po 'inutile in JS quando si confrontano oggetti) ma almeno si legge molto meglio di una gigantesca istruzione switch o se / else concatena in ogni chiamata del metodo a quello che potrebbe essere un ampio varietà di oggetti.

var rabbitPullMap = {
    String: ( function pullRabbitFromString(){
        //do stuff here
    } ),
    //parens so we can assign named functions if we want for helpful debug
    //yes, I've been inconsistent. It's just a nice unrelated trick
    //when you want a named inline function assignment

    HTMLElement: ( function pullRabitFromHTMLElement(){
        //do stuff here
    } )
}

Magician.pullRabbitFrom = function(someThingy){
    return rabbitPullMap[someThingy.constructor.name]();
}

O utilizzando lo stesso approccio di mappa, se si desidera accedere al componente "questo" dei diversi tipi di oggetto per utilizzarli come se fossero metodi senza toccare i loro prototipi ereditati:

var rabbitPullMap = {
    String: ( function(obj){

    //yes the anon wrapping funcs would make more sense in one spot elsewhere.

        return ( function pullRabbitFromString(obj){
            var rabbitReach = this.match(/rabbit/g);
            return rabbitReach.length;
        } ).call(obj);
    } ),

    HTMLElement: ( function(obj){
        return ( function pullRabitFromHTMLElement(obj){
            return this.querySelectorAll('.rabbit').length;
        } ).call(obj);
    } )
}

Magician.pullRabbitFrom = function(someThingy){

    var
        constructorName = someThingy.constructor.name,
        rabbitCnt = rabbitPullMap[constructorName](someThingy);

    console.log(
        [
            'The magician pulls ' + rabbitCnt,
            rabbitCnt === 1 ? 'rabbit' : 'rabbits',
            'out of her ' + constructorName + '.',
            rabbitCnt === 0 ? 'Boo!' : 'Yay!'
        ].join(' ');
    );
}

Un buon principio generale in qualsiasi linguaggio IMO, è di provare a ordinare i dettagli di ramificazione come questo prima di arrivare al codice che in realtà tira il grilletto. In questo modo è facile vedere tutti i giocatori coinvolti a quel livello API superiore per una bella panoramica, ma è anche molto più facile individuare dove i dettagli potrebbero interessare qualcuno che potrebbe essere trovato.

Nota: questo non è stato verificato, perché presumo che nessuno abbia effettivamente un RL usato per questo. Sono sicuro che ci siano errori di battitura.

    
risposta data 01.06.2013 - 00:57
fonte
1

Questo (per me) è una domanda interessante e complicata a cui rispondere. In realtà mi piace questa domanda, quindi farò del mio meglio per rispondere. Se fai qualsiasi ricerca in tutti gli standard per la programmazione in javascript, troverai tanti modi "giusti" per farlo, dato che ci sono persone che pubblicizzano il modo "giusto" di farlo.

Ma dal momento che stai cercando un parere su quale strada è meglio. Qui non va niente.

Personalmente preferirei l'approccio progettuale "ad hoc". Provenendo da uno sfondo c ++ / C #, questo è più il mio stile di sviluppo. È possibile creare una richiesta pullRabbit e avere quel tipo di richiesta controllare l'argomento passato e fare qualcosa. Ciò significa che non devi preoccuparti di quale tipo di argomento viene passato in qualsiasi momento. Se si utilizza l'approccio rigoroso, è comunque necessario verificare quale tipo è la variabile, ma lo si farebbe prima di effettuare la chiamata al metodo. Quindi alla fine la domanda è, vuoi controllare il tipo prima di effettuare la chiamata o dopo.

Spero che questo aiuti, per favore sentiti libero di fare più domande in relazione a questa risposta, farò del mio meglio per chiarire la mia posizione.

    
risposta data 30.05.2013 - 17:59
fonte
0

Quando scrivi, Magician.pullRabbitOutOfInt, documenta ciò a cui hai pensato quando hai scritto il metodo. Il chiamante si aspetta che funzioni, se ha passato un numero intero. Quando scrivi, Magician.pullRabbitOutOfAnything, il chiamante non sa cosa pensare e deve scavare nel tuo codice e sperimentare. Potrebbe funzionare per un Int, ma funzionerà per un lungo? Un galleggiante? Un doppio? Se stai scrivendo questo codice, quanto sei disposto ad andare? Che tipo di argomenti sei disposto a supportare?

  • Archi?
  • Array?
  • Maps?
  • Streams?
  • Funzioni?
  • Database?

L'ambiguità richiede tempo per capire. Non sono nemmeno convinto che sia più veloce scrivere:

Magician.pullRabbit = function(anything) {
  if (anything === undefined) {
    return new Rabbit(); // out of thin air
  } else if (isString(anything)) {
    // more
  } else if (isNumber(anything)) {
    // more
  } else {
      throw new Exception("You can't pull a rabbit out of that!");
  }
  // etc.
};

Vs:

Magician.pullRabbitFromAir = fromAir() {
    return new Rabbit(); // out of thin air
}
Magician.pullRabbitFromStr = fromString(str)) {
    // more
}
Magician.pullRabbitFromInt = fromInt(int)) {
    // more
};

OK, quindi ho aggiunto un'eccezione al codice (che consiglio vivamente) di dire al chiamante che non avresti mai immaginato che ti avrebbero passato quello che facevano. Ma scrivere metodi specifici (non so se JavaScript ti permette di farlo) non è più un codice e molto più facile da capire come il chiamante. Stabilisce ipotesi realistiche su ciò che l'autore di questo codice ha pensato e rende il codice facile da usare.

    
risposta data 30.05.2013 - 18:19
fonte

Leggi altre domande sui tag