Um devaneio sobre Classes, Objetos, Funções e organização em JavaScript
Published: 2020-02-09 16:16:37

Considere os dois trechos de código abaixo:

```javascript
// trecho 1
const user = new User('Gustavo', 25);
user.name();

// trecho 2
const user = new User('Gustavo', 25);
name(user);
```

Qual versão você prefere?


*Atenção: Este artigo contém, na sua grande maioria, as minhas opiniões pessoais sobre estilos de desenvolvimento de programas de forma sustentável.*

---

### Classes em JavaScript

JavaScript não possui suporte a classes. Ok, existe a palavra chave `class` adicionada no ES2015, que serve justamente para definir classes. Caso você seja novo no mundo JavaScript, esse exemplo pode ser esclarecedor:


```javascript
class User {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
}

let user = new User('Gustavo', 25);
console.log(user.name); // Gustavo
```


No trecho acima, é criada uma classe chamada `User` com um método construtor que recebe dois parâmetros, o primeiro será atribuído a `name` e o segundo, a `age`. Em seguida é criado uma instância da classe `User` e o atributo público `name` é impresso no console.


Porém, é neste momento que começam os problemas. Toda e qualquer classe em JavaScript possuirá todos os atributos e métodos públicos, violando os conceitos de encapsulamento.


E se quisermos que o atributo age seja privado? Ou seja, que este atributo seja acessível somente dentro da classe User. Talvez algo do tipo:

```javascript
class User {
    let __age;
    constructor(name, age) {
        this.name = name;
        age = age;
    }
}
```


Infelizmente essa sintaxe não é válida. Porém no ES2019 foi introduzido o indicador de atributo privado e a sintaxe fica bem parecida com a nossa tentativa inválida:


```javascript
class User {
    #age = null;
    constructor(name, age) {
        this.name = name;
        age = age;
    }
}
```


Se tentarmos acessar o atributo `age`, receberemos como resultado `undefined`:


```javascript
let user = new User('Gustavo', 25);
console.log(user.name); // Gustavo
console.log(user.age); // undefined
```


De fato, usando a api de reflexão podemos ter a certeza de que o atributo age não é exposto ao mundo externo:

```javascript
console.log(Reflect.has(user, 'name')) // true
console.log(Reflect.has(user, 'age')) // false
```


Legal, porém o suporte de classes em JavaScript é curto. Herança múltipla não é suportada, sobrecarga não existe, polimorfismo é simulado através da natureza dinâmica dos tipos do JavaScript. Ou seja, JavaScript não é uma linguagem que suporte Orientação a Objetos.


A minha crítica ao uso de OOP em JavaScript parte principalmente do pressuposto das pessoas tentarem usar conceitos de OOP em JavaScript sem conhecer muito bem como classes funcionam por de baixo dos panos, culminando em softwares de difícil manutenção e suscetíveis a problemas de herança que surgem após algum tempo de desenvolvimento.

---

### Uma tentativa Funcional

Não pense que JavaScript é uma linguagem funcional, pois não é. JavaScript está num limbo onde a maioria dos principais atrativos da linguagem não dão suporte completo aos paradigmas existentes. JavaScript quase implementa seu próprio paradigma de programação, um verdadeiro monstro de Frankenstein.


Linguagens que implementam o paradigma funcional possuem características únicas. Essas linguagens se apoiam em um sistema de tipos robusto e várias otimizações de performance, já que conceitos como recurção e imutabilidade caminham de mãos dadas ao paradigma funcional.


JavaScript não tem suporte a recursão de cauda, ou seja, o ato de, quando uma função chamar a si mesma e a chamada for a última instrução do corpo da função e o contexto atual não depender do próximo contexto, em vez de ser empilhado mais uma chamada, a função simplesmente *“pula”* para o início. Portanto é necessário o uso de iteração em JavaScript.


Também não existe um meio não muito verboso de criar dados imutáveis. Existe a palavra chave `const` que cria uma referência imutável a uma área de memória, porém não cria objetos imutáveis, ou seja:

```javascript
const u = {
    name: 'Gustavo'
}

u.name = 'Ciclano';
console.log(u); // { name: 'Ciclano' }
```


Porém, mesmo com todas as deficiências do JavaScript, essa linguagem é incrível. É excepcionalmente performática em lidar com objetos e criar funções. Operar com funções em JavaScript é uma tarefa prazerosa, é possível compor funções, misturar objetos e mesmo assim, se seguirmos as regras corretas, no final do dia ter um software extremamente estável e determinístico.

---

### Uma mistura inusitada

Particularmente, prefiro definir um tipo por módulo (arquivo), junto de funções que operam sobre este tipo. É como criar um protocolo de acesso de informação dentro de um módulo. Mas como podemos criar tipos em JavaScript?


Classes ou funções construtoras são formas de criar novos tipos. Para criar um tipo `User` poderíamos tomar dois caminhos:

```javascript
// função construtora
function User(name, age) {
    this.name = () => name;
    this.age = () => age;
}

// Classe
class User {
    constructor(name, age) {
        this.name = () => name;
        this.age = () => age;
    }
}
```


Em ambos os casos, a instanciação de novos objetos do tipo `User` ocorre da mesma forma:

```javascript
let u = new User('Gustavo', 25);
u.name(); // Gustavo
u.age(); // 25
```


Porém, em qualquer lugar é possível acessar as funções `name` e `age`. Existem casos onde pode ser necessário transitar esta instância de usuário por bibliotecas que não são totalmente confiáveis. Também existem casos onde simplesmente não queremos deixar estas informações acessíveis pelo motivo de seguirmos boas práticas de programação.


A forma que encontrei para contornar este problema de forma particularmente “elegante” é usar símbolos e funções construtoras. Porque símbolos? Símbolos são pedaços de informação única, não são copiáveis, tampouco acessíveis caso usados como parâmetros de objetos e é neste caso que usarei os símbolos do JavaScript.


Também prefiro funções construtoras em vez de classes, pelos motivos já citados no início do artigo.


Ok, mas como é possível organizar o software usando funções e símbolos? Primeiro vamos criar um arquivo chamado `user.js` e definir dois símbolos:

```javascript
// user.js
const nameAttr = Symbol('user/name');
const ageAttr = Symbol('user/age');
```

Agora vamos definir uma função construtora que define o tipo `User`:

```javascript
// user.js
const nameAttr = Symbol('user/name');
const ageAttr = Symbol('user/age');
export default function User(name, age) {
    this[nameAttr] = () => name;
    this[ageAttr] = () => age;
}
```


Há um porém no código acima: não queremos criar uma instância de User caso o nome e a idade não sejam atribuídos. Não devem existir instâncias de usuários sem nome ou sem idade. Para isso, podemos definir algumas asserções simples de existência (não vou me preocupar com os tipos dos parâmetros neste artigo):

```javascript
// user.mjs

const nameAttr = Symbol('user/name');
const ageAttr = Symbol('user/age');

export default function User(name, age) {
    if (!name) {
        throw new Error('Cannot create an User without name.');
    }
    if (!age) {
        throw new Error('Cannot create an User without age.');
    }
    this[nameAttr] = () => name;
    this[ageAttr] = () => age;
}
```
O que há de especial na função `User` acima? Vamos criar uma instância e inspecionar:

```javascript
let u = new User('Gustavo', 25);
// e agora?
```


Não é possível acessar as funções que retornam o nome e a idade. É simplesmente impossível acessar estas funções sem ter acesso aos símbolos que definem as propriedades que, então, definem estas funções. Foi criado um protocolo de acesso onde somente as funções que têm acesso aos símbolos conseguem invocar as funções definidas no tipo `User`, a sacada aqui é que os símbolos não são exportados do módulo.


Vamos então definir as funções que operam sobre instâncias de usuários:

```javascript
function getName(user) {
    return user[nameAttr]();
}

function getAge(user) {
    return user[ageAttr]();
}
```


Agora é possível acessar o nome e idade de um usuário usando estas funções:

```javascript
getName(u); // Gustavo
getAge(u); // 25
```


Legal, mas estas funções somente operam sobre instâncias de usuários. Para prevenir que estas funções sejam invocadas sob a informação incorreta, podemos definir uma asserção de tipo em cada função *getter*:

```javascript
function getName(user) {
    if (user instanceof User) {
        return user[nameAttr]();
    }

    throw new TypeError('Provided data is not of the type User');
}
```


E invocar como:

```javascript
getName(u); // Gustavo
getName({ name: 'Gustavo' });
// Uncaught TypeError: Provided data is not of the type User
```


Legal. Mas colocar esta validação de tipo dentro de cada função viola os princípios do **DRY**. Podemos usar os poderes da abstração e criar uma função que “decora” outras funções e serve como guardiã do argumento. Caso o argumento seja uma informação diferente de uma instância de `User` a função decorada não deve ser invocada. Podemos definir uma função chamada `withGuardUser` que vai lidar com o tipo do argumento das funções:

```javascript
function withGuardUser(fn) {
    return function withUser(user) {
        if (user instanceof User) {
            return fn(user);
        }

        throw new TypeError(
            'Provided data is not of the type User'
        );
    }
}
```

Agora podemos criar duas novas funções e exportá-las do nosso módulo `user.js` que vão operar de forma segura sobre instâncias de usuários:

```javascript
export const name = withGuardUser(getName);
export const age = withGuardUser(getAge);
```


Em um outro arquivo, por exemplo, `user1.js`, importe a função construtora `User` e as funções de projeção `name` e `age`:

```javascript
// user1.js
import User, { name, age } from './user.mjs';

const user = new User('Gustavo', 25);
console.log(name(user)); // Gustavo
console.log(age(user)); // 25
```


Aviso: para usar a sintaxe de ES Modules no Node.js 13, não esqueça de criar um arquivo `package.json` no diretório onde os seus arquivos estão armazenados contendo o parâmetro `"type"` cujo valor é `"module"`. Veja mais sobre isso em https://nodejs.org/api/esm.html.

---

### Um exemplo

Considere que é necessário construir uma interface de histórico de registro de ações de um determinado usuário em um determinado sistema, e gostaríamos de mostrar este registro da seguinte forma:


```
[Sun, 09 Feb 2020 04:07:11 GMT | GUSTAVO]: Action
[Sun, 09 Feb 2020 04:12:35 GMT | GUSTAVO]: Action
[Sun, 09 Feb 2020 04:54:03 GMT | GUSTAVO]: Action
...
```


Podemos começar definindo uma função que recebe um texto e retorna este texto com todas as letras maiúsculas:

```javascript
const upper = (text) => text.toUpperCase();
```


Agora precisamos definir uma função que retorna aquele texto de data de ação para um determinado carimbo de tempo:

```javascript
const utcString = (timestamp) =>
    (new Date(timestamp)).toUTCString();
```


Certo, agora precisamos gerar esse carimbo de tempo, podemos criar uma função chamada `now` que faz exatamente isso:

```javascript
const now = () => Date.now();
```


A parte crucial é montar a string de prefixo, então podemos definir uma função chamada `actionLoggerPrefix`, definida como:

```javascript
const actionLoggerPrefix = (userName) => {
    const ts = now();
    const date = utcString(ts);
    return `[${[date, userName].join(' | ')}]`;
}
```


Usando uma função de pipeline de dados, por exemplo, a função `pipe`:

```javascript
const pipe = (...fns) => (...args) =>
    fns.reduce((acc, fn) => fn(acc), ...args);
```


Podemos então criar a função `prefix` que usa as funções `name` , `upper` e `actionLoggerPrefix` para criar uma função que recebe uma instância de um usuário do tipo User e retorna um texto com a formatação adequada:

```javascript
const prefix = pipe(name, upper, actionLoggerPrefix);
console.log(prefix(user)); // [Sun, 09 Feb 2020 04:24:59 GMT | GUSTAVO]
```

Veja, criamos uma função complexa que é a junção de três funções bem simples, bem fáceis de dar manutenção e independentes de contexto. A função `name` opera somente sobre instâncias do tipo `User`, portanto as chances das funções serem invocadas sobre o tipo de dado incorreto são muito menores.

Confira o código final do módulo que opera sobre o tipo `User`:

```javascript
// user.mjs

const nameAttr = Symbol('user/name');
const ageAttr = Symbol('user/age');

export default function User(name, age) {
    if (!name) {
        throw new TypeError('Cannot create an User without name.');
    }
    if (!age) {
        throw new TypeError('Cannot create an User without age');
    }

    this[nameAttr] = () => name;
    this[ageAttr] = () => age;
}


function withGuardUser(fn) {
    return function withUser(user) {
        if (user instanceof User) {
            return fn(user);
        }

        throw new TypeError(
            'Provided data is not of the type User'
        );
    }
}


function getName(user) {
    return user[nameAttr]();
}


function getAge(user) {
    return user[ageAttr]();
}


export const name = withGuardUser(getName);
export const age = withGuardUser(getAge);
```


E esse é o segundo arquivo contendo as outras funções criadas para solucionar parte do problema de listagem de registro de ações:

```javascript
// user1.js

import User, { name, age } from './user.mjs';

const user = new User('Gustavo', 25);


const now = () => Date.now();

const utcString = (timestamp) =>
    (new Date(timestamp)).toUTCString();

const upper = (text) => text.toUpperCase();

const actionLoggerPrefix = (userName) => {
    const ts = now();
    const date = utcString(ts);

    return `[${[date, userName].join(' | ')}]`;
}

const pipe = (...fns) => (...args) =>
    fns.reduce((acc, fn) => fn(acc), ...args);


const prefix = pipe(name, upper, actionLoggerPrefix);

console.log(prefix(user));
// [Sun, 09 Feb 2020 14:50:02 GMT | GUSTAVO]
```

---

### Considerações finais e pessoais

No final do dia, seu software deve funcionar, ser testado, ser estável e não exigir mais energia cognitiva que o necessário para entender como as coisas funcionam. Eu acredito que adotar uma abordagem mais declarativa no dia a dia é de suma importância para desenvolver softwares estáveis e fáceis de testar. Também inibe más práticas de programação. É muito difícil violar o DRY escrevendo funções pequenas.


Mas lembre-se de que as pessoas que trabalham contigo precisam estar dispostas a abrir a mente e adotar abstrações que tornam o código mais declarativo. Isso é de uma importância inimaginável.


Portanto o meu conselho que fica é que, se você que leu este texto até o fim é uma pessoa que se considera pronta para adotar os conceitos funcionais e declarativos no dia a dia, não esqueça de conversar com seu time, saiba a opinião das pessoas quanto a programação funcional, descubra se são pessoas abertas a mudanças, talvez introduzir algumas regras no *linter* usado nos projetos da empresa. Forçar que todos desenvolvam de uma maneira que não estão acostumados, pode mais trazer problemas do que soluções.