Código simples é melhor do que código espertinho

Você sabe por quê programadores não gostam de temas claros? Porque luz atrai insetos!... Insetos... BUGS... Sacou?... Desculpa.

Recentemente, eu criei meu blog (https://andre.pro), e decidi que queria a capacidade de trocar o tema de cores entre claro e escuro. Então, aproveitei a deixa para treinar um pouquinho de Web Components, e criei um pequeno componente para essa tarefa.

O resultado ficou simples e funcional, mas uma coisa estava me incomodando neste trecho de código aqui:

// theme-changer-component.js

export default class ThemeChanger extends HTMLElement {

    ...

    createDOMTree() {
        const lang = this.getAttribute('lang');

        const lightThemeButton = document.createElement('button');
        lightThemeButton.classList.add('light-theme-button');
        lightThemeButton.type = 'button';
        lightThemeButton.innerText = 'A';
        lightThemeButton.title =
            lang === 'pt-BR' ? 'Tema Claro' : 'Light Theme';

        this.lightThemeButton.addEventListener(
            'click',
            this.handleLightThemeButtonClick.bind(this)
        );

        const darkThemeButton = document.createElement('button');
        darkThemeButton.classList.add('dark-theme-button');
        darkThemeButton.type = 'button';
        darkThemeButton.innerText = 'A';
        darkThemeButton.title =
            lang === 'pt-BR' ? 'Tema Escuro' : 'Dark Theme';

        this.darkThemeButton.addEventListener(
            'click',
            this.handleDarkThemeButtonClick.bind(this)
        );

        const lightThemeButtonContainer = document.createElement('div');
        lightThemeButtonContainer.classList.add('icon-container');
        lightThemeButtonContainer.append(lightThemeButton);

        const darkThemeButtonContainer = document.createElement('div');
        darkThemeButtonContainer.classList.add('icon-container');
        darkThemeButtonContainer.append(darkThemeButton);

        const wrapper = document.createElement('div');
        wrapper.ariaHidden = true;
        wrapper.classList.add('wrapper');
        wrapper.append(lightThemeButtonContainer);
        wrapper.append(darkThemeButtonContainer);

        return wrapper;
    }

    ...

}

Eu não conseguia parar de pensar: "Olha a quantidade de vezes que estou criando um elemento manualmente, depois setando atributos manualmente! Tem que haver um jeito mais conciso de fazer isso."

Então achei que era uma boa ideia escrever um pequeno utilitário para criar elementos HTML:

// createElement.js

export const a = new Proxy({}, {
    get: (target, name) => {
        if (!target[name])
            target[name] = (
                properties => createElement(name, properties)
            );
        return target[name];
    },
});

function createElement(name, properties) {
    const element = document.createElement(name);

    for (let key in properties) {
        if (key === 'children') {
            element.append(...properties[key])
            continue;
        }
        element[key] = properties[key];
    }

    return element;
}

Este utilitário permite criar elementos mais ou menos assim:

// theme-changer-component.js

import { a } from './createElement.js';

export default class ThemeChanger extends HTMLElement {

    ...

    createDOMTree() {

        ...

        // antes
        const lightThemeButton = document.createElement('button');
        lightThemeButton.classList.add('light-theme-button');
        lightThemeButton.type = 'button';
        lightThemeButton.title = lang === 'pt-BR' ? 'Tema Claro' : 'Light Theme';
        lightThemeButton.innerText = 'A';

        // depois
        const lightThemeButton = a.button({
            classList: ['light-theme-button'],
            type: 'button',
            title: lang === 'pt-BR' ? 'Tema Claro' : 'Light Theme',
            innerText: 'A',
        });

        ...

    }

    ...

}

Interessante! Esse código parece ter ficado bom e precisei digitar um pouco menos quando comparado à API nativa do DOM.

Uau! Essa ideia foi realmente muito esperta!

Agora, ao reescrever o código aplicando este utilitário, o resultado fica assim:

// theme-changer-component.js

import { a } from './createElement.js';

export default class ThemeChanger extends HTMLElement {

    ...

    createDOMTree() {
        const lang = this.getAttribute('lang');

        this.lightThemeButton = a.button({
            classList: ['light-theme-button'],
            type: 'button',
            innerText: 'A',
            title: lang === 'pt-BR' ? 'Tema Claro' : 'Light Theme',
            onclick: this.handleLightThemeButtonClick.bind(this),
        });

        this.darkThemeButton = a.button({
            classList: ['dark-theme-button'],
            type: 'button',
            innerText: 'A',
            title: lang === 'pt-BR' ? 'Tema Escuro' : 'Dark Theme',
            onclick: this.handleDarkThemeButtonClick.bind(this),
        });

        this.lightThemeButtonContainer = a.div({
            classList: ['icon-container'],
            children: [this.lightThemeButton],
        });

        this.darkThemeButtonContainer = a.div({
            classList: ['icon-container'],
            children: [this.darkThemeButton],
        });

        const wrapper = a.div({
            ariaHidden: true,
            classList: ['wrapper'],
            children: [
                this.lightThemeButtonContainer,
                this.darkThemeButtonContainer
            ],
        });

        return wrapper;
    }

    ...

}

Que vantagens obtemos com essa nova implementação? Bom, consigo pensar somente em 2 coisas:

  1. Digitamos um pouquinho menos de código, mas isso por si só não tem muito valor e a diferença também não é muito grande
  2. O código talvez esteja mais fácil de ler. Mas a forma como o código foi escrito anteriormente era tão difícil de ler a ponto de justificar essa mudança? Acredito que não

Mas e se irmos um pouquinho mais além? Vamos tentar aninhar a criação dos elementos e avaliar estes dois pontos novamente:

// theme-changer-component.js

import { a } from './createElement.js';

export default class ThemeChanger extends HTMLElement {

    ...

    createDOMTree() {
        const lang = this.getAttribute('lang');
    
        const wrapper = a.div({
            ariaHidden: true,
            classList: ['wrapper'],
            children: [
                a.div({
                    classList: ['icon-container'],
                    children: [
                        a.button({
                            classList: ['light-theme-button'],
                            type: 'button',
                            innerText: 'A',
                            title: lang === 'pt-BR' ? 'Tema Claro' : 'Light Theme',
                            onclick: this.handleLightThemeButtonClick.bind(this),
                        }),
                    ],
                }),
                a.div({
                    classList: ['icon-container'],
                    children: [
                        a.button({
                            classList: ['dark-theme-button'],
                            type: 'button',
                            innerText: 'A',
                            title: lang === 'pt-BR' ? 'Tema Escuro' : 'Dark Theme',
                            onclick: this.handleDarkThemeButtonClick.bind(this),
                        }),
                    ],
                }),
            ],
        });
    
        return wrapper;
    }

    ...

}

Agora, o código parece menos legível, além de termos múltiplos níveis de indentação. E mesmo que alguém ache isso mais legível, se resume a um gosto pessoal e, além do mais, ainda precisamos falar sobre as desvantagens do novo código:

  1. Uma pessoa lendo o novo código vai ter uma maior carga cognitiva por ter de entender o funcionamento da nova função a ao invés de ter de lidar somente com a API nativa do DOM
  2. Criar bibliotecas e utilitários dessa forma demanda uma grande quantidade de testes para evitar a introdução de novos bugs. Se eu avançar muito na funcionalidade do utilitário, é mais fácil simplesmente utilizar alguma bilbioteca já pronta, como o React.
  3. E a maior desvantagem de todas: Perdemos quase todo o suporte da IDE: O novo código nos faz perder autocomplete, detecção de erros e toda e qualquer ferramenta que trabalhe analisando as API do DOM. Tudo o que conseguimos da IDE agora são sugestões fora de contexto.

código bom Com a versão original do código a IDE faz sugestões baseadas no contexto

código ruim Com a nova versão do código a IDE faz sugestões sem contexto e que não fazem sentido

Inicialmente, a ideia de criar um utilitário para criar elementos HTML pareceu ser muito boa, mas após um pouco de análise, chegamos à conclusão que a tentativa de economizar código trouxe mais prejuízos do que benefícios.

Qual lição fica disso? Simples: Não tente reinventar a roda e não crie códigos "espertinhos" 😉

Publicação sensacional André! Eu adoto muito isso no TabNews e sempre vou pelo caminho mais simples e que fique mais fácil refatorar para que no futuro o código e todo o sistema me diga qual a melhor hora e forma mais correta de se fazer.

Uau! Essa ideia foi realmente muito esperta!

Isso aqui é um sinal muito claro que há uma alta probabilidade de problemas futuros não esperados surgirem.

Muitas vezes menos é mais. Isso me fez lembrar o Zen do Python. Para ver isso é só digitar import this no interpretador Python. Ele diz: