1

De POO a SOLID explicado con Pokémon - Los cuatro principios de POO

HERO
<h6>Guía para entender los cuatro principios de POO.</h6>

JavaScript - Los cuatro principios de POO

En un post anterior hablamos sobre el paradigma de POO aplicándolo a JavaScript, de esta manera dimos el primer paso para poder comprender SOLID.

Con anterior hablamos sobre el paradigma de POO aplicándolo a JavaScript, de esta manera dimos el primer paso para poder comprender SOLID. Si te perdiste el post anterior puedes consultarlo en el menú de arriba.

Retomando nuestra aventura Pokémon procederemos a entender los principios de POO, para entrar en materia nos preguntaremos lo siguiente.

🔖 Nota: Todos los ejemplos de código utilizados están basados en JavaScript usando las especificaciones más actuales hasta el momento.


🤔 ¿Cuáles son los principios de POO?

Todo lo que es POO es posible simplificarlos en cuatro grandes principios que permiten fundamentar dicho paradigma.

Fig. 1: Los cuatro principios de POO.

📕 Encapsulamiento

Su propósito es que nadie se meta donde no le llaman, cada objeto es responsable de su propia información, estados los cuales solo pueden ser modificados por sus propios métodos, por lo cual sus atributos internos no tienen que ser accesibles desde fuera.

🔖 Nota: Cuando un atributo de un objeto no es accesible desde fuera en POO esto se conoce como privacidad.

Utilizando como base la clase Pokémon que creamos en un POST anterior procederemos a implementar el principio de encapsulamiento.

classPokemon {
    /*
         Gracias a las nuevas especificaciones de JavaScript podemos
         utilizar el "#" para indicar que un atributo o método es privado,
         evitando el acceso desde fuera de la clase.
     */
     #name = ""; 
     #type = ""
     #evolutions = [];

    /*
         Desde el constructor podemos inicializar los atributos
         de la clase Pokémon.
     */constructor(name, type, evolutions) {
         // Se utilizan los métodos propios de la clase // para modificar sus atributos. this.#name = name;
         this.#type = type;
         this.#evolutions = evolutions;
     }

    /*
         En JavaScript podemos utilizar la propiedad llamada "set"
         la cual permite definir un método que solo recibe un parámetro
         y nunca tiene retorno, en este ejemplo nos permite modificar 
         los atributos de la clase.
     */
     setname(value) {
        this.#name = value;
     }

    settype(value) {
        this.#type = value;
    }

    setevolutions(value) {
        this.#evolutions = value;
    }

    /*
        En JavaScript podemos utilizar la propiedad llamada "get" 
        la cual nos permite definir un método que no recibe parámetros
        y siempre tiene retorno, en este ejemplo nos permite consultar 
        los atributos de la clase.
     */
     getname() {
        returnthis.#name;
    }

    gettype() {
        returnthis.#type;
    }

    getevolutions() {
        returnthis.#evolutions;
    }

    /*
        Listado de métodos propios de la clase Pokémon
    */

    attack() {
        return`${this.name}, esta atacando`;
    }

    evolve(evolution = 0) {
        const EVOLVE = this.evolutions[evolution] || "";
        let message = "No puedo evolucionar";
        if (EVOLVE) {
            message = `${this.name} esta evolucionando a ${EVOLVE}`;
            this.name = EVOLVE;
        }
        return message;
     }
}
// Creando instancia para charmanderconst Charmander = new Pokemon("charmander", "Fire", ["Charmeleon", "Charizar"]);
/*
    Consultamos el nuevo nombre del Pokémon utilizando uno
    de los métodos declarados con get.
*/console.log(`Mi nombre es ${Charmander.name}`);
// Salida actual// Mi nuevo nombre es Charmander/*
    Cambiaremos el nombre del Pokémon utilizando uno 
    de los métodos declarados con set.
*/
Charmander.name = "Charmander";

/*
    Consultamos el nuevo nombre del Pokémon utilizando uno
    de los métodos declarados con get.
*/console.log(`Mi nuevo nombre es ${Charmander.name}`);

// Nueva salida// Mi nuevo nombre es Charmander/*
    ❌ Si tratáramos de consultar alguno de los atributos
    sin utilizar los métodos de la Clase conseguiríamos
    un error.
*/console.log(`Esto mostrara un error ${Charmander.#name}`);
// Error// Private field '#name' must be declared in an enclosing class

¡Genial! nuestra clase Pokémon está utilizando privacidad para que algunos de sus atributos no san accesibles desde fuera y así es cómo funciona el principio de encapsulamiento😱.

🔖 Nota: Todas las métodos y atributos que no tienen el # son por defecto públicos, esto quiere decir que pueden ser consultados desde afuera de la clase.


📗 Abstracción

Hace pensar que los objetos son similares a cajas negras, ya que sabremos cómo interactuar con ellos, pero no conocemos su comportamiento interno, esto trae como consecuencia la capacidad de modificar el comportamiento de un objeto sin afectar a quienes lo utilicen.

Siguiendo con el tema de Pokémon, imaginemos el funcionamiento de una Pokédex el cual está abstraído del usuario, pues basta solamente con saber cómo interactuar con él para realizar ciertas acciones, pero no tenemos ni la remota idea de cómo funciona internamente.

/*
    Como primer ejemplo los datos se conseguiran de un 
    "JSON" el cual funciona como una Base de Datos.
*/import Data from"./infoPokemons.json";classPokedex{
    /*
        Se crea el método para conseguir la información de un 
        "Pokémon".
    */
    getInfo(name) {
        // buscamos la data referente al Pokémonconst DATA = Data[name] || null;
        // Decidimos que mensaje mostrarconst MESSAGE = DATA || `El pokémon ${name} no existe`;
        // Pintamos el mensajeconsole.log(MESSAGE);
    }
}

// Creamos la instancia de la clase "Pokedex"const POKEDEX = new Pokedex();

// Solicitamos la data de charmander
POKEDEX.getInfo("charmander");

// Salida// { name: "Charmander", type: "Fire", evolutions: ["Charmeleon", "Chrarizar"]};//==============================================================///*
    Utilizando el código anterior realizaremos un cambio con el
    que los datos se conseguirán desde un "API" pero al implementar 
    estos cambios no tenemos que afectar la forma en la que se esta 
    interactuando con la clase Pokedex 🤯.
*/classPokedex{
    /*
        Se utiliza el mismo método getInfo se recibe los mismos parametros
        pero su logica interna cambio.
    */
    getInfo(name) {
        // Se realizan cambios para conseguir la data desde la API.
        fetch(`https://workshop-mongo.herokuapp.com/pokemon/name/${name}`)
            .then((data) => data.json())
            .then((data) => {
                // buscamos la data referente al Pokémonconst [DATA = null] = Data;
                // Decidimos que mensaje mostrarconst MESSAGE = DATA || `El pokémon ${name} no existe`;
                // Pintamos el mensajeconsole.log(MESSAGE);
            });
    }
}

// Creamos la instancia de la clase "Pokedex"const POKEDEX = new Pokedex();

// Solicitamos la data de charmander
POKEDEX.getInfo("charmander");

// Salida// { name: "Charmander", type: "Fire", evolutions: ["Charmeleon", "Chrarizar"]};

¡Genial! ya contamos con una nueva clase llamada Pokédex con la que podemos consultar la información de un Pokémon, como podemos apreciar en el ejemplo sin importar el origen de la data el método getInfo siempre muestra la información solicitada, por lo que la manera de interactuar con dicho método no cambia solo la clase padre es la que conoce sus cambios y es así como funciona el principio de Abstracción.


📘 Herencia

Es la capacidad que permite crear objetos a partir de otros ya existentes, los métodos y atributos del objeto padre (Super Clase) pasan a ser parte de los objetos hijos (Sub-Clase) los cuales son creados a partir de la Super Clase, la herencia se basa en la reutilización de código.

Imaginemos que se necesita crear un nuevo objeto que permita definir a Pokémons de Tipo Fuego, pero este nuevo objeto tiene que contar con las características de la clase Pokémon.

// referencia a la clase pokemon https://gist.github.com/konami12/2359d276454cdcb81d291597a042e375/*
    Para poder implementar el principio de "Herencia" se utiliza
    la palabra reservada "extends"  seguida del nombre del
    objeto padre (Super Class) el cual seria "Pokemon".
*/
classTypeFireextendsPokemon{

    constructor(name, evolutions) {
        /*
            Cuando la Super Clase utiliza el un método "constructor"
            es necesario utilizar la palabra reservada "super" la cual
            permite invocar al constructor de la Super Clase.
        */super(name, "fire", evolutions);
    }

    /*
        Creamos un método propio de la clase "TypeFire", internamente
        utilizaremos algunos de los métodos heredados por la Super Clase
    */
    message() {
        // utilizamos uno de los mêtodos get de la clase padre.return`Hola soy ${this.name} y soy de tipo fuego`;
    }
}

/*
    Creamos la instancia de la clase "TypeFire", en este caso ya no mandamos
    el parametro "type" ya que por defecto la clase define.
*/const CHARMANDER = newTypeFire("charmander", ["Charmeleon", "Charizar"]);
// Invocamosel método message y Mostramos el resultadoconsole.log(CHARMANDER.message());
// Salida: Hola soy charmander y soy de tipo fuego/*
    Modificamos el nombre del pokémon utilizando uno de los métodos
    heredados de la Super Clase.
*/
CHARMANDER.name = "CHARMANDER";

// Invocamosel nuevamente método message y Mostramos el resultadoconsole.log(CHARMANDER.message());
// Salida: Hola soy CHARMANDER y soy de tipo fuego

¡Genial! la clase TypeFire entra a juego nos permite crear Pokémons de tipo fuego y al mismo tiempo cuenta con todos los métodos y propiedades de la clase Pokémon, además estamos reutilizando código😱 y así es cómo funciona el principio de Herencia.

🔖 Nota: Recordemos que la clase Pokémon cuenta con atributos privados los cuales no pueden ser consultados directamente por la clase TypeFire pero puede consultarlos gracias a los métodos que tienen acceso a dichos a tributos.


📙 Polimorfismo

Es la capacidad que tiene un objeto de presentar diferentes comportamientos al momento de realizar una acción, el polimorfismo se presenta cuando se aplica el principio de Herencia.

Utilizaremos nuevamente la clase TypeFire ya que con anterioridad le aplicamos el principio de Herencia, la clase Pokémon tiene un método attack con un comportamiento establecido, pero al utilizarlo dentro de la clase TypeFire su comportamiento tiene que cambiar.

// referencia a la clase pokemon // https://gist.github.com/konami12/2359d276454cdcb81d291597a042e375/*
    Para poder implementar el polimorfismo iniciaremos 
    aplicando el principio de herencia.
*/
classTypeFireextendsPokemon{

    constructor(name, evolutions) {
        super(name, "fire", evolutions);
    }

    message() {
        // Utilizamos uno de los métodos de la clase padre return`Hola soy ${this.name} y soy de tipo fuego`;
    }
}

/*
    Creamos la instancia de la clase "TypeFire"
    pero en este ejemplo no modificamos el mêtodo attack
*/const Charmander = newTypeFire("Charmander", ["Charmeleon", "Charizar"]);
/*
    Invocamos el mêtodo attack pero aun 
    tendra el comportamiento definido por
    la clase padre.
*/const ATTACK_FIRE = Charmander.attack();

console.log(ATTACK_FIRE);
// Salida : // Charmander, esta atacando./*
    Para poder implementar el polimorfismo iniciaremos 
    aplicando el principio de herencia.
*/
classTypeFireextendsPokemon{

    constructor(name, evolutions) {
        super(name, "fire", evolutions);
    }

    message() {
        // Utilizamos uno de los métodos de la clase padre return`Hola soy ${this.name} y soy de tipo fuego`;
    }

    /*
        En este caso sobre escribimos el mêtodo attack
        para definir un nuevo comportamiento.
    */
    attack() {
        return`${this.name}, ataca con AlientoIgneo`;
    }
}

/*
    Creamos la instancia de la clase "TypeFire"
*/const Charmander = newTypeFire("Charmander", ["Charmeleon", "Charizar"]);
/*
    Invocamos el mêtodo attack pero en esta
    ocación conseguiremos una repuesta diferente.
*/const ATTACK_FIRE = Charmander.attack();

console.log(ATTACK_FIRE);
// Salida  Charmander, ataca con Aliento Igneo.

¡Genial! la clase TypeFire define un nuevo comportamiento para el método attack proveniente de la clase Pokémon, de esta manera las dos clases comparten el método y cada una define un comportamiento diferente y así es como funciona el Polimorfismo.

🔖 Nota: Al igual que la herencia no podemos tener accesos a métodos y atributos privados.


🤔 Conclusiones

Aprendimos los cuatro principios que le dan fundamento al Paradigma de POO, sabes cómo aplicarlos en nuestros desarrollos, es importante saber que JavaScript no cuenta con una implementación al 100 del paradigma de Paradigma de POO pero parece que busca dar el paso.

Hemos dado un paso más para poder entender los cinco principios de SOLID ya que hasta este punto podríamos

AteriorDe POO a SOLID explicado con Pokémons - JavaScript - El paradigma de POO

SiguienteDe POO a SOLID explicado con Pokémon - Los cinco principios de SOLID

Escribe tu comentario
+ 2