Quais são algumas técnicas que posso usar para refatorar o código orientado a objetos em código funcional?

9

Passei cerca de 20 a 40 horas desenvolvendo parte de um jogo usando JavaScript e tela HTML5. Quando comecei, não fazia ideia do que estava fazendo. Então, começou como uma prova de conceito e está se saindo bem agora, mas não possui testes automatizados. O jogo está começando a se tornar complexo o suficiente para se beneficiar de alguns testes automatizados, mas parece difícil, porque o código depende da mutação do estado global.

Eu gostaria de refatorar a coisa toda usando Underscore.js , uma biblioteca de programação funcional para JavaScript. Parte de mim pensa que deveria começar do zero usando um estilo de programação funcional e testes. Mas acho que refatorar o código imperativo em código declarativo pode ser uma melhor experiência de aprendizado e uma maneira mais segura de chegar ao meu estado atual de funcionalidade.

O problema é que eu sei como eu quero que meu código seja no final, mas não sei como transformar meu código atual nele. Espero que algumas pessoas aqui possam me dar algumas dicas sobre o livro Refatoração e Trabalhando efetivamente com o código legado.


Por exemplo, como primeiro passo, estou pensando em "banir" o estado global. Pegue todas as funções que usam uma variável global e passe-a como um parâmetro.

O próximo passo pode ser "banir" a mutação e sempre retornar um novo objeto.

Qualquer conselho seria apreciado. Nunca peguei o código OO e o refatorei em código funcional antes.

Daniel Kaplan
fonte
1
"O próximo passo pode ser" banir "a mutação e sempre retornar um novo objeto.": Na IMO, você não precisa banir a mutação completamente: a transparência referencial é o que conta. Você pode usar atualizações destrutivas como uma técnica de otimização, desde que você preserve a transparência referencial.
Giorgio
@Giorgio bom saber. Eu preferiria "banir" a mutação primeiro e otimizar a segunda, no entanto.
Daniel Kaplan
1
Eu acho que seus dois primeiros passos são uma ótima idéia! Eu ficaria honrado se você desse uma olhada em uma biblioteca JS funcional que escrevi (pequena) para criar cadeias de funções monadicamente compostas, isso pode ajudar; passa o estado através da cadeia de funções, mas nunca muda; cada função que você associa a ela garantirá que os valores sejam clonados para retornar um novo valor, e não o valor entregue a ele. github.com/JimmyHoffa/Machinad A biblioteca em si é bastante complexa se você não conhece as mônadas, mas veja o exemplo e verá que é muito fácil de usar; era apenas complexo de implementar.
Jimmy Hoffa
1
Se você deseja fazer as coisas em um estilo funcional, uma coisa é garantir que você elimine seu código de herança, mesmo que esteja lá. forçar-se a compor funções e usar o que é ótimo no javascript: escopo lexical para disponibilizar instalações onde você precisa, em vez de árvores de hierarquia. Defina suas estruturas de dados apenas como essas, adicione funções a elas para ajudar no estilo fluente, onde a função seria tão boa quanto a estrutura de dados como primeiro parâmetro. No OO, esse seria um modelo anêmico; mas estamos falando de FP, a composição é a chave para evitar essas desvantagens.
Jimmy Hoffa
@tieTYT: Claro que concordo com você, todos sabemos que a otimização prematura é ruim.
Giorgio

Respostas:

9

Tendo sido autor de vários projetos no estilo funcional JavaScript, permita-me compartilhar minha experiência. Vou me concentrar em considerações de alto nível, não em preocupações específicas de implementação. Lembre-se, não há balas de prata, faça o que funciona melhor para sua situação específica.

Estilo funcional versus funcional

Considere o que você deseja obter com essa refatoração funcional. Deseja realmente uma implementação funcional pura ou apenas alguns dos benefícios que um estilo de programação funcional pode trazer, como transparência referencial.

Eu descobri que a última abordagem, o que chamo de estilo funcional, combina bem com JavaScript e geralmente é o que eu quero. Também permite que conceitos seletivos não funcionais sejam usados ​​com cuidado no código onde eles fazem sentido.

Escrever código em um estilo mais puramente funcional também é possível em JavaScript, mas nem sempre é a solução mais apropriada. E o Javascript nunca pode ser puramente funcional.

Interfaces de estilo funcional

No estilo funcional Javascript, a consideração mais importante é a fronteira entre o código funcional e o imperativo. Claramente entender e definir esse limite é essencial.

Organizo meu código em interfaces de estilo funcional. Essas são coleções de lógica que se comportam em um estilo funcional, variando de funções individuais a módulos inteiros. A separação conceitual de interface e implementação é importante, uma interface de estilo funcional pode ser implementada imperativamente, desde que a implementação imperativa não vaze através do limite.

Infelizmente em Javascript, o ônus de impor interfaces de estilo funcional recai inteiramente no desenvolvedor. Além disso, recursos de idioma, como exceções, tornam impossível uma separação realmente limpa.

Como você refatorará uma base de código existente, comece identificando as interfaces lógicas existentes no seu código que podem ser movidas corretamente para um estilo funcional. Uma quantidade surpreendente de código já pode ser de estilo funcional. Bons pontos de partida são pequenas interfaces com poucas dependências imperativas. Sempre que você move o código, as dependências imperativas existentes são eliminadas e mais códigos podem ser movidos.

Continue iterando, trabalhando gradualmente para o exterior para empurrar grandes partes do seu projeto para o lado funcional do limite, até ficar satisfeito com os resultados. Essa abordagem incremental permite testar alterações individuais e facilita a identificação e a aplicação do limite.

Repensar algoritmos, estruturas de dados e design

Soluções imperativas e padrões de design geralmente não fazem sentido no estilo funcional. Especialmente para estruturas de dados, as portas diretas do código imperativo para o estilo funcional podem ser feias e lentas. Um exemplo simples: você prefere copiar todos os elementos de uma lista para adicionar um prefixo ou incluir o elemento na lista existente?

Pesquise e avalie as abordagens funcionais existentes para os problemas. A programação funcional é mais do que apenas uma técnica de programação, é uma maneira diferente de pensar e resolver problemas. Projetos escritos nas linguagens de programação funcional mais tradicionais, como lisp ou haskell, são ótimos recursos nesse sentido.

Compreender o idioma e os recursos internos

Esta é uma parte importante da compreensão do limite imperativo funcional e afetará o design da interface. Entenda quais recursos podem ser usados ​​com segurança no código de estilo funcional e quais não. Tudo a partir do qual os built-in podem gerar exceções e quais, como [].push, modificar objetos, a objetos sendo passados ​​por referência e como as cadeias de protótipos funcionam. Também é necessário um entendimento semelhante de qualquer outra biblioteca que você consome no seu projeto.

Esse conhecimento permite tomar decisões de design informadas, como lidar com entradas e exceções ruins ou o que acontece se uma função de retorno de chamada fizer algo inesperado? Parte disso pode ser aplicada no código, mas outras apenas requerem documentação sobre o uso adequado.

Outro ponto é quão rigorosa sua implementação deve ser. Deseja realmente tentar aplicar o comportamento correto aos erros máximos de pilha de chamadas ou quando o usuário redefine alguma função interna?

Usando conceitos não funcionais, quando apropriado

As abordagens funcionais nem sempre são apropriadas, especialmente em uma linguagem como JavaScript. A solução recursiva pode ser elegante até você começar a obter o máximo de exceções da pilha de chamadas para entradas maiores, portanto, talvez uma abordagem iterativa seja melhor. Da mesma forma, uso herança para organização de código, atribuição em construtores e objetos imutáveis ​​onde eles fazem sentido.

Desde que as interfaces funcionais não sejam violadas, faça o que faz sentido e o que será mais fácil de testar e manter.


Exemplo

Aqui está um exemplo de conversão de código real muito simples em estilo funcional. Não tenho um exemplo grande e muito bom do processo, mas este pequeno aborda vários pontos interessantes.

Esse código cria um teste a partir de uma seqüência de caracteres. Começarei com algum código baseado na seção superior do módulo trie-js build-trie de John Resig . Muitos simplificações / formatação alterações foram feitas e minha intenção não é comentário sobre a qualidade do código original (Há maneiras muito mais claras e mais limpas para construir uma trie, então por que este código c estilo aparece em primeiro lugar no Google é interessante. Aqui está um implementação rápida que usa reduzir ).

Gosto deste exemplo porque não preciso levá-lo a uma verdadeira implementação funcional, apenas a interfaces funcionais. Aqui está o ponto 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}};

Vamos fingir que este é o programa inteiro. Este programa faz duas coisas: converter uma string em uma lista de palavras e criar um trie a partir da lista de palavras. Essas são as interfaces existentes no programa. Aqui está o nosso programa com as interfaces mais claramente expressas:

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

Apenas definindo nossas interfaces, podemos passar _get_wordspara o lado do estilo funcional do limite. Nem replaceou splitmodifica a string original. E nossa interface pública também build_trieé estilo funcional, mesmo que interaja com algum código muito imperativo. De fato, esse é um bom ponto de parada na maioria dos casos. Mais código ficaria avassalador, então deixe-me apresentar algumas outras alterações.

Primeiro, torne o estilo funcional de todas as interfaces. Isso é trivial neste caso, basta _build_trie_from_listretornar um objeto em vez de mutá-lo.

Manipulação de entrada incorreta

Considere o que acontece se chamarmos build_triecom uma matriz de caracteres build_trie(['a', ' ', 'a', 'b', 'c', ' ', 'f']),. O chamador assumiu que isso se comportaria em um estilo funcional, mas, em vez disso, gera uma exceção quando .replaceé chamado na matriz. Este pode ser o comportamento pretendido. Ou podemos fazer verificações explícitas de tipo e garantir que as entradas sejam as esperadas. Mas eu prefiro digitar pato.

Strings são apenas matrizes de caracteres e matrizes são apenas objetos com uma lengthpropriedade e números inteiros não negativos como chaves. Então, se nós reformulado e escreveu novas replacee splitmétodos que opera na matriz como objetos de cordas, eles não tem sequer a ser strings, o nosso código poderia apenas fazer a coisa certa. ( String.prototype.*não funcionará aqui, converte a saída em uma string). A digitação com patos é totalmente separada da programação funcional, mas o ponto mais importante é que as entradas ruins devem sempre ser consideradas.

Redesenho fundamental

Há também considerações mais fundamentais. Suponha que também queremos criar o teste em estilo funcional. No código imperativo, a abordagem era construir a palavra trie de cada vez. Uma porta direta nos faria copiar o trie inteiro toda vez que precisamos fazer uma inserção para evitar a mutação de objetos. Claramente, isso não vai funcionar. Em vez disso, o trie pode ser construído nó por nó, de baixo para cima, para que, uma vez que um nó seja concluído, ele nunca precise ser tocado novamente. Ou outra abordagem inteiramente pode ser melhor, uma pesquisa rápida mostra muitas implementações funcionais existentes de tentativas.

Espero que o exemplo tenha esclarecido um pouco as coisas.


No geral, acho que escrever código de estilo funcional em Javascript é um esforço interessante e interessante. Javascript é uma linguagem funcional surpreendentemente capaz, embora muito detalhada em seu estado padrão.

Boa sorte com seu projeto.

Alguns projetos pessoais escritos em estilo funcional Javascript:

  • parse.js - Combinadores de analisadores
  • Nu - córregos preguiçosos
  • Atum - intérprete Javascript escrito em estilo Javascript funcional.
  • Khepri - Linguagem de programação de protótipo que eu uso para o desenvolvimento funcional de Javascript. Implementado em estilo funcional Javascript.
Matt Bierner
fonte
Muito obrigado por reservar um tempo para escrever isso. Alguns exemplos seriam muito apreciados (e os projetos pessoais não ajudam nisso porque mostram o "depois", não mostram como chegar lá do "antes"). Isso seria mais útil na sua seção intitulada "Interfaces de estilo funcional".
Daniel Kaplan
Eu adicionei um exemplo simples. Encontrar um exemplo maior demonstrando o processo de conversão será difícil, por isso recomendo tentar começar a trabalhar com o código do mundo real o mais rápido possível. É realmente um processo muito específico do projeto e realmente funciona, embora o código o ensine bastante. Talvez o primeiro passe não seja ótimo, mas continue repetindo e aprendendo até ficar satisfeito com o resultado. E existem muitos exemplos excelentes de design funcional on-line para outros idiomas (pessoalmente considero o esquema e o clojure os melhores pontos de partida para muitos tópicos).
Matt Bierner
Eu desejo que esta resposta foram divididos em várias para que eu pudesse upvote cada parte
paul