IronWoods.es

Desarrollo web

Blog / JavaScript / Programación orientada a objetos en JavaScript: instancias y herencia

Las clases permiten encapsular propiedades y métodos (datos y comportamientos). Si la instancia de una clase puede acceder a sus propiedades y métodos las clases que extienden la misma tienen acceso a estos y, además, pueden implementar los suyos propios o sobrescribir lo heredado.

Instancias de la clase

Tengo una clase, que encapsula los métodos que permiten trabajar con el , donde la implementación del constructor permite crear instancias para trabajar con el localStorage o sessionStorage del navegador según se requiera.


    class Storage // class 
    {
        storageObject;

        
        /**
        * Set the storage mechanism in use
        *
        * @param string storageMechanisms: local | session
        */ 
        constructor(storageMechanisms = 'local') {
            this.storageObject = (storageMechanisms
                && storageMechanisms.trim() === 'session')
                ? window.sessionStorage
                : window.localStorage;
        }

        clear() // void 
        {
            this.storageObject.clear();
        }

        get(key) // mixed 
        {
            return this.storageObject.getItem(key);
        }

        has(key) // bool 
        {
            return (this.storageObject.getItem(key));
        }

        set(key, value) // void 
        {
            this.storageObject.setItem(key.trim(), value);
        }

        unset(key) // void 
        {
            this.storageObject.removeItem(key);
        }
    }

Podemos crear una instancia para trabajar con el LocalStorage, sin usar parámetros (mecanismo por defecto):


    const localStorage = new Storage();

A través de la instancia 'localStorage' tenemos acceso a la propiedad 'storageObject' que en este caso contiene un objeto de tipo 'window.localStorage' y a los métodos de la clase, por ejemplo, vamos a guardar y recuperar un número imprimiéndolo en la consola:


    const exampleKey = 'this_is_a_key';

    localStorage.set(exampleKey, 756);
    console.log(localStorage.set(exampleKey)); // 756 

    // Ahora, podemos borrar TODO el contenido del LocalStorage con: 
    localStorage.clear();

Y no hay más. Si quisiéramos usar modificadores de visibilidad, interfaces, etc. tendríamos a usar .

Herencia

Declaro e instancio una clase para guardar preferencias de usuario en local que extiende a Storage para poder usar sus métodos:


    class UserPreferences extends Storage // class 
    {

        constructor(storageMechanisms = 'local') // construct 
        {
            super(storageMechanisms);
        }
    }
    const userPreferences = new UserPreferences();

Puedo repetir el código del ejemplo anterior, usando ahora la instancia de UserPreferences, y funciona de la misma forma:


    // const exampleKey = 'this_is_a_key'; // ya declarada 

    userPreferences.set(exampleKey, 756);
    console.log(userPreferences.set(exampleKey)); // 756 

    // Ahora, podemos borrar TODO el contenido del LocalStorage con: 
    userPreferences.clear();

La clase UserPreferences no aporta nada a nivel de comportamiento, pero hace el código más legible: si vemos la llamada userPreferences.clear(); podemos intuir que se están borrando preferencias de usuario. Pero para que UserPreferences sea funcional deberíamos, al menos, sobrescribir el método clear para que borre sólo los datos considerados "preferencias de usuario" y NO todo el contenido del storage en uso.

¿Cómo saber que datos corresponden a las preferncias del usuario?

Definiendo previamente las claves que pueden usarse. Para esto usaré una clase en la que almacenaré las preferencias de usuario por defecto, las hago accesibles en UserPreferences, añado también un método que las inicializa y la implementación de clear. Así tendré:


    class DefaultUserPreferences // class 
    {
        COLOR_SCHEME = 'dark'; // 'dark' | 'light' 
        ROWS_BY_PAGE = 25
    }

    class UserPreferences extends Storage // class 
    {
        arrPreferenceKeys;
        objDefaultUserPreferences;

        constructor(storageMechanisms = 'local') // construct 
        {
            super(storageMechanisms);

            this.objDefaultUserPreferences = new DefaultUserPreferences();
            this.arrPreferenceKeys
                = Object.getOwnPropertyNames(this.objDefaultUserPreferences);

            this.init();
        }

        // overwrite 
        clear() // void 
        {
            for (const key of this.arrPreferenceKeys) {
                this.unset(key);
            }
        }

        init() // void 
        {
            this.arrPreferenceKeys.forEach(key => {
                if (!super.has(key)
                    || super.get(key) === 'undefined') {
                    super.set(key, this.objDefaultUserPreferences[key]);
                }
            });
        }
    }
    const userPreferences = new UserPreferences();

     // Para acceder al valor de la llave COLOR_SCHEME: 
    console.log(new DefaultUserPreferences().COLOR_SCHEME); // 'dark' 
    console.log(userPreferences.objDefaultUserPreferences['COLOR_SCHEME']); // 'dark' 

He guardado en propiedades de la clase UserPreferences el objeto que contiene los valores por defecto junto con sus claves y un array con las mismas, lo que requería para implementar el método clear

TIP: JavaScript permite acceder a las propiedades de una instancia con Object.getOwnPropertyNames(new ClassName).

Mejorando UserPreferences

Voy a almacenar en el storage un par de valores:


    userPreferences.set('xxx', 123);
    userPreferences.set('COLOR_SCHEME', 123);

Ahora en el storage tendremos, al menos, esos dos pares clave-valor y se plantean 2 problemas. Primero, con la instancia userPreferences tenemos acceso a todas las acciones sobre el contenido del storage, sin restricciones, salvo al usar el método clear que ya hemos acotado y segundo, que para la clave almacenada en la propiedad COLOR_SCHEME de DefaultUserPreferences esperábamos que se guarde un string: 'dark' o 'light', pero podemos guardar cualquier valor.

La falta de restricciones supone una serie de errores potenciales que puede paliarse sobreescribiendo otros métodos heredados de Storage. El caso más grave es aquel en que para una clave de nuestra "feature" podemos introducir un valor inesperado.

Voy a reescribir el método set para impedir introducir en el storage pares clave-valor ajenos a los que debe gestionar la instanciade UserPreferences. Ademas, veremos también como en caso de sobrescribir un método como se accede al original heredado de la clase padre, con la palabra super que ya aparece en el constructor y en el método init donde lo he usado explícitamente para protegerlo en caso de que los métodos de la clase has, get y el propio set, como es el caso, sean sobrescritos.

Añado una nueva clase con las claves que uso para guardar preferencias y los valores admisibles:


    class UserPreferencesKeyAndValues // class 
    {
        COLOR_SCHEME = [
            'dark',
            'light',
        ];
        ROWS_BY_PAGE = [10, 25, 50, 100];
    }

En la clase UserPreferences añado una nueva propiedad donde guardo una instancia de UserPreferencesKeyAndValues y la implementación del método set:


    class UserPreferences extends Storage // class 
    {
        arrPreferenceKeys;
        objDefaultUserPreferences;
        objUserPreferencesKeyAndValues;


        constructor(storageMechanisms = 'local') // construct 
        {
            super(storageMechanisms);

            this.objDefaultUserPreferences = new DefaultUserPreferences();
            this.objUserPreferencesKeyAndValues = new UserPreferencesKeyAndValues();

            this.arrPreferenceKeys
                = Object.getOwnPropertyNames(this.objDefaultUserPreferences);

            this.init();
        }

        ...

        // overwrite 
        set(key, value) // void 
        {
            if (this.arrPreferenceKeys.includes(key)
                && this.objUserPreferencesKeyAndValues[key]
                && this.objUserPreferencesKeyAndValues[key].includes(value)) {
                super.set(key, value);
            }
        }
    }
    const userPreferences = new UserPreferences();

Si volvemos a intentar guardar en el storage valores erróneos, podemos ver el resultado de las nuevas restricciones:


    userPreferences.set('xxx', 123);
    userPreferences.set('COLOR_SCHEME', 123);
    console.log(userPreferences.get('xxx')); // undefined 
    console.log(userPreferences.get(userPreferences.keys.COLOR_SCHEME)); // 'dark' 

El código completo se encuentra aquí.



2-9-2024