Implementando interfaces em JavaScript com Implement.js

Neste artigo, vou apresentar o conceito de interfaces e como elas podem ser úteis mesmo em linguagens dinâmicas. Também usarei a biblioteca Implement.js para trazer o conceito para JavaScript e mostrar-lhe como obter algum utilitário extra de interfaces.

O que é uma interface?

O Google define uma interface como “um ponto em que dois sistemas, assuntos, organizações, etc. se encontram e interagem”, e esta definição é válida para as interfaces na programação. No desenvolvimento de software, uma interface é uma estrutura que impõe propriedades específicas em um objeto – na maioria dos idiomas esse objeto é uma classe.

Aqui está um exemplo de uma interface em Java:

No exemplo acima, a interface do Car descreve uma classe que possui dois métodos com nenhum tipo de retorno, ambos levando um único argumento inteiro. Os detalhes da implementação de cada função são deixados para a classe, é por isso que os métodos não possuem corpo. Para garantir que uma classe implemente a interface do Car , usamos a palavra-chave implements :

Simples

Interfaces em JavaScript

Interfaces não são uma coisa no JavaScript, na verdade, de qualquer forma. O JavaScript é um idioma dinâmico, onde os tipos são alterados com tanta frequência que o desenvolvedor nem sequer perceberam, porque essas pessoas argumentam que não há necessidade de uma interface ser adicionada ao padrão ECMAScript com o qual o JavaScript está baseado.

No entanto, o JavaScript cresceu massivamente como uma linguagem de backend na forma de Node.js, e com isso vem diferentes requisitos e uma multidão diferente que pode ter uma opinião diferente. Para adicionar a isso, o idioma está rapidamente se tornando a ferramenta de front-end; Chegou ao ponto em que muitos desenvolvedores escreverão a grande maioria do seu HTML dentro de arquivos .js na forma de JSX.

Assim, à medida que o idioma cresce para assumir mais papéis, é útil garantir que uma das nossas estruturas de dados mais importantes seja o que esperávamos que seja. O JavaScript pode ter a palavra-chave da class , mas, na verdade, essa é apenas uma função de construtor não anunciada, e uma vez chamada é simplesmente um objeto. Os objetos são onipresentes, por isso às vezes é benéfico garantir que eles correspondam a uma forma específica.

Recentemente no trabalho, encontrei um caso em que um desenvolvedor esperava que uma propriedade fosse retornada como parte de uma resposta da API para ser true mas sim "true" , causando um erro. Um erro fácil, e um que também poderia ter sido evitado se tivéssemos uma interface.

Mas espere, há mais!

Interfaces, com poucas modificações menores, podem ser usadas para remodelar objetos. Imagine implementar uma interface “rigorosa”, onde não são permitidas propriedades fora da interface, podemos excluir ou renomear essas propriedades, ou mesmo lançar um erro se as encontrarmos.

Então, agora temos uma interface que nos informará quando faltarmos certas propriedades, mas também quando tivermos propriedades inesperadas, ou se os tipos de propriedades não são o que esperamos. Isso adiciona outras possibilidades, por exemplo, refatorando uma resposta de uma API ao adicionar essa camada extra de segurança em cima do comportamento padrão da interface. Nós também poderíamos usar interfaces em testes de unidade se nós tivermos erros de lançamento.

Implement.js

Implement.js é uma biblioteca que tenta trazer interfaces para JavaScript. A idéia é simples: definir uma interface, definir os tipos de propriedades e usá-la para garantir que um objeto seja o que você espera que seja.

Configuração

Primeiro instale o pacote:

 npm install implement-js

Em seguida, crie um arquivo .js e importe o implement , Interface e type :

Nossa primeira interface

Para criar uma interface, basta chamar Interface e passar em um objeto onde as propriedades são todos os objetos de type , um segundo argumento também pode ser passado com opções para mostrar avisos, lançar erros, excluir ou renomear propriedades, garantir apenas as propriedades da interface são presente, ou para estender uma Interface existente.

Aqui está uma interface que descreve um carro:

Tem uma propriedade de seats que deve ser de número de tipo, uma matriz de passageiros que contém objetos que devem implementar a interface do Passenger , e ela contém uma propriedade beep , que deve ser uma função. O error e as opções strict foram definidas como verdadeiras, o que significa que os erros serão lançados quando uma propriedade da interface estiver faltando e também quando uma propriedade que não esteja na interface for encontrada.

Implementando nossa interface

Agora queremos implementar nossa interface, em um exemplo simples, criaremos um objeto usando um objeto literal e veremos se somos capazes de implementar nossa interface.

Primeiro, criamos um objeto Ford , então tentaremos implementá-lo contra nossa interface de Car :

Como vemos no comentário acima, isso lança um erro. Vejamos nossa interface do Car :

Podemos ver que, enquanto todas as propriedades estão presentes, o modo estrito também é verdadeiro, o que significa que a propriedade adicional fuelType faz com que um erro seja acionado. Além disso, embora tenhamos uma propriedade de passengers , não é uma matriz.

Para implementar corretamente a interface, removemos fuelType e fuelType o valor dos passengers para que seja uma matriz contendo objetos que implementam a interface Passenger :

“Mas o JavaScript não é uma linguagem orientada a objetos!”

É verdade que, embora as interfaces sejam normalmente associadas a linguagens orientadas a objetos, e o JavaScript é uma linguagem multi-paradigma que usa herança prototípica, as interfaces ainda podem ser altamente úteis.

Por exemplo, usando implement-js , podemos facilmente refatorar uma resposta da API, ao mesmo tempo em que não se desviou do que esperamos. Aqui está um exemplo usado em conjunto com redux-thunk :

Primeiro, definimos a interface TwitterUser , que amplia a interface do User , como um objeto com propriedades twitterId e twitterUsername . trim é verdade, o que significa que vamos descartar quaisquer propriedades não descritas na interface do TwitterUser . Uma vez que nossa API retorna as propriedades em um formato não amigável, renomeamos as propriedades do twitter_username e do twitter_id para versões camelcase de si mesmos.

Em seguida, definimos uma ação assíncrona com redux-thunk , a ação desencadeia uma chamada de API e usamos nossa interface do TwitterUser para descartar as propriedades que não queremos e garantir que implementa as propriedades que esperamos, com os tipos corretos. Se você preferir manter os seus criadores de ação puro (er) ou não usar o redux-thunk , você pode querer verificar a interface dentro do twitterService.getUser e retornar o resultado.

Nota: ao estender as opções de uma interface não são herdadas

Os testes unitários também são um local adequado para usar as interfaces:

Em suma

Vimos como as interfaces podem ser úteis em JavaScript: mesmo que seja uma linguagem altamente dinâmica, verificando a forma de um objeto e as suas propriedades são um tipo de dados específico nos dá uma camada extra de segurança, de outra forma, estaríamos perdendo. Ao construir o conceito de interfaces e usar implement-js também conseguimos obter utilidade adicional em cima da segurança adicional.

Texto original em inglês.