Desenvolvimento

29 nov, 2016

Composição é rei

Publicidade

A comunidade JavaScript tem sido inundada por artigos que buscam avançar para a programação funcional ou ao menos para a composição por herança. Por muito tempo, nós tentamos padronizar a herança sem a grande bagunça que vem com a verbosidade da sintaxe do protótipo, e agora que nós temos a palavra-chave padrão “class” no ES2015, as pessoas estão tentando, ainda mais que antes, nos dizer que não precisamos disso. Essas pessoas estão, em grande parte, corretas.

É claro, elas não podem estar erradas. A fonte da verdade para todos os desenvolvedores orientados a objetos é  Design Patterns: Elements of Reusable Object-Oriented Software, de “Gang of Four”, que, em si, diz preferir a composição à herança. Parece que a maioria das pessoas não entende isso. Elas são ensinadas sobre herança e então querem fazer tudo com ela, mas isso não é poderoso nem escalável.

De volta ao JavaScript, que pode tirar lições do livro de padrões de design, mas é uma linguagem tão diferente da utilizada no livro. Além de utilizar protótipos em vez de classes verdadeiras, ela também é preenchida com muitas porções de funcionalidades de programação funcional. Eu não vou dizer “não utilize a nova palavra-chave “class” ou herança”, ou nada do tipo. Eu só quero que você entenda como utilizar a melhor ferramenta para o trabalho. Eu quero dizer que fazer tudo com programação funcional pode ser uma ótima maneira de programar, no entanto, não é o conceito mais simples (ao menos não se você mergulhar de cabeça), então, por favor, faça somente o que fizer sentido para você.

Tendo dito isso, eu gostaria de mostrar alguns ótimos exemplos de composição para te ajudar a entender como utilizá-la e mostrar onde ela pode ser útil.

Composição de função

Vamos iniciar com a composição de funções, por que não? Vamos dizer que você tem a seguinte função simples.

function addAndLog (a, b) {
    let result = a + b;
    console.log(result);
    return result;
}

Isso parece bastante simples, mas pode ser dividido em duas operações: recuperar o resultado de uma operação e fazer o logging do resultado. Isso significa que você se só quer obter o resultado da operação, sem logar nela, você está sem sorte, então vamos dividir a operação em uma função separada:

function add (a, b) {
    return a + b;
}
function addAndLog (a, b) {
    let result = add(a, b);
    console.log(result);
    return result;
}

Ótimo, agora a operação de adição pode ser utilizada em qualquer lugar separado do logging, mas aquela “addAndLog” ainda é ligada ao log da operação “add”, em vez de ser generalizada para a utilização do resultado em qualquer operação. Então, vamos dividir a funcionalidade de logging para sua nova função única.

function log (value) {
    console.log(value);
    return value;
}

Eu adicionei a declaração “return” no fim do código para que nós possamos adicionar, por exemplo:

add(1,2); // returns 3... but we want to log the result too
// so we wrap it:
log(add(1,2)); // logs 3 AND returns 3 so the result can still be used elsewhere

Puxa, uma das maiores razões para não podermos apenas utilizar o “console.log” nesse caso é porque ele simplesmente retorna “undefined”. De qualquer maneira, as chamadas aninhadas das funções estão entre as coisas que eu gosto menos das práticas de programação funcional, porque é essencialmente ler da direita para a esquerda, que é agora como os ocidentais tendem a ler.

Então, uma das coisas que nós podemos fazer é converter o “log” em uma função de ordem superior. Uma função de ordem superior é aquela que retorna uma função (definição simples). A nova função, que nós vamos chamar de “logWrapper” será capaz de aceitar uma função como argumento e, então, retornar uma nova função que chama a função que você passou, e ainda faz o logging e retorna o resultado.

function logWrapper (operation) {
    return function (...args) {
        return log(operation(...args));
    }
}

Então, agora nós podemos criar nossa velha função “addAndLog” assim:

var addAndLog = logWrapper(add);
addAndLog(1,3); // logs 3 and returns 3

Ou nós podemos combiná-la com qualquer outra operação, então ela é boa e genérica.

Isso é a composição! Você criou flexibilidade ao permitir que a função de log fosse composta por qualquer operação mais a funcionalidade de logging. É claro, agora “logWrapper” está preso à funcionalidade de logging. Existem muitas maneiras de generalizar isso ainda mais, fazendo uma função que pode receber qualquer número de funções e compô-las juntas para você, mas eu acho que você pegou a ideia. Existem muitos outros tutoriais por aí sobre encadeamento ou composição. Eu só queria dar um exemplo.

Composição de componente ou visualização

Eu poderia falar somente  sobre a composição de objetos comuns, mas todos já fizeram isso. Em vez disso, vamos falar sobre a composição de  visualizações ou elementos (como os componentes React). Por que visualizações e componentes? Principalmente porque todos estão utilizando algum tido de framework com visualizações e/ou componentes neles, então isso pode ser mais relevante.

Composição de um componente React

Vamos começar com o React, apesar do fato de que eu nunca tenha escrito sobre o React. Um exemplo comum utilizado pelos “Mixins” são os modais ou overlays, como você quiser chamá-los. Eu acho que modais podem ser tratados melhor com composição:

const Modal = React.createClass({
    render() {
        return (
            <div class="modal">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
                    <h3>this.props.title</h3>
                </div>
                <div class="modal-body">
                    {this.props.children}
                </div>
            </div>
        );
    },
    ... // all the life-cycle stuff
});

Desde que você esteja utilizando “props.children”, você pode aninhar sua visualização diretamente no componente “Modal”:

ReactDOM.render(<Modal> <MyView/> </Modal>, mountNode);

Ou você pode utilizar o que é chamado de “componente de ordem superior”, que é uma função que retorna um componente que envolve seu componente para você:

function modalify(WrappedComponent) {
    return React.createClass({
        render: function() {
            return (
                <div class="modal">
                    <div class="modal-header">
                        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
                        <h3>this.props.title</h3>
                    </div>
                    <div class="modal-body">
                        <WrappedComponent {...this.props} {...this.state} />
                    </div>
                </div>
            )
        },
        ... // all the life-cycle stuff
  });
}

Agora, se você quer que seu componente esteja dentro de um “Modal”, você pode passar seu componente para uma chamada de “modalify”, e você receberá um componente modal que mostrará seu componente.

ReactDOM.render(modalify(<MyView/>), mountNode);

modalify” utiliza a sintaxe de propagação do JSX para passar todas as propriedades e estados automaticamente para baixo, apesar de ser mais útil utilizar alguma coisa como a função “omit” de Lodash para separar propriedades modais específicas. O interessante sobre esse padrão de componente de ordem superior é você poder acessar os métodos de ciclo de vida do componente envolvido, e qualquer outra funcionalidade que o modal tenha acesso. Por exemplo, se o componente envolvido for um formulário, você pode querer fechar o modal assim que o formulário for submetida com sucesso, então você pode passar o método closeModal (não mostrado no exemplo de código acima), para o WrappedComponent como uma propriedade para que ele possa chamar closeModal quando o formulário for submetido.

Você pode, tecnicamente, passar o acesso a esses componentes para o MyView no primeiro exemplo de componentes aninhados assim:

const Modal = React.createClass({
    render() {
        return (
            <div class="modal">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
                    <h3>this.props.title</h3>
                </div>
                <div class="modal-body">
                    {
                        React.Children.map(this.props.children, child => {
                            return React.cloneElement(child, {
                                closeModal: this.closeModal,
                                ...
                            });
                        })
                    }
                </div>
            </div>
        );
    },
    ... // all the life-cycle stuff
});

Em vez de somente utilizar {this.props.children}, nós utilizamos React.Children.map e React.cloneElement para aumentar a visibilidade das filhas em relação à funcionalidade Modal.

Se você quiser mais exemplos de como o React pode ser composto em vez de utilizar herança ou “mixins”, veja o artigo “Mixins Considered Harmful” (em inglês), por Dan Abramov. Esse artigo, na verdade, foi o que me deu inspiração para escrever este texto, pois ele falou primeiro sobre o React, e eu queria ir além e demonstrá-lo com Backbone também, que é o que farei agora.

Composição de visualização Backbone

Você pode fazer a mesma coisa utilizando o Backbone que fizemos com o React, com a exceção de o Backbone não ter a sintaxe JSX ou uma maneira mais “limpa” de passar as views das filhas, mas ainda podemos fazer as mesmas coisas com “options”.

const ModalView = Backbone.view.extend({
    attributes: {
        class: 'modal'
    },
    init: function() {
        _.extend(this.options.childView, {
            closeModal: this.closeModal,
            ...
        });
    },
    render: function() {
        // Ignore the fact that I'm not using a template. Please!
        this.$el.html(
            '<div class="modal-header">' +
                '<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>' +
                '<h3>' + this.options.title +</h3>' +
            '</div>' +
            '<div class="modal-body"></div>'
        )
        .find('.modal-body').append(this.options.childView.render());
        return this.$el; // Assume this practice for all `render` methods
    },
    ... // all the life-cycle stuff
});

Então, você pode utilizá-lo assim:

let myView = new MyView();
let modal = new ModalView({childView: myView});
$('body').append(modal.render());

Você também pode utilizar um padrão de “view de ordem superior”, como nós fizemos com o React, mas, pessoalmente, eu acredito que as views aninhadas fazem mais sentido nesse caso. Os padrões view de ordem superior e  componente de ordem superior normalmente são mais úteis se você estiver adicionando funcionalidades sem encapsular o componente em mais HTML, como utilizando o método “moveTo(x,y)”, que anima o posicionamento do conteúdo encapsulado.

function makeAnimatable(WrappedView) {
    return Backbone.View.extend({
        initialize: function(options) {
            this.wrapped = new WrappedView(options);
        },
        moveTo: function(x, y) {
            this.wrapped.$el.animate({
                top: y,
                left: x
            });
        },
        render: function() {
            return this.wrapped.render();
        }
    });
}

Ele faz isso. Você provavelmente vai querer encontrar uma maneira de delegar todas as chamadas de método para “this.wrapped”. Provavelmente, uma maneira simples de fazer isso seria criar uma função que possa ser chamada de qualquer ponto, em vez de utilizar o método “moveTo(x,y)”:

function moveTo(view, x, y) {
    view.$el.animate({
        top: y,
        left: x
    });
}

Mas isso seria fácil demais ;).

Essa é uma das vantagens de ter uma linguagem que é tanto orientada a objetos (não baseada em classes, no senso comum, mas ainda assim orientada a objetos) e funcional. Funções isoladas podem reduzir significantemente a complexidade versus tentar fazer as coisas com encapsulamentos ou heranças ou mixins etc.

Conclusão

Isso é tudo por hoje. Eu espero que vocês tenham aprendido algo útil: composição, ou mesmo somente que funções planas, como exibidas no fim, podem ser utilizadas para desemaranhar alguns dos padrões mais aninhados da orientação a objetos. Somente se lembre: composição em vez de herança… e mantenha simples, não importa por qual caminho você vá. Deus abençoe e feliz codificação.

***

Joe Zim faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://www.joezimjs.com/javascript/composition-is-king/.