Portada POO

Programación Orientada a Objetos | Guía práctica | Parte #2

Última actualización:

Retomamos esta serie sobre POO (me encantan sus siglas 💩). Si acabas de aterrizar aquí, puedes ver la parte #1 antes.

Herencia de clases (subclases)

Hemos visto en la parte #1 qué es una clase (en inglés, class), cómo crear una y cómo crear diferentes instances de ella. Pero podemos hacer mucho más con las clases. Por ejemplo, crear una subclase que herede los métodos y propiedades de una clase superior. En nuestro caso, la "clase superior" sería User.

En base a esa clase, podríamos crear otra (estrictamente, una subclase) que heredase los métodos de User, pero que también tuviese otros métodos únicamente reservados para esa subclase. Lo que haría esa subclase sería ampliar (en inglés, extend) las características de la clase superior.

Esa subclase podría ser un usuario con otras cualidades, por ejemplo, un usuario-administrador (Admin). Podría tener un método para eliminar usuarios. 

Con este sistema respetamos el principio DRY, ya que no re-escribimos la clase User para poder heredar sus mismas características. Además, es mucho más re-utilizable

1. Para crear la subclase, usamos las keywords "class" y "extendsy especificamos cuál es la clase de la que queremos heredar propiedades y métodos. Si creamos un nuevo Admin object, verás que en la consola aparecen las mismas propiedades y métodos que para el User. Aunque ahora tenemos dos User objects y un Admin object.

class Admin extends User {

}

const userOne = new User('gandalf', 'gandalf@thewhite.com');
const userTwo = new User('frodo', 'frodo@bolson.com');
const userThree = new Admin('sauron', 'sauron@mordor.com');

console.log(userOne, userTwo, userThree);

Genial, ahora ya podemos definir nuestros propios métodos en la subclase. 

2. Creamos un método llamado deleteUser, que usaremos para eliminar usuarios. 

3. Creamos un array de usuarios que contenga a nuestros tres usuarios como elementos del array. Podemos hacer un console.log del array para ver que la consola muestra lo que esperamos.

class Admin extends User {
    deleteUsers() {

    }
}

const userOne = new User('gandalf', 'gandalf@thewhite.com');
const userTwo = new User('frodo', 'frodo@bolson.com');
const userThree = new Admin('sauron', 'sauron@mordor.com');

console.log(userOne, userTwo, userThree);

let users = [userOne, userTwo, userThree];

console.log(users); // > (3) [User, User, Admin]

4. El objetivo es utilizar el userThree, que es un Admin, para llamar a su método deleteUser, y que éste sea capaz de tomar como argumento un elemento del array, para posteriormente eliminarlo.

userThree.deleteUsers(userTwo);

Para eso, definimos el comportamiento del método deleteUser.

5. Usamos el método filterCon este método podemos barrer los elementos del array y filtrar sólo los que cumplan los requisitos que establezcamos.

A deleteUser le pasamos un parámetro (user), porque espera algo que eliminar.

Como el método filter filtra y recopila todo elemento que cumpla nuestra condición, lo que queremos es que  filtre (y se quede) con los elementos que la cumplan. La condición que vamos a poner es que el username  que reciba la función deleteUser como argumento sea distinto a cualquier username de cualquier elemento del array users.

👨‍🔧 Así, JS hará lo siguiente:

  •  empezará a barrer el array. El primer elemento es userOne. Comprueba si userOne.username es igual que userTwo.username (gandalf frodo).  No son iguales ⏭ se cumple nuestra condición ⏭ JS le aplica el filtro al elemento y lo guarda.
  • JS pasa al siguiente elemento (userTwo). Comprueba si userTwo.username es igual que userTwo.username (frodo frodo)  ¡lo son! no se cumple nuestra condición JS lo descarta.
  • JS pasa al siguiente elemento (userThree). Comprueba si userThree.username es igual que userTwo.username (sauron frodo) ⏭ No son iguales ⏭ se cumple nuestra condición⏭ JS le aplica el filtro al elemento y lo guarda.

No olvides guardar el resultado del método filter (lo guardamos sobrescribiendo el valor del array users), porque filter es un método no-destructivo. 😌

6. Para comprobar que ha funcionado, hacemos un console.log del array users, y eso nos debería devolver un array con todos los elementos del array menos userTwo. 

class Admin extends User {
    deleteUsers(user) {
        users = users.filter(u => {
            return user.username !== u.username;
        });
    }
}

const userOne = new User('gandalf', 'gandalf@thewhite.com');
const userTwo = new User('frodo', 'frodo@bolson.com');
const userThree = new Admin('sauron', 'sauron@mordor.com');

console.log(userOne, userTwo, userThree);

let users = [userOne, userTwo, userThree];
console.log(users); // > (3) [User, User, Admin]

userThree.deleteUsers(userTwo);
console.log(users);

Y ¡tachán! El userTwo (el pobre Frodo) ha sido eliminado. 🙈

7. Como tenemos solamente un return, podemos simplificar el código.

class Admin extends User {
    deleteUsers(user) {
        users = users.filter(u => user.username !== u.username);
    }
}

Hasta aquí la forma de añadir métodos extra a una subclase. Para añadir propiedades extra, la cosa cambia un poco. Vamos a por ello en la siguiente sección. 💪

Cómo añadir propiedades a una subclase: super()

Para añadir propiedades extra a una subclase (que sólo queremos que tenga esa subclase y no su clase superior), la subclase debe contener su propio constructor. Vamos a añadir una nueva propiedad que se refiera a un rol del Admin, por ejemplo, "director general" o "el jefe supremo". 🤪

const userThree = new Admin('sauron', 'sauron@mordor.com', 'supreme master');

Cuando creamos un constructor en una subclase, debemos especificar qué propiedades queremos que la subclase herede de su clase superior. De lo contrario, el constructor de la subclase simplemente reemplazará al constructor de la clase, no pudiendo acceder a las propiedades de la clase. Y eso no es lo que queremos. 🙄

1. Así que lo primero que debemos hacer es pasarle al constructor de la subclase (por parámetro) las propiedades que queremos heredar de la clase, más la nueva propiedad que queremos crear.

2. No basta con incluir las propiedades de la clase superior, sino que también debemos llamarlas. Para ello, usamos la keyword "super", que no deja de ser otra función, y le pasamos como argumentos las propiedades que queremos heredar. 

3. Añadimos una nueva propiedad a la subclase, que será el rol o title de un Admin.

class Admin extends User {
    constructor(username, email, title) {
        super(username, email);
        this.title = title;
    }
    deleteUsers(user) {
        users = users.filter(u => user.username !== u.username);
    }
}

const userOne = new User('gandalf', 'gandalf@thewhite.com');
const userTwo = new User('frodo', 'frodo@bolson.com');
const userThree = new Admin('sauron', 'sauron@mordor.com', 'supreme master');

console.log(userOne, userTwo, userThree);

// let users = [userOne, userTwo, userThree];
// console.log(users); // > (3) [User, User, Admin]

// userThree.deleteUsers(userTwo);
// console.log(users);

¡Y ya lo tenemos! En la consola podemos ver ahora a un Admin con las mismas propiedades que un User, más una adicional. 👍

|   Elements    Console    Sources    Performance    Network    ...

Admin
 email: "sauron@mordor.com"
 score: 0
 title: "supreme master"
 username: "sauron"
 __proto__: User

El mecanismo detrás de los constructores

Hemos mencionado anteriormente que la sintaxis relativa a las clases de JS no es más que sintatic sugar, porque JS no viene por defecto con la sintaxis de las clases ya construida, al contrario de lo que ocurre con otros lenguajes. En lugar de eso, utiliza un modelo o prototipo (en inglés, prototype) para emular ese comportamiento. 

En la práctica, usamos la moderna sintaxis de las clases para construirlas, pero la realidad es que podríamos usar también el prototype sobre el que están construidas. Vamos a probarlo. 😮

En una clase, la constructor function es la responsable de construir el objeto con sus propiedades.

1. Comentamos todo el código anterior y trabajamos con este:

class User {
    constructor(username, email) {
        this.username = username;
        this.email = email;
    }
}

const userOne = new User('gandalf', 'gandalf@thewhite.com');
const userTwo = new User('frodo', 'frodo@bolson.com');

2. Comentamos la clase User y creamos un código con el mismo resultado, pero en su forma original, sin sintatic sugar, siguiendo la sintaxis de un prototype. Esto lo hacemos definiendo una función común. La llamamos igual que la clase, User. También la escribimos en mayúscula, porque esa es la convención que nos indica que estamos ante una constructor function. 🧐

function User() {
    
}

// class User {
//     constructor(username, email) {
//         this.username = username;
//         this.email = email;
//     }
// }

3. A esta función podemos pasarle los mismos parámetros (username, email). Para crear propiedades, usamos la keyword "thisigual que cuando construimos una clase, porque this se refiere al objeto vacío que crea la constructor function. 

function User(username, email) {
    this.username = username;
    this.email = email;
}

Si ahora hacemos un console.log de userOne userTwo, verás que se imprime exactamente lo mismo que si hubiésemos utilizado la sintaxis de las clases.

console.log(userOne, userTwo);

4. Esto en cuanto a las propiedades. En cuanto a los métodos, también podríamos añadirlos dentro de la constructor function.

function User(username, email) {
    this.username = username;
    this.email = email;
    this.login = function() {
        console.log(`${this.username} is logged in`);
    }
}
userOne.login(); // gandalf is logged in

Sin embargo, esta no es la mejor manera de crear métodos. La manera más correcta sería usando el prototype model. Vamos a verlo a continuación. 👩‍💻

El Prototype model

Cómo acceder y crear métodos en el prototype model

La mejor manera de añadir métodos a un objeto es a través de su prototypeA lo largo de esta serie hemos visto por encima lo que era un prototype. Es ese elemento que aparece en la consola cada vez que creamos un objeto y lo imprimimos.

const user = {
    username: 'Gandalf',
    email: 'gandalf@thewhite.com',
    login() {
 }
};

|   Elements    Console    Sources    Performance    Network    ...

> {username: "Gandalf", email: "gandalf@thewhite.com", login: ƒ}
    email: "gandalf@thewhite.com"
    login: ƒ login()
    username: "Gandalf"
    __proto__:

Lo único que sabemos por el momento es que en la propiedad "__proto__" se almacenan lo métodos del objeto al que está vinculado. Pero esto tiene mucha más chicha 😏. Vamos a indagar en el asunto.

En la consola, creamos un array de números como este:

const numbers = [1, 2, 4, 5, 8];

Si ahora escribimos el nombre del array en la consola, sabemos que nos devuelve sus propiedades (solo tiene una, length) y sus métodos (dentro de "__proto__"). Si quisiéramos usar uno de esos métodos, no tendríamos que hacer:

numbers.__proto__.concat();

sino simplemente:

numbers.concat();

Porque JS sube esos métodos de nivel hasta la raíz de nuestro array para que podamos usarlos directamente sobre el objeto.

Fijémonos ahora en la función User. Tenemos hechos dos console.log sobre userOne userTwo. Si expandimos cualquiera en la consola, verás que el método login no está dentro de "__proto__", sino fuera, junto a las propiedades.

|   Elements    Console    Sources    Performance    Network    ...

User {username: "frodo", email: "frodo@bolson.com", login: ƒ}
    email: "frodo@bolson.com"
    login: ƒ ()
    username: "frodo"
    __proto__: Object

Pero, si en lugar de utilizar una constructor function "a la antigua" usamos una clase, JS colocaría los métodos dentro de "__proto__" automáticamente sin que nosotros tuviésemos que hacer nada. Entonces, ¿cómo deberíamos incluir nuestros métodos en la propiedad "__proto__" si no usamos clases? 🤔 Vamos a desgranar un poco qué es un prototype para dar respuesta a eso.

Debemos tener presente que cada objeto en JS tiene un prototype, tanto objetos contenidos en JS de base (arrays, Date, etc) como objetos construidos por nosotros, como el User object.

El prototype de un objeto es como una caja de herramientas que contiene todos los métodos relacionados con ese objeto, a los que puede acceder.

Por ejemplo, un Date object tiene un prototype distinto a un array object.

Date prototype
getDay()
getFullYear()
etc...
Array prototype
filter()
forEach()
etc...

Esto significa que el prototype del Date object siempre será el mismo, porque los métodos de un Date object nunca cambian. Lo mismo para el prototype de un array y para cualquier prototype de cualquier objeto que ya venga con JS por defecto.

Así, cuando creamos arrays, las propiedades se guardan en la instance de ese array, pero no ocurre lo mismo con los métodos. Los métodos se guardan solo una vez, y lo hacen dentro del prototype al que pertenezcan. Así que la propiedad "__proto__" es únicamente una referencia que tiene JS para saber de qué objeto estamos hablando y qué prototype debe poner a nuestra disposición para que utilicemos sus métodos

esquema prototipo arrays

Por eso en la consola, "__proto__" se ve ligeramente transparente. Porque los métodos no están realmente ahí, sólo estamos estamos apuntando al prototype que los contiene. 🏹

Todo esto también es válido para los objetos que creamos nosotros, como el User object. Por tanto, este objeto también tiene su prototype, solo que por el momento no contiene ningún método porque no hemos creado ninguno. Recuerda que el método login lo hemos escrito en el constructor con el resto de propiedades, por tanto, no está dentro del prototype.

👉 El guardar los métodos en un prototype tiene dos ventajas fundamentales:

1. nuestro código es más eficiente, porque guardamos los métodos en un solo sitio, en lugar de en cada una de las instances de un objeto.

2. es útil para trabajar con herencia de prototipos (en inglés, prototypal inheritance). Veremos esto más adelante, no sufras. 😌

Con estos conceptos sobre la mesa, vamos a intentar añadir métodos al prototype de nuestro User object. 

👀 Otra manera de ver el prototype de un objeto es escribiendo el constructor de ese objeto y aplicarle la propiedad prototypePor ejemplo, para un objeto de tipo array sería:

|   Elements    Console    Sources    Performance    Network    ...

Array.prototype

    [constructor: ƒ, concat: ƒ, copyWithin: ƒ, fill: ƒ, find: ƒ, …]
    concat: ƒ concat()
    constructor: ƒ Array()
    copyWithin: ƒ copyWithin()
    entries: ƒ entries()
    every: ƒ every()
    fill: ƒ fill()
    filter: ƒ filter()
    find: ƒ find()
    findIndex: ƒ findIndex()
    ...

Con esta regla, podemos comprobar si existen métodos en nuestro User object dentro de su prototype, aunque ya sabemos que no existen todavía. Y, como podemos hacerlo en la consola, también podemos escribir este mismo código en nuestro archivo sandbox.js, y añadirle métodos ahí.

Sintaxis:

NuestroObjeto.prototype.método = function() {
    // contenido de la función
}

Apliquémoslo a nuestro User object. 

User.prototype.login = function() {
    console.log(`${this.username} is logged in`);
}

Y con este cambio, ya podemos retirar el método del constructor.

function User(username, email) {
    this.username = username;
    this.email = email;
    // this.login = function() {
    //     console.log(`${this.username} is logged in`);
    // }
}

Si ahora volvemos a hacer en la consola User.prototype, verás que ya recoge el método login. ¡Genial! 👏

Como sabes, podemos añadir todos los métodos que queramos, por ejemplo:

User.prototype.logout = function() {
    console.log(`${this.username} is logged out`);
}

pasando así a formar parte del prototype del User object. 

Cómo encadenar métodos

Al explicar las clases en la parte #1 de esta serie, hablamos de cómo encadenar métodos. No voy a repetir lo ya dicho ahí, pero sabrás que de momento no podemos encadenar métodos porque no hemos especificado que devuelvan nada. 

Así que para poder encadenarlos, debemos hacer un return del objeto, cosa que se hace utilizando el this, ya que this se refiere al objeto al que esté vinculadoCon estos cambios, ya podemos encadenar los métodos.

function User(username, email) {
    this.username = username;
    this.email = email;
}

User.prototype.login = function() {
    console.log(`${this.username} is logged in`);
    return this;
}

User.prototype.logout = function() {
    console.log(`${this.username} is logged out`);
    return this;
}

const userOne = new User('gandalf', 'gandalf@thewhite.com');
const userTwo = new User('frodo', 'frodo@bolson.com');

console.log(userOne, userTwo);

userOne.login().logout();

Con todo lo anterior, ya hemos conseguido escribir un código que hace exactamente lo mismo que las modernas clases de JavaScript. Y además, ahora ya sabemos lo que sucede entre bambalinas. 🕵️‍♀️

THE END!

¡Y hasta aquí la parte #2 de esta saga sobre POO 💩! Espero que hayas aprendido algo nuevo 😊.  Si te queda alguna duda, ¡nos vemos en los comentarios! Si quieres seguir aprendiendo, nos vemos en la parte #3 (disponible próximamente)

Y si crees que este post puede serle útil a alguien, ¡compártelo!

Otros artículos que pueden interesarte

Días del 353 al 386
Objetivos versus realidad Y nuevamente, llegó otro día clave. Llegó…y pasó. El pasado 4 de marzo este Reto Computer Geek[...]
Angular: Entendiendo la Directiva ngModel
Angular es un framework que nos permite, entre otras cosas, añadir contenido dinámico a nuestros archivos HTML. Una de las formas[...]
Días del 2 al 4
"Si buscas resultados distintos no hagas siempre lo mismo" - Albert Einstein Estos días estoy aprendiendo a hacer loops en[...]

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Como toda web legal que se precie, utilizamos cookies para asegurar que damos la mejor experiencia al usuario en nuestro sitio web. Si continúas utilizando este sitio asumiremos que estás de acuerdo. más información

Los ajustes de cookies de esta web están configurados para "permitir cookies" y así ofrecerte la mejor experiencia de navegación posible. Si sigues utilizando esta web sin cambiar tus ajustes de cookies o haces clic en "Aceptar" estarás dando tu consentimiento a esto.

Cerrar