¿Cuáles son algunas técnicas que puedo usar para refactorizar el código orientado a objetos en código funcional?


9

He pasado alrededor de 20-40 horas desarrollando parte de un juego usando JavaScript y HTML5 Canvas. Cuando comencé no tenía idea de lo que estaba haciendo. Así que comenzó como una prueba de concepto y ahora se está desarrollando muy bien, pero no tiene pruebas automatizadas. El juego comienza a ser lo suficientemente complejo como para beneficiarse de algunas pruebas automáticas, pero parece difícil de hacer porque el código depende de un estado global mutante.

Me gustaría refactorizar todo usando Underscore.js , una biblioteca de programación funcional para JavaScript. Una parte de mí piensa que debería comenzar desde cero usando un estilo de programación funcional y pruebas. Pero, creo que refactorizar el código imperativo en código declarativo podría ser una mejor experiencia de aprendizaje y una forma más segura de llegar a mi estado actual de funcionalidad.

El problema es que sé cómo quiero que se vea mi código al final, pero no sé cómo convertir mi código actual en él. Espero que algunas personas aquí puedan darme algunos consejos al estilo del libro Refactoring y Working Effectively With Legacy Code.


Por ejemplo, como primer paso estoy pensando en "prohibir" el estado global. Tome todas las funciones que usan una variable global y páselo como parámetro.

El siguiente paso puede ser "prohibir" la mutación y siempre devolver un nuevo objeto.

Cualquier consejo sería apreciado. Nunca he tomado el código OO y lo he refactorizado en código funcional antes.


1
"El siguiente paso puede ser" prohibir "la mutación y siempre devolver un nuevo objeto": OMI, no es necesario prohibir la mutación por completo: lo que cuenta es la transparencia referencial. Puede usar actualizaciones destructivas como una técnica de optimización siempre que conserve la transparencia referencial.
Giorgio

@Giorgio es bueno saberlo. Sin embargo, preferiría "prohibir" la mutación primero y optimizar la segunda.
Daniel Kaplan

1
¡Creo que tus dos primeros pasos son una gran idea! Sería un honor si echara un vistazo a una biblioteca JS funcional que escribí (pequeña) para hacer cadenas de funciones compuestas monádicamente, puede ayudar; pasa el estado a lo largo de la cadena de funciones pero nunca muta; cada función que encadene asegurará que los valores se clonen para devolver un nuevo valor en lugar del valor que se le entrega. github.com/JimmyHoffa/Machinad La biblioteca en sí es bastante compleja si no estás familiarizado con las mónadas, pero mira el ejemplo y deberías ver que es muy fácil de usar; fue simplemente complejo de implementar.
Jimmy Hoffa

1
Si desea hacer las cosas en un estilo funcional, una cosa es que me aseguraría de eliminar su código de la herencia si es que existe. obligarse a componer funciones y usar lo que javascript es excelente en: alcance léxico para hacer que las instalaciones estén disponibles donde las necesite en lugar de los árboles de jerarquía. Defina sus estructuras de datos como solo esas, solo agregue funciones a ellas para ayudar a un estilo fluido donde la función sería tan buena tomando la estructura de datos como primer parámetro. En OO, este sería un modelo anémico; pero estamos hablando de FP, la composición es la clave para evitar esos inconvenientes.
Jimmy Hoffa

@tieTYT: Por supuesto que estoy de acuerdo con usted, todos sabemos que la optimización prematura es mala.
Giorgio

Respuestas:


9

Después de haber creado varios proyectos en JavaScript con estilo funcional, me permite compartir mi experiencia. Me enfocaré en consideraciones de alto nivel, no en preocupaciones específicas de implementación. Recuerde, no hay balas de plata, haga lo que funcione mejor para su situación específica.

Estilo funcional puro versus funcional

Considere lo que desea obtener de esta refactorización funcional. ¿Realmente desea una implementación funcional pura o solo algunos de los beneficios que puede aportar un estilo de programación funcional, como la transparencia referencial?

Encontré el último enfoque, lo que yo llamo estilo funcional, para combinar bien con JavaScript y generalmente es lo que quiero. También permite que los conceptos no funcionales selectivos se utilicen cuidadosamente en el código donde tengan sentido.

Escribir código en un estilo más puramente funcional también es posible en JavaScript, pero no siempre es la solución más adecuada. Y Javascript nunca puede ser puramente funcional.

Interfaces de estilo funcional

En Javascript de estilo funcional, la consideración más importante es el límite entre el código funcional y el código imperativo. Comprender y definir claramente este límite es esencial.

Organizo mi código alrededor de interfaces de estilo funcional. Estas son colecciones de lógica que se comportan en un estilo funcional, que van desde funciones individuales hasta módulos completos. La separación conceptual de la interfaz y la implementación es importante, una interfaz de estilo funcional puede implementarse imperativamente, siempre que la implementación imperativa no se filtre a través del límite.

Desafortunadamente en Javascript, la carga de aplicar interfaces de estilo funcional recae completamente en el desarrollador. Además, las características del lenguaje, como las excepciones, hacen imposible una separación verdaderamente limpia.

Dado que refactorizará una base de código existente, comience por identificar las interfaces lógicas existentes en su código que se puedan mover limpiamente a un estilo funcional. Una sorprendente cantidad de código ya puede ser un estilo funcional. Los buenos puntos de partida son pequeñas interfaces con pocas dependencias imperativas. Cada vez que se mueve sobre el código, se eliminan las dependencias imperativas existentes y se puede mover más código.

Siga iterando, trabajando gradualmente hacia afuera para empujar grandes partes de su proyecto hacia el lado funcional del límite, hasta que esté satisfecho con los resultados. Este enfoque incremental permite probar cambios individuales y facilita la identificación y el cumplimiento de los límites.

Replantear algoritmos, estructuras de datos y diseño

Las soluciones imperativas y los patrones de diseño a menudo no tienen sentido en el estilo funcional. Especialmente para estructuras de datos, los puertos directos de código imperativo al estilo funcional pueden ser feos y lentos. Un ejemplo simple: ¿preferiría copiar cada elemento de una lista para un antecedente o contras el elemento en la lista existente?

Investigue y evalúe los enfoques funcionales existentes a los problemas. La programación funcional es más que una simple técnica de programación, es una forma diferente de pensar y resolver problemas. Los proyectos escritos en los lenguajes de programación funcional más tradicionales, como lisp o haskell, son excelentes recursos a este respecto.

Comprender el lenguaje y las construcciones

Esta es una parte importante de la comprensión del límite imperativo funcional y afectará el diseño de la interfaz. Comprenda qué características se pueden usar de forma segura en el código de estilo funcional y cuáles no. Todo desde el cual los componentes integrados pueden generar excepciones y que, como [].push, mutar objetos, a objetos que se pasan por referencia y cómo funcionan las cadenas de prototipos. También se requiere una comprensión similar de cualquier otra biblioteca que consuma en su proyecto.

Tal conocimiento permite tomar decisiones de diseño informadas, como cómo manejar las malas entradas y excepciones, o ¿qué sucede si una función de devolución de llamada hace algo inesperado? Algo de esto puede aplicarse en el código, pero algunos solo requieren documentación sobre el uso adecuado.

Otro punto es cuán estricta debería ser su implementación. ¿Realmente quiere intentar imponer el comportamiento correcto en los errores de pila de llamadas máximas o cuando el usuario redefine alguna función incorporada?

Uso de conceptos no funcionales donde sea apropiado

Los enfoques funcionales no siempre son apropiados, especialmente en un lenguaje como JavaScript. La solución recursiva puede ser elegante hasta que comience a obtener la máxima cantidad de excepciones de la pila de llamadas para entradas más grandes, por lo que quizás sería mejor un enfoque iterativo. Del mismo modo, uso la herencia para la organización del código, la asignación en constructores y los objetos inmutables donde tienen sentido.

Mientras no se violen las interfaces funcionales, haga lo que tenga sentido y lo que será más fácil de probar y mantener.


Ejemplo

Aquí hay un ejemplo de conversión de un código real muy simple a un estilo funcional. No tengo un muy buen ejemplo grande del proceso, pero este pequeño toca varios puntos interesantes.

Este código construye un trie a partir de una cadena. Comenzaré con un código basado en la sección superior del módulo trie-js build-trie de John Resig . Se han realizado muchas simplificaciones / cambios de formato y mi intención no es comentar sobre la calidad del código original (Hay formas mucho más claras y limpias de construir un trie, por lo que es interesante por qué este código de estilo c aparece primero en Google. Aquí hay un Implementación rápida que utiliza reducir ).

Me gusta este ejemplo porque no tengo que llevarlo a una implementación funcional verdadera, solo a interfaces funcionales. Aquí está el punto de partida:

var txt = SOME_USER_STRING,
    words = txt.replace(/\n/g, "").split(" "),
    trie = {};

for (var i = 0, l = words.length; i < l; i++) {
    var word = words[i], letters = word.split(""), cur = trie;
    for (var j = 0; j < letters.length; j++) {
        var letter = letters[j];
        if (!cur.hasOwnProperty(letter))
            cur[letter] = {};
        cur = cur[ letter ];
    }
    cur['$'] = true;
}

// Output for text = 'a ab f abc abf'
trie = {
    "a": {
        "$":true,
        "b":{
            "$":true,
            "c":{"$":true}
        "f":{"$":true}}},
    "f":{"$":true}};

Supongamos que este es el programa completo. Este programa hace dos cosas: convertir una cadena en una lista de palabras y construir un trie a partir de la lista de palabras. Estas son las interfaces existentes en el programa. Aquí está nuestro programa con las interfaces más claramente expresadas:

var _get_words = function(txt) {
    return txt.replace(/\n/g, "").split(" ");
};

var _build_trie_from_list = function(words, trie) {
    for (var i = 0, l = words.length; i < l; i++) {
        var word = words[i], letters = word.split(""), cur = trie;
        for (var j = 0; j < letters.length; j++) {
            var letter = letters[j];
            if (!cur.hasOwnProperty(letter))
                cur[letter] = {};
            cur = cur[ letter ];
        }
        cur['$'] = true;
    }
};

// The 'public' interface
var build_trie = function(txt) { 
    var words = _get_words(txt), trie = {};
    _build_trie_from_list(words, trie);
    return trie;
};

Simplemente definiendo nuestras interfaces, podemos pasar _get_wordsal lado del estilo funcional del límite. Ni replaceo splitmodifica la cadena original. Y nuestra interfaz pública también build_triees de estilo funcional, aunque interactúa con un código muy imperativo. De hecho, este es un buen punto de parada en la mayoría de los casos. Más código se volvería abrumador, así que permítanme ver algunos otros cambios.

Primero, haga que todas las interfaces sean funcionales. Esto es trivial en este caso, solo tiene que _build_trie_from_listdevolver un objeto en lugar de mutar uno.

Manejo de entrada incorrecta

Considere lo que sucede si llamamos build_triecon una serie de personajes, build_trie(['a', ' ', 'a', 'b', 'c', ' ', 'f']). La persona que llama asumió que esto se comportaría en un estilo funcional, pero en cambio arroja una excepción cuando .replacese llama en la matriz. Este puede ser el comportamiento previsto. O podríamos hacer verificaciones de tipo explícitas y asegurarnos de que las entradas son como esperábamos. Pero prefiero escribir pato.

Las cadenas son solo matrices de caracteres y las matrices son solo objetos con una lengthpropiedad y números enteros no negativos como claves. Entonces, si refactorizamos y escribimos nuevos replacey splitmétodos que operan en objetos de cadenas similares a una matriz, ni siquiera tienen que ser cadenas, nuestro código podría hacer lo correcto. ( String.prototype.*no funcionará aquí, convierte la salida en una cadena). La escritura de pato es una programación totalmente separada de la funcional, pero el punto más importante es que la entrada incorrecta siempre debe considerarse

Rediseño fundamental

También hay consideraciones más fundamentales. Supongamos que también queremos construir el estilo funcional. En el código imperativo, el enfoque consistía en construir el trie palabra por palabra. Un puerto directo nos haría copiar todo el archivo cada vez que necesitemos hacer una inserción para evitar la mutación de objetos. Claramente eso no va a funcionar. En cambio, el trie podría construirse nodo por nodo, de abajo hacia arriba, de modo que una vez que se complete un nodo, nunca más se tenga que tocar. O bien, otro enfoque por completo puede ser mejor, una búsqueda rápida muestra muchas implementaciones funcionales existentes de intentos.

Espero que el ejemplo aclare un poco las cosas.


En general, creo que escribir código de estilo funcional en Javascript es un esfuerzo valioso y divertido. Javascript es un lenguaje funcional sorprendentemente capaz, aunque demasiado detallado en su estado predeterminado.

Buena suerte con tu proyecto.

Algunos proyectos personales escritos en estilo funcional Javascript:

  • parse.js - Combinadores de analizador
  • Nu - corrientes perezosas
  • Atum - Intérprete Javascript escrito en estilo funcional Javascript.
  • Khepri : lenguaje de programación prototipo que uso para el desarrollo funcional de Javascript. Implementado en estilo funcional Javascript.

Muchas gracias por tomarse el tiempo para escribir esto. Algunos ejemplos serían muy apreciados (y los proyectos personales no ayudan con esto porque muestran el "después", no muestran cómo llegar desde "antes"). Esto sería más útil en su sección titulada "Interfaces de estilo funcional".
Daniel Kaplan

Agregué un ejemplo simple. Encontrar un ejemplo más amplio que demuestre el proceso de conversión será difícil, por lo que recomiendo tratar de comenzar a trabajar con código del mundo real lo antes posible. Realmente es un proceso muy específico del proyecto y realmente funciona, aunque el código le enseñará mucho. Quizás el primer pase no sea excelente, pero sigue iterando y aprendiendo hasta que estés satisfecho con el resultado. Y hay muchos ejemplos excelentes de diseño funcional en línea para otros idiomas (personalmente considero que el esquema y la configuración son los mejores puntos de partida para muchos temas).
Matt Bierner

Ojalá esta respuesta se dividiera en varias para poder votar cada parte
Paul
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.