JS: Abstracción de datos
Teoría: Invariantes
La abstracción nos permite no preocuparnos por los detalles de implementación y centrarnos en su uso. Además, si es necesario, siempre se puede reescribir la implementación de la abstracción sin temor a romper el código que la utiliza. Pero hay otra razón importante por la que se debe utilizar la abstracción: el cumplimiento de los invariantes.
Un invariante en programación es una expresión lógica que define la consistencia de un estado (conjunto de datos).
Veamos un ejemplo. Cuando describimos el constructor y los selectores para los números racionales, implícitamente asumimos el cumplimiento de los siguientes invariantes:
Al pasar el numerador y el denominador al constructor de un número racional, esperamos obtener los mismos valores si aplicamos los selectores a ese número racional. Así es como se define la corrección del funcionamiento de esta abstracción. ¡Este código prácticamente es una prueba!
Los invariantes existen para cualquier operación. Y a veces son bastante complicados. Por ejemplo, se pueden comparar números racionales entre sí, pero no de manera directa, porque las mismas fracciones se pueden representar de diferentes formas: 1/2 y 2/4. El código que no tiene en cuenta este hecho funciona incorrectamente:
La tarea de llevar una fracción a su forma normal se llama normalización. Esto incluye varias operaciones, como simplificar la fracción, determinar el signo, llevar el signo al numerador. Se puede implementar la normalización de diferentes maneras. La más obvia es realizarla durante la creación de la fracción, dentro de la función makeRational(). Otra opción es realizar la normalización cuando se accede a través de las funciones getNumer() y getDenom(). Esta última opción tiene la desventaja de que se calcula la forma normal en cada llamada. Esto se puede evitar utilizando la técnica de memoización.
Teniendo en cuenta las nuevas consideraciones, queda claro que el invariante que relaciona el constructor y los selectores necesita modificarse. Las funciones getNumer() y getDenom() deben devolver los valores después de la normalización (si la fracción ya está normalizada, serán los mismos valores).
La abstracción no solo oculta la implementación, sino que también se encarga de cumplir los invariantes. Cualquier manipulación directa de los datos, sin utilizar la abstracción, puede romper fácilmente los invariantes que se garantizaban mediante lógica adicional en el constructor o los selectores. Por lo tanto, es importante utilizar el código tal como fue concebido por los autores.
Al observar los ejemplos anteriores, surge una pregunta lógica. ¿Es posible evitar eludir la abstracción? Globalmente, sí. Este enfoque se conoce como ocultación de datos (data hiding). Por lo general, para garantizar la ocultación, los lenguajes utilizan una sintaxis especial. Sin embargo, se puede lograr la protección de los datos incluso sin herramientas especiales, solo con funciones de orden superior. Este método se basa en la creación de abstracciones mediante funciones anónimas, closures y el envío de mensajes (más detalles en SICP).