Encapsulación de datos en JavaScript: getters y setters
Cuando se construyen aplicaciones JavaScript más grandes, pronto surge la necesidad de dividirlas en módulos vinculados por contratos claros. Cuando se trata del mantenimiento a largo plazo, es necesario proporcionar una forma de mantener la compatibilidad ante cambios en la interfaz. Para ello, los lenguajes orientados a objetos se basan en la encapsulación o en la información que oculta los detalles de implementación a los usuarios de un trozo de código, de modo que pueda cambiar sin afectar a los clientes.
En este post hablaremos de la encapsulación de datos en JavaScript. Más concretamente, hablaremos de encapsular propiedades detrás de getters y setters.
Algunos conceptos
JavaScript fue diseñado teniendo como base el principio de tipado de pato:
“Cuando veo un pájaro que camina como un pato y nada como un pato y grazna como un pato, llamo a ese pájaro pato.”
En otras palabras, en su núcleo, JavaScript es un lenguaje orientado a objetos, pero a diferencia de otros lenguajes orientados a objetos como C# y Java, en JavaScript las clases son ciudadanos de segunda clase y las interfaces no existen. Los objetos no tienen tipos y, por lo tanto, es necesario escribir el código para establecer una interfaz con los objetos que tienen ciertas propiedades y métodos. Los contratos entre módulos se basan en un conjunto de métodos y propiedades que se supone que están presentes en los objetos intercambiados.
La encapsulación es fundamental en la programación orientada a objetos. Significa que un objeto dado debe ser capaz de ocultar los detalles de su funcionamiento interno, de modo que otros objetos que interactúan con él no dependen de los detalles de su implementación, sino de una interfaz de alto nivel acordada. Esto simplifica la vida de los desarrolladores, tanto del lado del proveedor como del lado de los usuarios, ya que ambos saben que los detalles de la implementación pueden cambiar sin romper los objetos del cliente.
Por qué exponer datos en una interfaz es una mala idea
En JavaScript un objeto es simplemente un diccionario de nombres de propiedades mapeados en valores, cuando el valor de una propiedad es una función, lo llamamos método. La interfaz entre métodos es el conjunto de propiedades que el código cliente espera encontrar en un objeto.
Si un objeto expone sólo métodos, es fácil hacer que la interfaz evolucione con el tiempo. Un método puede comprobar sus parámetros y reaccionar en consecuencia. También es posible crear nuevos métodos que proporcionen nuevas características. Y, apoyar a los antiguos que adaptan el comportamiento antiguo sobre el nuevo. Con las propiedades no es posible hacer eso.
En el caso de las propiedades, mantener una interfaz estable no es fácil. Ya que, por defecto, el código del cliente puede hacer referencia a propiedades no existentes de un objeto. Escribir en una propiedad no existente la creará. Leerla devolverá undefined. El código cliente no podrá detectar rápidamente si está utilizando propiedades obsoletas. Peor aún, el error puede propagarse a otras partes del código, haciendo que la detección del problema sea más difícil.
La API estándar de JavaScript tiene métodos que pueden ser útiles para evitar estos problemas: es posible congelar o sellar un objeto, para que los nombres de las propiedades no soportadas no puedan ser utilizados.
Getter/Setters para salvar el día
Ocultar los detalles de la implementación del método es fácil.
Los setters/setters se han introducido oficialmente en el lenguaje en ECMAScript 5.1 (ECMA-262). Actualmente están soportados en todos los principales navegadores de escritorio y móviles.
La idea básica es que se añade una sintaxis para definir las propiedades accesorias como métodos, en lugar de simples propiedades de datos. Un getter se define con la palabra clave get seguida de una función con el nombre de la propiedad, que no toma argumentos y devuelve el valor de la propiedad. Un setter se define con la palabra clave set seguida de una función con el nombre de la propiedad que toma el nuevo valor de la propiedad como parámetro.
El siguiente ejemplo ilustra un getter y un setter utilizados para definir una propiedad accessor llamada prop:
var obj = { v: 0, get prop() { return this.v; }, set prop(newValue) { this.v = newValue; }};console.log(obj.prop);obj.prop = 42;console.log(obj.prop);
Output:
042
También se puede crear una propiedad de sólo lectura definiendo un getter sin setter:
var obj = { get prop() { return -1; },};console.log(obj.prop);obj.prop = 42;console.log(obj.prop);
Output:
-1-1
Una propiedad puede entonces ser sustituida por un par de métodos que en caso de que la implementación de una clase cambie, puedan reaccionar a ello, y adaptar el comportamiento de la clase. En el peor de los casos, puede lanzar una excepción para indicar al usuario que la propiedad está obsoleta y no debe seguir utilizándose.
Lectura adicional
Espero que hayas aprendido algo nuevo sobre el diseño de aplicaciones evolutivas complejas en JavaScript en este post. En esta sección, te daré un par de enlaces a links que contienen más información.
Artículos introductorios
Esta sección trae un par de artículos cortos que presentan los conceptos y proporcionan una visión general de las APIs relacionadas:
- Property getters and setters.
- JavaScript Getters and Setters
Artículos más avanzados
Estos artículos hablan de temas relacionados más avanzados como los problemas que traen los getters y setters, y cómo resolverlos.
- Por qué getters/setters es una mala idea en JavaScript
- Capsulación de datos en JavaScript
Documentación de referencia
Por último, para dominar realmente el tema, los enlaces a la documentación de mozilla de las APIs relacionadas:
- get y set permiten definir getters y setters. Más allá de lo que explicamos aquí. Hay otras características interesantes como el soporte de nombres de propiedades generados dinámicamente, y el uso de getters y setters para simular el establecimiento/consecución de valores en un array
- seal y freeze permiten controlar qué propiedades de un objeto son modificables o no y cómo. Se puede utilizar para evitar que los clientes utilicen partes obsoletas de la API de un objeto.
- defineProperty permite definir getters y setters y al mismo tiempo tener más control sobre cómo estas propiedades son vistas por los clientes y cómo son modificables.