En el diseño de API, ¿cuándo usar / evitar el polimorfismo ad hoc?

14

Sue está diseñando una biblioteca de JavaScript, Magician.js . Su eje es una función que saca a Rabbit del argumento pasado.

Ella sabe que sus usuarios pueden querer sacar un conejo de un String , un Number , un Function , quizás incluso un HTMLElement . Con eso en mente, ella podría diseñar su API de esta manera:

La interfaz estricta

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

Cada función en el ejemplo anterior sabría cómo manejar el argumento del tipo especificado en el nombre de la función / parámetro.

O, ella podría diseñarlo así:

La interfaz "ad hoc"

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

pullRabbit debería tener en cuenta la variedad de diferentes tipos esperados que podría ser el argumento anything , así como (por supuesto) un tipo inesperado:

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.
};

El primero (estricto) parece más explícito, quizás más seguro y quizás más eficaz, ya que hay poca o ninguna sobrecarga para la verificación de tipo o la conversión de tipo. Pero este último (ad hoc) se siente más simple mirándolo desde el exterior, en el sentido de que "simplemente funciona" con cualquier argumento que el consumidor de API considere conveniente transmitirle.

Para la respuesta a esta pregunta , me gustaría ver los pros y los contras específicos de cualquiera de los enfoques (o de un enfoque diferente, si ninguno es el ideal), que Sue debe saber qué enfoque para tomar al diseñar la API de su biblioteca.

    
pregunta GladstoneKeep 30.05.2013 - 17:49

5 respuestas

6

Algunos pros y contras

Pros para polimorfos:

  • Una interfaz polimórfica más pequeña es más fácil de leer. Solo tengo que recordar un método.
  • Va con la forma en que se debe usar el lenguaje: escritura de pato.
  • Si está claro de qué objetos quiero sacar un conejo, no debería haber ambigüedad de todos modos.
  • Hacer muchas comprobaciones de tipos se considera malo incluso en lenguajes estáticos como Java, donde tener muchas comprobaciones de tipos para el tipo del objeto hace que sea un código feo, si el mago realmente necesita diferenciar entre el tipo de objetos que está tirando conejo fuera de?

Pros para ad-hoc:

  • Es menos explícito, ¿puedo extraer una cadena de una instancia Cat ? ¿Eso solo funcionaría? Si no, ¿cuál es el comportamiento? Si no limito el tipo aquí, tengo que hacerlo en la documentación o en las pruebas que podrían empeorar el contrato.
  • Usted tiene todo el manejo de jalar un conejo en un lugar, el Mago (algunos podrían considerar esto como una estafa)
  • Los optimizadores JS modernos se diferencian entre las funciones monomórficas (funcionan en un solo tipo) y polimórficas. Saben cómo optimizar los monomorfos mucho mejor, por lo que es probable que la versión pullRabbitOutOfString sea mucho más rápida en motores como el V8. Vea este video para obtener más información. Editar: Yo mismo escribí un artículo perf. Fuera de en la práctica, este no es siempre el caso .

Algunas soluciones alternativas:

En mi opinión, este tipo de diseño no es muy 'Java-Scripty' para empezar. JavaScript es un idioma diferente con diferentes idiomas de lenguajes como C #, Java o Python. Estas expresiones idiomáticas se originan en años de desarrolladores que intentan comprender las partes débiles y fuertes de la lengua, lo que yo haría es tratar de mantener estas expresiones idiomáticas.

Hay dos buenas soluciones en las que puedo pensar:

  • Elevar objetos, hacer que los objetos sean "pulibles", hacer que se ajusten a una interfaz en el tiempo de ejecución, y luego hacer que el Mago trabaje en objetos que se pueden tirar.
  • Utilizando el patrón de estrategia, enseñándole al Mago dinámicamente cómo manejar diferentes tipos de objetos.

Solución 1: Elevar objetos

Una solución común a este problema, es "elevar" los objetos con la capacidad de sacar a los conejos de ellos.

Es decir, tener una función que tome algún tipo de objeto y agregue sacar un sombrero para ello. Algo como:

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

Puedo hacer tales funciones makePullable para otros objetos, podría crear un makePullableString , etc. Estoy definiendo la conversión en cada tipo. Sin embargo, después de elevar mis objetos, no tengo ningún tipo para usarlos de forma genérica. Una interfaz en JavaScript está determinada por una tipificación de pato, si tiene un método pullOfHat puedo tirar de ella con el método del mago.

Entonces Mago podría hacer:

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

Elevar objetos, usar algún tipo de patrón de mezcla parece ser lo más que JS puede hacer. (Tenga en cuenta que esto es problemático con los tipos de valor en el idioma que son cadena, número, nulo, indefinido y booleano, pero todos son compatibles con el cuadro)

Este es un ejemplo de cómo podría verse dicho código

Solución 2: Patrón de estrategia

Al analizar esta pregunta en la sala de chat de JS en StackOverflow, mi amigo phenomnomnominal sugirió el uso de Patrón de estrategia .

Esto le permitiría agregar las habilidades para sacar a los conejos de varios objetos en el tiempo de ejecución, y crearía un código muy JavaScript. Un mago puede aprender cómo sacar objetos de diferentes tipos de sombreros, y los saca basándose en ese conocimiento.

Aquí es cómo podría verse esto en 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

Puede ver el código JS equivalente here .

De esta manera, se beneficia de ambos mundos, la acción de cómo tirar no está estrechamente unida a los objetos, o al Mago y creo que esto constituye una solución muy agradable.

El uso sería algo como:

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");
    
respondido por el Benjamin Gruenbaum 30.05.2013 - 18:07
4

El problema es que estás intentando implementar un tipo de polimorfismo que no existe en JavaScript. JavaScript se trata casi universalmente como un lenguaje tipográfico de pato, a pesar de que admite algunas facultades de tipo.

Para crear la mejor API, la respuesta es que debe implementar ambas. Es un poco más de escritura, pero a la larga ahorrará mucho trabajo para los usuarios de su API.

pullRabbit debería ser solo un método de árbitro que verifique los tipos y llame a la función apropiada asociada con ese tipo de objeto (por ejemplo, pullRabbitOutOfHtmlElement ).

De esa manera, mientras los usuarios de prototipos pueden usar pullRabbit , pero si notan una desaceleración, pueden implementar la verificación de tipos en su final (probablemente de una forma más rápida) y simplemente llamar a pullRabbitOutOfHtmlElement directamente.

    
respondido por el Jonathan Rich 30.05.2013 - 18:14
2

Esto es JavaScript. A medida que lo consiga mejor, encontrará que a menudo hay un camino intermedio que ayuda a negar dilemas como este. Además, realmente no importa si un "tipo" no compatible es atrapado por algo o se rompe cuando alguien intenta usarlo porque no hay compilación o tiempo de ejecución. Si lo usas mal se rompe. Tratar de ocultar que se rompió o hacer que funcione a medio camino cuando se rompió no cambia el hecho de que algo esté roto.

Tenga su pastel y cómalo también, y aprenda a evitar la confusión de tipos y las roturas innecesarias manteniendo todo realmente, muy obvio, como en el nombre y con todos los detalles correctos en todos los lugares correctos.

En primer lugar, te recomiendo que adquieras el hábito de poner a tus patos en fila antes de que necesites verificar los tipos. Lo más inteligente y eficiente (pero no siempre es lo mejor para los constructores nativos) es atacar los prototipos primero para que su método no tenga que preocuparse por qué tipo de soporte está en juego.

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

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

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

Nota: en general, se considera una mala forma de hacer esto a Object, ya que todo se hereda de Object. Yo personalmente evitaría la función también. Algunos pueden sentirse ansiosos por tocar cualquier prototipo de constructor nativo, lo que podría no ser una mala política, pero el ejemplo podría seguir sirviendo cuando trabaje con sus propios constructores de objetos.

No me preocuparía este enfoque para un método de uso tan específico que no es probable que obstruya algo de otra biblioteca en una aplicación menos complicada, pero es un buen instinto para evitar afirmar cualquier cosa en general a través de métodos nativos en JavaScript si no tiene que hacerlo a menos que esté normalizando métodos más nuevos en navegadores desactualizados.

Afortunadamente, siempre puede asignar previamente los tipos o los nombres de los constructores a los métodos (tenga cuidado con IE < = 8, que no tiene < object > .constructor.name, lo que requiere que analice los resultados de la propiedad del constructor ). Aún está en efecto verificando el nombre del constructor (typeof es un poco inútil en JS al comparar objetos) pero al menos se lee mucho mejor que una instrucción de cambio gigante o si / else encadena en cada llamada del método a lo que podría ser una amplia variedad de objetos.

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 utilizando el mismo enfoque de mapa, si quisiera acceder al componente "this" de los diferentes tipos de objetos para usarlos como si fueran métodos sin tocar sus prototipos heredados:

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 buen principio general en cualquier IMO de idioma, es tratar de ordenar los detalles de bifurcación como este antes de llegar al código que realmente aprieta el gatillo. De esa manera, es fácil ver a todos los jugadores involucrados en ese nivel superior de API para obtener una buena visión general, pero también es mucho más fácil averiguar dónde es probable que se encuentren los detalles que a alguien podría interesar.

Nota: todo esto no se ha probado, porque asumo que en realidad nadie tiene un uso de RL para ello. Estoy seguro de que hay errores tipográficos / errores.

    
respondido por el Erik Reppen 01.06.2013 - 00:57
1

Esta (para mí) es una pregunta interesante y complicada de responder. De hecho, me gusta esta pregunta, así que haré mi mejor esfuerzo para responder. Si realiza alguna investigación sobre los estándares para la programación de javascript, encontrará tantas formas "correctas" de hacerlo como personas que están promocionando la forma "correcta" de hacerlo.

Pero ya que estás buscando una opinión sobre qué camino es mejor. Aquí no va nada.

Personalmente preferiría el enfoque de diseño "adhoc". Procedente de un fondo c ++ / C #, este es más mi estilo de desarrollo. Puede crear una solicitud pullRabbit y hacer que ese tipo de solicitud verifique el argumento pasado y haga algo. Esto significa que no tiene que preocuparse por el tipo de argumento que se transmite en un momento dado. Si utiliza el enfoque estricto, aún deberá verificar qué tipo de variable es la variable, pero en lugar de eso, deberá hacerlo antes de realizar la llamada al método. Entonces, al final, la pregunta es: ¿desea verificar el tipo antes de hacer la llamada o después?

Espero que esto ayude, no dude en hacer más preguntas en relación con esta respuesta, haré todo lo posible para aclarar mi posición.

    
respondido por el Kenneth Garza 30.05.2013 - 17:59
0

Cuando escribes, Magician.pullRabbitOutOfInt, documenta lo que pensaste cuando escribiste el método. La persona que llama esperará que esto funcione si se pasa cualquier Entero. Cuando escribe, Magician.pullRabbitOutOfAnything, la persona que llama no sabe qué pensar y tiene que investigar su código y experimentar. Puede funcionar para un Int, pero ¿funcionará para un largo? Un flotador ¿Un doble? Si está escribiendo este código, ¿hasta dónde está dispuesto a ir? ¿Qué tipo de argumentos está dispuesto a apoyar?

  • ¿Cadenas?
  • ¿Arrays?
  • ¿Mapas?
  • ¿Arroyos?
  • ¿Funciones?
  • ¿Bases de datos?

La ambigüedad lleva tiempo para comprender. Ni siquiera estoy convencido de que sea más rápido escribir:

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
};

Bien, entonces agregué una excepción a su código (que recomiendo encarecidamente) para decirle a la persona que llama que nunca imaginó que le pasaría lo que hicieron. Pero escribir métodos específicos (no sé si JavaScript te permite hacer esto) no es más código y es más fácil de entender que el que llama. Establece suposiciones realistas sobre lo que pensó el autor de este código, y hace que el código sea fácil de usar.

    
respondido por el GlenPeterson 30.05.2013 - 18:19

Lea otras preguntas en las etiquetas