En esta publicación de blog, presentaré el concepto de interfaces y cómo pueden ser útiles incluso en lenguajes dinámicos. También usaré la biblioteca Implement.js para llevar el concepto a JavaScript y mostrarle cómo obtener alguna utilidad adicional de las interfaces.
Google define una interfaz como "un punto donde dos sistemas, sujetos, organizaciones, etc. se encuentran e interactúan", y esta definición es válida para las interfaces en la programación. En el desarrollo de software, una interfaz es una estructura que impone propiedades específicas en un objeto; en la mayoría de los lenguajes, este objeto es una clase.
Aquí hay un ejemplo de una interfaz en Java:
En el ejemplo anterior, la interfaz Car
describe una clase que tiene dos métodos sin tipo de retorno, los cuales toman un único argumento entero. Los detalles de la implementación de cada función se dejan en manos de la clase, por eso ambos métodos no tienen cuerpo. Para garantizar que una clase implemente la interfaz Car
, usamos la palabra clave implements
:
Las interfaces no son una cosa en JavaScript, no realmente de todos modos. JavaScript es un lenguaje dinámico, en el que los tipos se cambian con tanta frecuencia que es posible que el desarrollador ni siquiera se haya dado cuenta, debido a esto, las personas argumentan que no es necesario agregar una interfaz al estándar ECMAScript en el que se basa JavaScript.
Sin embargo, JavaScript ha crecido enormemente como lenguaje de back-end en la forma de Node.js, y eso conlleva diferentes requisitos y una multitud diferente que puede tener una opinión diferente. Para agregar a esto, el lenguaje se está convirtiendo rápidamente en la herramienta de front-end; ha llegado al punto en que muchos desarrolladores escribirán la gran mayoría de su HTML dentro de archivos .js en forma de JSX.
Entonces, a medida que el lenguaje crece para asumir más roles, es útil asegurarse de que una de nuestras estructuras de datos más cruciales sea lo que esperábamos que fuera. JavaScript puede tener la palabra clave class
, pero en realidad es solo una función constructora no instanciada y, una vez que se la llama, es simplemente un objeto. Los objetos son omnipresentes, por lo que a veces es beneficioso asegurarse de que coincidan con una forma específica.
Recientemente, en el trabajo, encontré un caso en el que un desarrollador esperaba que una propiedad devuelta como parte de una respuesta API fuera true
, pero en cambio obtuvo "true"
, lo que provocó un error. Un error fácil, y que también podría haberse evitado si tuviéramos una interfaz.
Las interfaces, con algunas modificaciones menores, podrían usarse para remodelar objetos. Imagine implementar una interfaz "estricta", donde no se permiten propiedades fuera de la interfaz, podríamos eliminar o cambiar el nombre de estas propiedades, o incluso arrojar un error si las encontramos.
Así que ahora tenemos una interfaz que nos dirá cuando nos faltan ciertas propiedades, pero también cuando tenemos propiedades inesperadas, o si los tipos de propiedades no son lo que esperábamos. Esto agrega otras posibilidades, por ejemplo, refactorizar una respuesta de una API mientras se agrega esa capa adicional de seguridad además del comportamiento estándar de la interfaz. También podríamos usar interfaces en pruebas unitarias si tenemos que arrojar errores.
Implement.js es una biblioteca que intenta traer interfaces a JavaScript. La idea es simple: defina una interfaz, defina los tipos de sus propiedades y utilícela para garantizar que un objeto sea lo que espera que sea.
Primero instale el paquete:
npm instalar implementar-js
A continuación, cree un archivo .js e importe implement
, Interface
y type
:
Para crear una interfaz, simplemente llame a Interface
y pase una cadena como el nombre de su interfaz; no se recomienda, pero si omite el nombre, se generará una identificación única. Esto devuelve una función que acepta un objeto donde las propiedades son todos objetos de type
, también se puede pasar un segundo argumento con opciones para mostrar advertencias, generar errores, eliminar o cambiar el nombre de las propiedades, asegurarse de que solo estén presentes las propiedades de la interfaz o extender una Interface
existente.
Aquí hay una interfaz que describe un automóvil:
Tiene una propiedad de seats
que debe ser de tipo número, una matriz de pasajeros que contiene objetos que deben implementar la interfaz de Passenger
y contiene una propiedad de beep
, que debe ser una función. Las opciones de error
y strict
se han establecido en verdadero, lo que significa que se generarán errores cuando falte una propiedad de la interfaz y también cuando se encuentre una propiedad que no esté en la interfaz.
Ahora queremos implementar nuestra interfaz, en un ejemplo simple, crearemos un objeto usando un objeto literal y veremos si podemos implementar nuestra interfaz.
Primero, creamos un objeto Ford
, luego intentaremos implementarlo en nuestra interfaz Car
:
Como vemos en el comentario anterior, esto arroja un error. Miremos hacia atrás en nuestra interfaz de Car
:
Podemos ver que si bien todas las propiedades están presentes, el modo estricto también es verdadero, lo que significa que la propiedad adicional fuelType
provoca que se arroje un error. Además, aunque tenemos una propiedad de passengers
, no es una matriz.
Para implementar correctamente la interfaz, eliminamos fuelType
y cambiamos el valor de passengers
para que sea una matriz que contenga objetos que implementen la interfaz Passenger
:
Es cierto que, si bien las interfaces generalmente se asocian con lenguajes orientados a objetos y JavaScript es un lenguaje multiparadigma que utiliza la herencia de prototipos, las interfaces aún pueden ser muy útiles.
Por ejemplo, al usar implement-js
, podemos refactorizar fácilmente una respuesta de API mientras nos aseguramos de que no se haya desviado de lo que esperábamos. Aquí hay un ejemplo usado junto con redux-thunk
:
Primero, definimos la interfaz de usuario de TwitterUser
, que amplía la interfaz User
, como un objeto con las propiedades twitterId
y twitterUsername
. trim
es verdadero, lo que significa que descartaremos cualquier propiedad que no se describa en la interfaz de usuario de TwitterUser
. Dado que nuestra API devuelve propiedades en un formato poco amigable, hemos cambiado el nombre de las propiedades de twitter_username
y twitter_id
a versiones camelcase de ellas mismas.
A continuación, definimos una acción asíncrona con redux-thunk
, la acción desencadena una llamada a la API y usamos nuestra interfaz TwitterUser
para descartar las propiedades que no queremos y asegurarnos de que implemente las propiedades que esperamos, con los tipos correctos. Si prefiere mantener puros a los creadores de sus acciones o no usar redux-thunk
, es posible que desee verificar la interfaz dentro twitterService.getUser
y devolver el resultado.
Nota: cuando se amplía una interfaz, las opciones no se heredan
Las pruebas unitarias también son un lugar adecuado para usar interfaces:
Hemos visto cómo las interfaces pueden ser útiles en JavaScript: a pesar de que es un lenguaje muy dinámico, verificar la forma de un objeto y que sus propiedades sean un tipo de datos específico nos brinda una capa adicional de seguridad que de otro modo nos estaríamos perdiendo. Al desarrollar el concepto de interfaces y usar implement-js
, también hemos podido obtener una utilidad adicional además de la seguridad adicional.