POO en JavaScript
- views - 10-04-2023
¡Bienvenidos al blog! En este post, exploraremos la Programación Orientada a Objetos en JavaScript con ejemplos prácticos y visuales.
Imagina la Programación Orientada a Objetos como un mundo de aventuras al estilo Pokémon en la programación, donde "Herencia, Encapsulamiento, Abstracción y Polimorfismo" son los movimientos especiales que tus programas utilizan para organizarse y ser flexibles.
En JavaScript, utilizamos las "Classes" como las Pokébolas para crear objetos con habilidades únicas. Aprenderemos cómo mantener algunas cosas "privadas" y otras "públicas", al igual que las estrategias secretas de combate de los entrenadores Pokémon. 🌟
¡Y no acaba ahí! También descubriremos cómo "construir" nuestros propios Pokémon usando "constructores", y cómo enseñarles movimientos especiales usando "métodos". ¡Imagina tener un Pokémon personalizado con habilidades que tú inventaste!
En resumen, prepárate para embarcarte en una emocionante aventura para convertirte en un maestro Pokémon de la programación.
Programación orientada a objetos Definición: La programación orientada a objetos puede considerarse como el diseño de software a través de un conjunto de objetos que cooperan.
👀 ¡Todo en JavaScript es un objeto, te lo dicen todo el tiempo, ¿verdad?!
JavaScript es un lenguaje de programación que se destaca por su enfoque basado en prototipos. Sin embargo, a pesar de su naturaleza, JavaScript implementa ciertos conceptos fundamentales de una manera que permite comprender y aplicar la Programación Orientada a Objetos (POO) de manera efectiva.
En otras palabras, cuando los objetos 1 y 2 cooperan, hacen conjunción y avanzamos en el camino de la programación orientada a objetos. Pero para que esta colaboración sea exitosa, es esencial contar con fundamentos sólidos y partes clave que actúen como los pilares de nuestro enfoque.
Dentro del mundo de la programación orientada a objetos, existen cuatro pilares fundamentales que son cruciales para construir sistemas robustos y eficientes.
Clases en JavaScript
Una clase es un creador de objetos y en JavaScript se ve así:
class Pokemon {}
1. Herencia
Primer pilar es la herencia, una class puede heredar características de otra clase.
Pokémon es una clase, también tenemos las clases Gengar y Cubone, estas pueden heredar características de la clase Pokémon.
Los Pokémones tienen características similares; sabemos que tienen un nombre, un ID, un tipo de ataque, etc. Tienen similitudes y particularidades.
Ahora como llevamos esto al código
class Pokemon {
launchAttack(attack) {
//do something
}
}
class Gengar extends Pokemon {}
const darkGengar = new Gengar(); //my Gengar in the pokedex
darkGengar.launchAttack("Sombra Trampa");
🤓 Analicemos:
La clase Pokémon define métodos que se van a poder heredar dentro de otras clases. En este caso, ese método es launchAttack porque todos los Pokémon pueden lanzar un ataque.
Luego, la clase Gengar tiene una palabra reservada llamada extends: Quiere decir que Gengar extiende de Pokémon y hereda todas las características de Pokémon.
Por lo tanto, Gengar tiene todo lo de Pokémon y, además, Gengar puede definir particularidades.
const darkGengar = new Gengar();
- En esta linea lo que tenemos es algo que le llamamos instancia el nombre es Gengar
Class ahora veamos esto con un ejemplo visual
- La clase es muy abstracta, muy general, por eso utilizamos ¿Quién es ese Pokémon? que va a poner las cosas principales de qué es un Pokémon.
- Clase hija Gengar que hereda las características básicas de un Pokémon como lanzar un ataque.
- Por último, tenemos la instancia, el Pokémon que yo crié, el que yo tengo en mi Pokédex, el que tiene un nivel específico darkGengar.
Entendemos que tenemos la clase Pokémon como padre de todo, luego la clase hija como algo que extiende sus características base y, finalmente, de una manera particular, nuestro Pokémon creado, al cual le llamamos instancia de clase o el objeto creado por una clase. Tenemos a darkGengar.
- Tanto la clase como la clase hija son creadoras.
- La instancia es el objeto creado.
Constructor
Vamos a ver ahora el método constructor, nos va a ayudar a hacer muchas cosas.
Es un método llamado en el momento de la creación de instancias.
⚠️ Un constructor puede utilizar la palabra clave super para llamar al constructor de una clase padre. Si no especifica un método constructor, se utiliza un constructor predeterminado.
Cada vez que creamos una nueva instancia, como la instancia de Gengar, yo puedo inicializarla y mandarle cosas. Es un método auto invocado, no se puede hacer darkGengar.constructor automáticamente se llama.
class Gengar extends Pokemon {
consructor() {
super();
this.id = id;
}
}
- estoy asignando el valor del parámetro
this en este caso hace referencia al objeto contexto en el cual se ejecuta el código, la instancia de la clase es el objeto contexto.
- antes llamamos a la función super()
Si vamos a invocar a super dentro de un constructor, tiene que ser en la primera línea. Lo que hace super en este caso es que cualquier propiedad que establezcamos dentro de esta nueva clase estará disponible para su manipulación y acceso en la clase superior.
Gengar setea el id, pero Pokémon también puede leer id de otra manera no podría y gracias al super puede.
2. Encapsulamiento
El encapsulamiento en programación orientada a objetos en JavaScript se refiere a la idea de ocultar ciertos detalles internos de un objeto y proporcionar una interfaz controlada para interactuar con él. Esto se logra utilizando propiedades y métodos públicos y privados.
En este ejemplo sería la evolución de un Pokémon.
Una clase solo define las características del objeto, un método solo define cómo se ejecuta el método.
Nosotros sabemos que de manera natural todo evoluciona con el tiempo. No provocamos que un Pokémon evolucione por sí mismo ni ejecutamos el método 'evolucionar' del Pokémon de manera directa. En su lugar, ocurren varias cosas: lo llevamos a combates para que suba de nivel, le proporcionamos alimento especial y cariño. Existen numerosas variantes en el juego que permiten lograr esto. Sin embargo, tenemos acceso a ciertas acciones específicas. Por ejemplo, puedo participar en una batalla con mi Pokémon, y es posible que esa batalla me proporcione experiencia y otras recompensas. Estas acciones, con el tiempo, contribuyen a que el Pokémon evolucione.
// Define la clase base Pokemon
class Pokemon {
// Propiedad de solo lectura para obtener el estado del Pokemon
get status() {
// Aquí podrías calcular y retornar el estado real del Pokemon,
// pero en este ejemplo, simplemente lo estableceremos como "saludable".
return "healthy";
}
// Método para que el Pokemon lance un ataque
launchAttack(attack) {
// Aquí iría la lógica para realizar un ataque
// por ejemplo, actualizar puntos de vida, estado, etc.
}
// Método para que el Pokemon reciba un ataque
receiveAttack(attack) {
// Aquí iría la lógica para manejar un ataque recibido
// por ejemplo, reducir puntos de vida, cambiar el estado, etc.
}
}
// Define una subclase Gengar que hereda de Pokemon
class Gengar extends Pokemon {
constructor() {
super(); // Llama al constructor de la clase base (Pokemon)
}
}
// Crea una instancia de Gengar
const darkGengar = new Gengar();
// Accede al estado del Gengar (propiedad status) de forma segura
console.log("Estado inicial de darkGengar:", darkGengar.status);
// Ve el estado por el daño recibido de un ataque oponente a darkGengar (llama a la función receiveAttack)
darkGengar.receiveAttack("confusion");
// Vuelve a consultar el estado del Gengar después del ataque
console.log("Estado de darkGengar después del ataque:", darkGengar.status); //se que mi pokémon esta confundido
Suponiendo que estamos en una batalla, el Pokémon oponente nos atacó varias veces, entonces yo consulto por el estado y esa propiedad.
console.log(darkGengar.status);
Antes de continuar vamos a ver un pequeño resumen de los setter y getter
Los setters y getters en clases de JavaScript son métodos especiales que permiten establecer y obtener valores de propiedades de un objeto de manera controlada. Un setter se utiliza para asignar un valor a una propiedad, mientras que un getter se utiliza para obtener el valor de una propiedad.
Esto permite aplicar lógica personalizada antes de modificar o recuperar valores, lo que puede ser útil para la validación o transformación de datos dentro de un objeto. Los setters se definen con la palabra clave set, y los getters con la palabra clave get en una clase.
Acá estamos consultando la propiedad status, que es un getter. Hay un montón de estados en los Pokémon: dormido, congelado, envenenado, etc. Entonces, luego de recibir este ataque, vamos a ver que el estado de mi Pokémon ha cambiado. Pero yo no cambio mi estado, simplemente sé que recibí un ataque, lo que ocurre ahí está abstracto. Vamos a pasar al tercer pilar de la programación orientada a objetos para poder conectar mejor este ejemplo.
Abstracción
La abstracción es uno de los pilares fundamentales de la programación orientada a objetos (POO) y se refiere a la idea de simplificar y modelar objetos del mundo real en tu código de manera que representen conceptos esenciales de una manera fácil de entender y utilizar.
En JavaScript, la abstracción se logra principalmente a través de la creación de clases y objetos.
Es la conjunción de herencia compleja, métodos y propiedades que un objeto debe ser capaz de simular en un modelo de la realidad."
Sabemos lo que está sucediendo con nuestro Pokémon y, lo que es más importante, tenemos la capacidad de tomar medidas al enviar órdenes específicas. Podemos ejecutar acciones (métodos) como lanzar un ataque o recibir uno de manera controlada. Podemos asignarle al Pokémon un objeto para que se cure. Una piedra para que evolucione, aunque puede que funcione o no porque puede que pida una serie de requisitos adicionales.
Toda la lógica compleja de todo eso que está sucediendo está abstraída. Hay 2 tipos de estados que tiene el Pokémon:
- Estados volátiles que desaparecen después de una batalla.
- Estados no volátiles que permanecen incluso después de la batalla, por ejemplo, estar envenenado o estar dormido.
// Define la clase base Pokemon
class Pokemon {
#volatileStatus = ["confusion", "course"];
#nonVolatileStatus = ["sleep", "poison"];
#status = {
volatile: "",
nonVolatile: "",
};
set #newStatus(status) {
if (this.#nonVolatileStatus.includes(status)) {
this.#status.nonVolatile = status;
}
if (this.#volatileStatus.includes(status)) {
this.#status.volatile = status; // Corregido asignación de status
}
}
// Propiedad de solo lectura para obtener el estado del Pokemon
get status() {
// Retorna el estado actual del Pokemon
return [this.#status.volatile, this.#status.nonVolatile];
}
// Método para que el Pokemon lance un ataque
launchAttack(attack) {
// Aquí iría la lógica para realizar un ataque
// por ejemplo, actualizar puntos de vida, estado, etc.
console.log(`El Pokemon realizó el ataque: ${attack}`);
}
// Método para que el Pokemon reciba un ataque
receiveAttack(attack) {
// Aquí iría la lógica para manejar un ataque recibido
// por ejemplo, reducir puntos de vida, cambiar el estado, etc.
if (attack === "confusion") {
this.#newStatus = "confusion";
}
}
}
// Define una subclase Gengar que hereda de Pokemon
class Gengar extends Pokemon {
constructor() {
super(); // Llama al constructor de la clase base (Pokemon)
}
}
// Crea una instancia de Gengar
const darkGengar = new Gengar();
// Accede al estado del Gengar (propiedad status) de forma segura
console.log("Estado inicial de darkGengar:", darkGengar.status); //devuelve un array con 2 string vacíos
// Ve el estado por el daño recibido de un ataque oponente a darkGengar (llama a la función receiveAttack)
darkGengar.receiveAttack("confusion"); //recive un ataque darkGengar, valida
//esto y se setea ese nuevo estado, el set que recibe el status, valida si es un estado volatil o no volatil
// Vuelve a consultar el estado del Gengar después del ataque
console.log("Estado de darkGengar después del ataque:", darkGengar.status);
Ya puedes tener métodos, propiedades privadas y campos en general privados dentro de tus clases. Se declaran con un #.
¿Cuál es la particularidad de estos campos privados? Es que cualquier otra clase que no sea Pokemon no podrá acceder a esos campos. Entonces, ¿qué significa esto? Que están abstraídos.
🛑 No puedo cambiar lo siguiente:
#volatileStatus = ["confusion", "course"];
#nonVolatileStatus = ["sleep", "poison"];
#status = {
volatile: "",
nonVolatile: "",
};
set #newStatus(status) {
if (this.#nonVolatileStatus.includes(status)) {
this.#status.nonVolatile = status;
}
if (this.#volatileStatus.includes(status)) {
this.#status.volatile = status; // Corregido asignación de status
}
}// todo es inaccesible para el usuario perse
De otra manera, sí podría hacerlo, hubiera puesto 'status' y lo habría cambiado desde esta otra clase. Entonces, qué chiste, así no hay programación orientada a objetos, pero ahora sí tenemos campos totalmente privados. Si intentamos cambiarlo 🛑 NO puedes, nos manda un error.
4. Polimorfismo
Diferentes clases podrían definir el mismo método o propiedad.
Si bien las clases te van a servir de una forma muy dinámica, el polimorfismo lo hace explícito. Diferentes clases podrían definir el mismo método o propiedad; el callback y todo eso son diferentes. El ataque de Charizard y el ataque de Pikachu tienen que ser definidos en su propia clase en lugar de la clase superior, que sería "Pokemon".
Estos Pokémon, como vemos en esta imagen, están atacando, pero ambos con un ataque diferente. El ataque diferente podría ser simplemente un string como "ataque trueno", pero es más complejo, el tipo de daño que hace, los cálculos internos, cómo se tiene que mover, el callback, etc. Todo eso es diferente. Entonces, el ataque de estos dos Pokémon tiene que ser definido en su propia clase en lugar de ser definido en la clase superior, que sería class Pokemon.
A pesar de tener cosas idénticas o similares en ambas clases, también tienen cosas particulares que se definen dependiendo de la clase a la que pertenecen.
class Gengar extends Pokemon {
id = 25;
name = "Dark Gengar";
attack() {}
}
class Cubone extends Pokemon {
id = 6;
name = "Charizar";
atack() {}
}
Veamos un poco mas de esto.
Propiedades
Una característica del objeto, como el color.
class Gengar extends Pokemon {
type = "Fantasma";
}
Métodos
También tenemos métodos dentro de JavaScript que se definen de esa manera gracias a los "shorthand property names"
class Gengar extends Pokemon {
attack() {}
}
Public / Static fields
Una capacidad del objeto, como caminar. A diferencia de los campos privados, son accesibles por las demás clases. Entre los campos públicos, tenemos los campos estáticos, y hay una particularidad con ellos. La particularidad de los campos estáticos es que se pueden acceder no necesariamente dentro de una clase o una instancia, sino que se pueden llamar directamente desde la clase a esa propiedad.
- Por ejemplo para hacer un utility.
Ejemplo de como funciona Public / Static fields
class MyClass {
// Campo estático público
static counter = 0;
// Constructor de la clase
constructor(name) {
this.name = name;
MyClass.counter++; // Incrementa el contador cada vez que se crea una instancia
}
// Método para obtener el nombre
getName() {
return this.name;
}
// Método estático para obtener el contador
static getCounter() {
return MyClass.counter;
}
}
// Crear algunas instancias de la clase
const objeto1 = new MyClass("Objeto 1");
const objeto2 = new MyClass("Objeto 2");
// Acceder a un campo estático desde la clase
console.log("Contador desde la clase:", MyClass.counter); // Output: 2
// Acceder a un campo estático desde una instancia
console.log("Contador desde una instancia:", objeto1.counter); // Output: undefined
// Acceder al contador usando el método estático
console.log("Contador usando el método estático:", MyClass.getCounter()); // Output: 2
Fuentes de estudio
object-oriented-programming-javascript
🤓 Conclusiones
Espero que este artículo haya sido útil para comprender la Programación Orientada a Objetos en JavaScript al relacionar los conceptos con Pokémon. Los ejemplos gráficos proporcionados deberían haber ayudado a comprender mejor los cuatro pilares fundamentales.
Si decides compartir este artículo, estarás contribuyendo a que otras personas que están empezando con JavaScript o que están aprendiendo sobre este tema puedan acceder al contenido.
¡Muchas gracias por tu lectura dejo el link de cafecito apoyo voluntario, tu contribución me motiva a seguir creando contenido de alta calidad. ¡Muchas gracias por tu apoyo!
