Bienvenido al curso de Introducción a la Programación Orientada a Objetos (POO), diseñado para iniciarte en los conceptos fundamentales de este paradigma de programación. Este curso tiene una duración de 30 días y está estructurado para que dediques aproximadamente 30 minutos al día, facilitando el aprendizaje de manera progresiva y manejable.
Objetivos del Curso
Comprender los Conceptos Básicos de POO: Familiarizarte con las clases, objetos, atributos, métodos, herencia, polimorfismo, encapsulamiento y abstracción.
Aplicar Principios SOLID: Aprender y aplicar los principios de diseño SOLID para crear código robusto, mantenible y escalable.
Implementar Patrones de Diseño: Conocer e implementar patrones de diseño comunes como Singleton, Factory Method, Observer y Decorator.
Manejo de Excepciones: Aprender a gestionar errores y excepciones de manera efectiva.
Realizar Pruebas Unitarias: Comprender la importancia de las pruebas unitarias y cómo escribirlas usando herramientas como Jest.
Preparación para Exámenes: Prepararte para exámenes básicos relacionados con los conocimientos adquiridos en POO.
Metodología
El curso está diseñado para ser práctico y enfocado en ejemplos claros y concisos en JavaScript. Cada día cubrirá un tema específico o conjunto de temas, con ejemplos de código y ejercicios prácticos para consolidar tu aprendizaje. Además, se incluyen sesiones de repaso y simulaciones de examen para asegurarte de que estás listo para evaluar tus conocimientos.
Estructura del Curso
Semana 1: Fundamentos de POO
Día 1: Introducción a POO - Clases, objetos, atributos y métodos.
Día 2: Encapsulamiento - Protegiendo datos en POO.
Día 3: Herencia - Reutilización de código.
Día 4: Polimorfismo - Flexibilidad en POO.
Día 5: Abstracción - Simplificación de la complejidad.
Día 6: Ejercicios Prácticos - Creación de clases simples.
Día 7: Repaso y Quiz - Revisión de conceptos y cuestionario.
Semana 2: Principios de Diseño
Día 8: Herencia Avanzada.
Día 9: Polimorfismo Avanzado.
Día 10: Interfaces y Clases Abstractas.
Día 11: Composición vs. Herencia.
Día 12: Ejercicios Prácticos.
Día 13: Principios SOLID.
Día 14: Repaso y Quiz.
Semana 3: Patrones de Diseño
Día 15: Introducción a los Patrones de Diseño.
Día 16: Singleton.
Día 17: Factory Method.
Día 18: Observer.
Día 19: Decorator.
Día 20: Ejercicios Prácticos.
Día 21: Repaso y Quiz.
Semana 4: Técnicas Avanzadas y Preparación para el Examen
Día 22: Refactorización.
Día 23: Pruebas Unitarias.
Día 24: Manejo de Excepciones.
Día 25: Ejercicio Completo.
Día 26: Optimización de Código.
Día 27: Estudio de Caso.
Día 28: Preparación para el Examen.
Día 29: Simulación de Examen.
Día 30: Repaso Final.
Requisitos
No se requieren conocimientos básicos de programación.
Familiaridad con JavaScript.
Aconsejamos el uso de APPS SCRIPT de Google.
Compromiso de 30 minutos diarios para aprender y practicar.
Este curso es una excelente manera de iniciar tu viaje en la Programación Orientada a Objetos y preparar los conocimientos necesarios para enfrentarte a exámenes básicos en este tema. ¡Vamos a empezar!
La Programación Orientada a Objetos (POO) es un enfoque de programación que organiza el software en torno a objetos, que son instancias de clases. Este paradigma permite modelar entidades del mundo real en el software, facilitando la estructura y organización del código. A continuación, se presentan los conceptos clave de la POO:
Una clase es una plantilla para crear objetos. Define el tipo de objeto, incluyendo sus atributos (propiedades) y métodos (funciones). Una clase actúa como un plano que describe cómo se deben crear los objetos y qué características tendrán.
Un objeto es una instancia de una clase. Representa una entidad concreta en el software con propiedades y comportamientos específicos definidos por su clase. Los objetos interactúan entre sí para realizar tareas en el programa.
Los atributos son variables que pertenecen a un objeto. Describen las características del objeto, como su estado o propiedades. Por ejemplo, un objeto "Persona" puede tener atributos como "nombre" y "edad".
Los métodos son funciones que pertenecen a un objeto. Describen los comportamientos del objeto, permitiendo realizar acciones o calcular resultados basados en sus atributos. Por ejemplo, un objeto "Persona" puede tener un método "saludar".
Ejemplo en JavaScript
Vamos a crear una clase sencilla llamada Persona. Esta clase tendrá atributos nombre y edad, y un método saludar.
// Definición de la clase Persona
class Persona {
// El constructor se llama cuando se crea un objeto de la clase
constructor(nombre, edad) {
this.nombre = nombre; // atributo nombre
this.edad = edad; // atributo edad
}
// Método saludar
saludar() {
console.log(`Hola, mi nombre es ${this.nombre} y tengo ${this.edad} años.`);
}
}
// Crear un objeto de la clase Persona
const persona1 = new Persona('Juan', 30);
// Usar el método saludar
persona1.saludar(); // Output: Hola, mi nombre es Juan y tengo 30 años.
En este ejemplo:
Definimos la clase Persona con el constructor constructor(nombre, edad) para inicializar los atributos nombre y edad.
Creamos un método saludar que imprime un saludo con el nombre y la edad de la persona.
Creamos un objeto persona1 de la clase Persona y llamamos al método saludar.
Ejemplo en JAVA
// Definición de la clase Persona
public class Persona {
// Atributos de la clase
private String nombre;
private int edad;
// Constructor de la clase
public Persona(String nombre, int edad) {
this.nombre = nombre; // Asignación del atributo nombre
this.edad = edad; // Asignación del atributo edad
}
// Método saludar
public void saludar() {
System.out.println("Hola, mi nombre es " + this.nombre + " y tengo " + this.edad + " años.");
}
// Método main para ejecutar el código
public static void main(String[] args) {
// Crear un objeto de la clase Persona
Persona persona1 = new Persona("Juan", 30);
// Usar el método saludar
persona1.saludar(); // Output: Hola, mi nombre es Juan y tengo 30 años.
}
}
Clase Persona:
La clase Persona define dos atributos, nombre y edad, que son privados. Los atributos se inicializan a través de un constructor.
Constructor:
El constructor de la clase Persona toma dos parámetros, nombre y edad, que se asignan a los atributos de la clase.
Método saludar:
Este método imprime un mensaje en la consola utilizando el método System.out.println, similar a console.log en JavaScript.
Método main:
El método main es el punto de entrada del programa. Aquí se crea una instancia de la clase Persona y se llama al método saludar para mostrar el mensaje en la consola.
Ahora es tu turno. Crea una clase Coche con los siguientes requisitos:
Atributos: marca, modelo, año.
Método: detalles que imprime los detalles del coche en formato: "Este coche es un [marca] [modelo] del año [año]."
JavaScript. Aquí tienes un esquema para ayudarte a empezar:
// Definición de la clase Coche
class Coche {
// Constructor
constructor(marca, modelo, año) {
this.marca = marca;
this.modelo = modelo;
this.año = año;
}
// Método detalles
detalles() {
console.log(`Este coche es un ${this.marca} ${this.modelo} del año ${this.año}.`);
}
}
// Crear un objeto de la clase Coche
const coche1 = new Coche('Toyota', 'Corolla', 2020);
// Usar el método detalles
coche1.detalles(); // Output: Este coche es un Toyota Corolla del año 2020.
Java. Aquí tienes un esquema para ayudarte a empezar:
// Definición de la clase Coche
public class Coche {
// Atributos de la clase
private String marca;
private String modelo;
private int año;
// Constructor de la clase
public Coche(String marca, String modelo, int año) {
this.marca = marca; // Asignación del atributo marca
this.modelo = modelo; // Asignación del atributo modelo
this.año = año; // Asignación del atributo año
}
// Método detalles
public void detalles() {
System.out.println("Este coche es un " + this.marca + " " + this.modelo + " del año " + this.año + ".");
}
// Método main para ejecutar el código
public static void main(String[] args) {
// Crear un objeto de la clase Coche
Coche coche1 = new Coche("Toyota", "Corolla", 2020);
// Usar el método detalles
coche1.detalles(); // Output: Este coche es un Toyota Corolla del año 2020.
}
}
Practica creando varios objetos Coche con diferentes marcas, modelos y años, y llama al método detalles para cada uno de ellos.
El encapsulamiento es uno de los pilares fundamentales de la Programación Orientada a Objetos. Consiste en restringir el acceso directo a ciertos componentes de un objeto y controlar cómo estos pueden ser modificados o accedidos. Esto se logra mediante la definición de atributos y métodos como privados o protegidos, permitiendo que solo los métodos dentro de la misma clase puedan acceder o modificar estos datos.
El encapsulamiento protege la integridad de los datos y asegura que los objetos se comporten de manera coherente y predecible, lo que facilita el mantenimiento y la evolución del código.
Protección de datos: Los atributos sensibles o críticos no pueden ser modificados directamente desde fuera de la clase, evitando comportamientos inesperados.
Modularidad: Facilita la modularidad del código, ya que los cambios internos en la implementación de una clase no afectan a otras partes del programa.
Mantenibilidad: Hace que el código sea más fácil de mantener, ya que puedes controlar y rastrear cómo se accede y modifica el estado de un objeto.
En JavaScript, el encapsulamiento se puede implementar de varias maneras, incluyendo el uso de convenciones, propiedades privadas y métodos de acceso (getters y setters).
Una convención común en JavaScript es usar un guion bajo (_) al principio del nombre de un atributo para indicar que es privado.
javascript
class Persona {
constructor(nombre, edad) {
this._nombre = nombre; // Convención de nombre para indicar privado
this._edad = edad;
}
// Método para acceder al nombre
getNombre() {
return this._nombre;
}
// Método para modificar la edad
setEdad(nuevaEdad) {
if (nuevaEdad > 0) {
this._edad = nuevaEdad;
} else {
console.log('La edad debe ser un número positivo.');
}
}
// Método para mostrar información de la persona
mostrarInfo() {
console.log(`Nombre: ${this._nombre}, Edad: ${this._edad}`);
}
}
const persona1 = new Persona('Juan', 30);
persona1.mostrarInfo(); // Output: Nombre: Juan, Edad: 30
persona1.setEdad(35);
persona1.mostrarInfo(); // Output: Nombre: Juan, Edad: 35
persona1.setEdad(-5); // Output: La edad debe ser un número positivo.
Desde ES6, JavaScript permite el uso de la sintaxis # para definir atributos privados que no pueden ser accedidos fuera de la clase.
javascript
class CuentaBancaria {
#saldo; // Atributo privado
constructor(titular, saldoInicial) {
this.titular = titular;
this.#saldo = saldoInicial; // Asignación inicial del saldo
}
// Método para depositar dinero
depositar(cantidad) {
if (cantidad > 0) {
this.#saldo += cantidad;
console.log(`Depositaste ${cantidad}. Nuevo saldo: ${this.#saldo}`);
} else {
console.log('La cantidad a depositar debe ser mayor que cero.');
}
}
// Método para retirar dinero
retirar(cantidad) {
if (cantidad > 0 && cantidad <= this.#saldo) {
this.#saldo -= cantidad;
console.log(`Retiraste ${cantidad}. Nuevo saldo: ${this.#saldo}`);
} else {
console.log('Fondos insuficientes o cantidad inválida.');
}
}
// Método para consultar el saldo
consultarSaldo() {
console.log(`El saldo de ${this.titular} es ${this.#saldo}`);
}
}
const cuenta1 = new CuentaBancaria('Ana', 1000);
cuenta1.consultarSaldo(); // Output: El saldo de Ana es 1000
cuenta1.depositar(500); // Output: Depositaste 500. Nuevo saldo: 1500
cuenta1.retirar(200); // Output: Retiraste 200. Nuevo saldo: 1300
En este ejemplo, el saldo (#saldo) es un atributo privado que no puede ser accedido directamente desde fuera de la clase CuentaBancaria.
Para interactuar con atributos privados, se utilizan los métodos get y set, que permiten obtener y modificar el valor de los atributos de manera controlada.
javascript
class Producto {
#precio; // Atributo privado
constructor(nombre, precio) {
this.nombre = nombre;
this.#precio = precio;
}
// Getter para obtener el precio
get precio() {
return this.#precio;
}
// Setter para establecer un nuevo precio
set precio(nuevoPrecio) {
if (nuevoPrecio > 0) {
this.#precio = nuevoPrecio;
} else {
console.log('El precio debe ser un número positivo.');
}
}
}
const producto1 = new Producto('Laptop', 1200);
console.log(producto1.precio); // Output: 1200
producto1.precio = 1500; // Actualiza el precio
console.log(producto1.precio); // Output: 1500
producto1.precio = -100; // Output: El precio debe ser un número positivo.
Aquí, usamos get para acceder al precio y set para modificarlo, asegurando que el valor siempre sea positivo.
Hoy hemos aprendido sobre el encapsulamiento, su importancia para proteger los datos y cómo implementarlo en JavaScript usando convenciones, atributos privados, y métodos de acceso (getters y setters).
Tareas para Practicar:
Crear una clase Vehiculo con atributos marca, modelo, y #kilometraje (privado).
class Vehiculo {
// Atributo privado #kilometraje
#kilometraje;
// Constructor de la clase Vehiculo
constructor(marca, modelo, kilometrajeInicial) {
this.marca = marca;
this.modelo = modelo;
this.#kilometraje = kilometrajeInicial; // Asignación del kilometraje inicial
}
// Método getter para obtener el valor de #kilometraje
getKilometraje() {
return this.#kilometraje;
}
// Método setter para establecer un nuevo valor para #kilometraje
setKilometraje(nuevoKilometraje) {
if (nuevoKilometraje >= 0) {
this.#kilometraje = nuevoKilometraje;
console.log(`El kilometraje ha sido actualizado a ${this.#kilometraje} km.`);
} else {
console.log('El kilometraje debe ser un número positivo.');
}
}
// Método para mostrar información del vehículo
mostrarInfo() {
console.log(`Marca: ${this.marca}, Modelo: ${this.modelo}, Kilometraje: ${this.#kilometraje} km.`);
}
}
Implementar métodos getKilometraje y setKilometraje que permitan consultar y modificar el kilometraje, asegurando que el nuevo valor sea positivo.
// Crear un objeto de la clase Vehiculo
const vehiculo1 = new Vehiculo('Toyota', 'Corolla', 50000);
// Mostrar información inicial del vehículo
vehiculo1.mostrarInfo(); // Output: Marca: Toyota, Modelo: Corolla, Kilometraje: 50000 km.
// Consultar el kilometraje actual
console.log(`Kilometraje actual: ${vehiculo1.getKilometraje()} km`); // Output: Kilometraje actual: 50000 km
// Actualizar el kilometraje a un nuevo valor válido
vehiculo1.setKilometraje(55000); // Output: El kilometraje ha sido actualizado a 55000 km.
// Intentar actualizar el kilometraje a un valor negativo
vehiculo1.setKilometraje(-100); // Output: El kilometraje debe ser un número positivo.
// Mostrar la información del vehículo después de la actualización
vehiculo1.mostrarInfo(); // Output: Marca: Toyota, Modelo: Corolla, Kilometraje: 55000 km.
Crear objetos de la clase Vehiculo y probar los métodos implementados.
// Crear otro objeto de la clase Vehiculo
const vehiculo2 = new Vehiculo('Honda', 'Civic', 30000);
// Mostrar información inicial del segundo vehículo
vehiculo2.mostrarInfo(); // Output: Marca: Honda, Modelo: Civic, Kilometraje: 30000 km.
// Consultar el kilometraje actual del segundo vehículo
console.log(`Kilometraje actual: ${vehiculo2.getKilometraje()} km`); // Output: Kilometraje actual: 30000 km
// Actualizar el kilometraje a un nuevo valor válido
vehiculo2.setKilometraje(32000); // Output: El kilometraje ha sido actualizado a 32000 km.
// Mostrar la información del segundo vehículo después de la actualización
vehiculo2.mostrarInfo(); // Output: Marca: Honda, Modelo: Civic, Kilometraje: 32000 km.
¡Esto concluye nuestra sesión del Día 2 sobre Encapsulamiento! Mañana continuaremos con el tema de Herencia.
Concepto de Herencia
La herencia es un principio fundamental de la Programación Orientada a Objetos que permite crear nuevas clases a partir de clases existentes. Las clases derivadas (o "subclases") heredan atributos y métodos de una clase base (o "superclase"), lo que facilita la reutilización del código y la creación de jerarquías de clases más organizadas y lógicas.
La herencia permite que las subclases puedan extender o modificar el comportamiento de la clase base, lo que es útil para evitar duplicación de código y mantener un diseño más limpio y manejable.
Beneficios de la Herencia
Reutilización de Código: Las subclases pueden reutilizar el código existente de la clase base, evitando la repetición de código.
Extensibilidad: Permite agregar o modificar funcionalidades en subclases sin alterar la clase base.
Organización Jerárquica: Facilita la creación de una estructura jerárquica donde las clases más específicas derivan de clases más generales.
Herencia en JavaScript
En JavaScript, la herencia se implementa utilizando la palabra clave extends para crear una subclase que hereda de una clase base. La palabra clave super se utiliza para llamar al constructor o a métodos de la clase base desde la subclase.
Ejemplo 1: Creación de una Jerarquía de Clases
Supongamos que estamos creando un sistema para gestionar vehículos, y queremos crear diferentes tipos de vehículos como automóviles y motocicletas. En lugar de repetir código, podemos crear una clase base Vehiculo y derivar de ella clases específicas como Automovil y Motocicleta.
javascript
// Clase base Vehiculo
class Vehiculo {
constructor(marca, modelo, año) {
this.marca = marca;
this.modelo = modelo;
this.año = año;
}
// Método para mostrar la información del vehículo
mostrarInfo() {
console.log(`Vehículo: ${this.marca} ${this.modelo} (${this.año})`);
}
}
// Subclase Automovil que hereda de Vehiculo
class Automovil extends Vehiculo {
constructor(marca, modelo, año, puertas) {
super(marca, modelo, año); // Llamada al constructor de la clase base
this.puertas = puertas;
}
// Método específico de Automovil
mostrarInfo() {
super.mostrarInfo(); // Llamada al método de la clase base
console.log(`Puertas: ${this.puertas}`);
}
}
// Subclase Motocicleta que hereda de Vehiculo
class Motocicleta extends Vehiculo {
constructor(marca, modelo, año, tipo) {
super(marca, modelo, año); // Llamada al constructor de la clase base
this.tipo = tipo;
}
// Método específico de Motocicleta
mostrarInfo() {
super.mostrarInfo(); // Llamada al método de la clase base
console.log(`Tipo: ${this.tipo}`);
}
}
Ejemplo 2: Creación y Uso de Objetos de Subclases
Vamos a crear instancias de las clases Automovil y Motocicleta y utilizar sus métodos para ver cómo la herencia facilita la reutilización del código.
javascript
// Crear un objeto de la clase Automovil
const auto1 = new Automovil('Toyota', 'Corolla', 2021, 4);
auto1.mostrarInfo();
// Output:
// Vehículo: Toyota Corolla (2021)
// Puertas: 4
// Crear un objeto de la clase Motocicleta
const moto1 = new Motocicleta('Honda', 'CBR', 2022, 'Deportiva');
moto1.mostrarInfo();
// Output:
// Vehículo: Honda CBR (2022)
// Tipo: Deportiva
Clase Base Vehiculo: Define los atributos comunes a todos los vehículos (marca, modelo, año) y un método para mostrar esta información.
Subclase Automovil: Hereda de Vehiculo y añade un atributo específico (puertas). Además, sobrescribe el método mostrarInfo() para incluir información adicional sobre el número de puertas.
Subclase Motocicleta: También hereda de Vehiculo, pero añade un atributo diferente (tipo) y sobrescribe mostrarInfo() para mostrar el tipo de motocicleta.
Uso de super: Se utiliza para llamar al constructor y métodos de la clase base dentro de las subclases, permitiendo que las subclases amplíen o modifiquen la funcionalidad de la clase base.
Jerarquía de Clases en Acción
Este ejemplo muestra cómo se pueden crear jerarquías de clases para representar diferentes tipos de vehículos, reutilizando código de la clase base Vehiculo y añadiendo comportamientos específicos en las subclases Automovil y Motocicleta.
Crear una subclase Camion que herede de Vehiculo y añada un atributo capacidadCarga. Sobrescribe el método mostrarInfo() para incluir la capacidad de carga.
// Clase base Vehiculo
class Vehiculo {
constructor(marca, modelo, año) {
this.marca = marca;
this.modelo = modelo;
this.año = año;
}
// Método para mostrar la información del vehículo
mostrarInfo() {
console.log(`Vehículo: ${this.marca} ${this.modelo} (${this.año})`);
}
}
// Subclase Camion que hereda de Vehiculo
class Camion extends Vehiculo {
constructor(marca, modelo, año, capacidadCarga) {
super(marca, modelo, año); // Llamada al constructor de la clase base
this.capacidadCarga = capacidadCarga;
}
// Sobrescribir el método mostrarInfo para incluir la capacidad de carga
mostrarInfo() {
super.mostrarInfo(); // Llamada al método de la clase base
console.log(`Capacidad de carga: ${this.capacidadCarga} toneladas`);
}
}
Crear varios objetos de Automovil, Motocicleta, y Camion, y utilizar sus métodos para mostrar la información completa de cada vehículo.
// Subclase Automovil que hereda de Vehiculo
class Automovil extends Vehiculo {
constructor(marca, modelo, año, puertas) {
super(marca, modelo, año);
this.puertas = puertas;
}
mostrarInfo() {
super.mostrarInfo();
console.log(`Puertas: ${this.puertas}`);
}
}
// Subclase Motocicleta que hereda de Vehiculo
class Motocicleta extends Vehiculo {
constructor(marca, modelo, año, tipo) {
super(marca, modelo, año);
this.tipo = tipo;
}
mostrarInfo() {
super.mostrarInfo();
console.log(`Tipo: ${this.tipo}`);
}
}
// Crear y mostrar información de un Automovil
const auto1 = new Automovil('Toyota', 'Corolla', 2021, 4);
auto1.mostrarInfo();
// Output:
// Vehículo: Toyota Corolla (2021)
// Puertas: 4
// Crear y mostrar información de una Motocicleta
const moto1 = new Motocicleta('Honda', 'CBR', 2022, 'Deportiva');
moto1.mostrarInfo();
// Output:
// Vehículo: Honda CBR (2022)
// Tipo: Deportiva
// Crear y mostrar información de un Camion
const camion1 = new Camion('Volvo', 'FH16', 2020, 18);
camion1.mostrarInfo();
// Output:
// Vehículo: Volvo FH16 (2020)
// Capacidad de carga: 18 toneladas
Experimentar con la Sobrescritura de Métodos: Intenta modificar y extender otros métodos en las subclases para observar cómo funciona la herencia y la sobrescritura.
// Clase base Vehiculo con método arrancar
class Vehiculo {
constructor(marca, modelo, año) {
this.marca = marca;
this.modelo = modelo;
this.año = año;
}
mostrarInfo() {
console.log(`Vehículo: ${this.marca} ${this.modelo} (${this.año})`);
}
// Método arrancar
arrancar() {
console.log(`El ${this.marca} ${this.modelo} está arrancando...`);
}
}
// Subclase Automovil que hereda de Vehiculo y sobrescribe el método arrancar
class Automovil extends Vehiculo {
constructor(marca, modelo, año, puertas) {
super(marca, modelo, año);
this.puertas = puertas;
}
mostrarInfo() {
super.mostrarInfo();
console.log(`Puertas: ${this.puertas}`);
}
arrancar() {
console.log(`El automóvil ${this.marca} ${this.modelo} está arrancando en modo de conducción normal...`);
}
}
// Subclase Motocicleta que hereda de Vehiculo y sobrescribe el método arrancar
class Motocicleta extends Vehiculo {
constructor(marca, modelo, año, tipo) {
super(marca, modelo, año);
this.tipo = tipo;
}
mostrarInfo() {
super.mostrarInfo();
console.log(`Tipo: ${this.tipo}`);
}
arrancar() {
console.log(`La motocicleta ${this.marca} ${this.modelo} está arrancando con un rugido potente...`);
}
}
// Subclase Camion que hereda de Vehiculo y sobrescribe el método arrancar
class Camion extends Vehiculo {
constructor(marca, modelo, año, capacidadCarga) {
super(marca, modelo, año);
this.capacidadCarga = capacidadCarga;
}
mostrarInfo() {
super.mostrarInfo();
console.log(`Capacidad de carga: ${this.capacidadCarga} toneladas`);
}
arrancar() {
console.log(`El camión ${this.marca} ${this.modelo} está arrancando con un motor potente, listo para la carga...`);
}
}
// Crear objetos y probar el método arrancar
const auto1 = new Automovil('Toyota', 'Corolla', 2021, 4);
auto1.arrancar(); // Output: El automóvil Toyota Corolla está arrancando en modo de conducción normal...
const moto1 = new Motocicleta('Honda', 'CBR', 2022, 'Deportiva');
moto1.arrancar(); // Output: La motocicleta Honda CBR está arrancando con un rugido potente...
const camion1 = new Camion('Volvo', 'FH16', 2020, 18);
camion1.arrancar(); // Output: El camión Volvo FH16 está arrancando con un motor potente, listo para la carga...
El polimorfismo es un concepto fundamental en la programación orientada a objetos que permite que las clases tengan diferentes implementaciones de un método bajo la misma interfaz o estructura. La palabra "polimorfismo" proviene del griego y significa "muchas formas", lo que implica que el mismo método puede actuar de manera diferente según el objeto que lo utilice. Esto es crucial para la flexibilidad y extensibilidad de un sistema, ya que permite tratar objetos de diferentes clases de manera uniforme.
Polimorfismo en tiempo de compilación (estático): Este tipo de polimorfismo se logra mediante la sobrecarga de métodos y operadores. Los métodos sobrecargados comparten el mismo nombre pero tienen diferentes firmas (tipo o número de parámetros).
Polimorfismo en tiempo de ejecución (dinámico): Este tipo de polimorfismo se logra mediante la sobrescritura de métodos. Las clases derivadas pueden tener sus propias implementaciones de un método definido en la clase base.
Supongamos que estamos creando una aplicación para gestionar diferentes tipos de cuentas de usuario (Usuario regular, Usuario Premium y Usuario Administrador). Utilizaremos el polimorfismo para definir diferentes comportamientos para cada tipo de usuario al enviar una notificación.
javascript
// Definición de la clase base Usuario
class Usuario {
constructor(nombre) {
this.nombre = nombre;
}
enviarNotificacion(mensaje) {
console.log(`${this.nombre} recibió una notificación: ${mensaje}`);
}
}
// Clase derivada UsuarioRegular
class UsuarioRegular extends Usuario {
enviarNotificacion(mensaje) {
console.log(`(Regular) ${this.nombre} recibió un correo electrónico: ${mensaje}`);
}
}
// Clase derivada UsuarioPremium
class UsuarioPremium extends Usuario {
enviarNotificacion(mensaje) {
console.log(`(Premium) ${this.nombre} recibió una notificación por SMS: ${mensaje}`);
}
}
// Clase derivada UsuarioAdministrador
class UsuarioAdministrador extends Usuario {
enviarNotificacion(mensaje) {
console.log(`(Admin) ${this.nombre} recibió una alerta en la aplicación: ${mensaje}`);
}
}
// Crear objetos de cada clase de usuario
const usuario1 = new UsuarioRegular('Ana');
const usuario2 = new UsuarioPremium('Carlos');
const usuario3 = new UsuarioAdministrador('Elena');
// Enviar notificaciones
usuario1.enviarNotificacion('Tu pedido ha sido enviado.');
// Output: (Regular) Ana recibió un correo electrónico: Tu pedido ha sido enviado.
usuario2.enviarNotificacion('Tu pedido ha sido entregado.');
// Output: (Premium) Carlos recibió una notificación por SMS: Tu pedido ha sido entregado.
usuario3.enviarNotificacion('Hay una nueva actualización disponible.');
// Output: (Admin) Elena recibió una alerta en la aplicación: Hay una nueva actualización disponible.
En este ejemplo:
Definimos una clase base Usuario con un método enviarNotificacion que se encarga de enviar una notificación al usuario.
Creamos clases derivadas UsuarioRegular, UsuarioPremium y UsuarioAdministrador, cada una con su propia implementación del método enviarNotificacion.
El método enviarNotificacion está sobrescrito en cada clase derivada para proporcionar un comportamiento específico según el tipo de usuario.
Creamos objetos de cada tipo de usuario y utilizamos el método enviarNotificacion para demostrar cómo cada usuario recibe la notificación de manera diferente.
Ahora es tu turno. Crea una clase Figura para una aplicación de dibujo con los siguientes requisitos:
Atributos: nombre.
Métodos:
calcularArea(): Método abstracto que será sobrescrito por las clases derivadas.
dibujar(): Método abstracto que será sobrescrito por las clases derivadas.
Crea clases derivadas Circulo, Rectangulo, y Triangulo, cada una con su propia implementación de calcularArea() y dibujar().
javascript
// Definición de la clase base Figura
class Figura {
constructor(nombre) {
this.nombre = nombre;
}
calcularArea() {
throw new Error("Este método debe ser implementado por una clase derivada.");
}
dibujar() {
throw new Error("Este método debe ser implementado por una clase derivada.");
}
}
// Clase derivada Circulo
class Circulo extends Figura {
constructor(nombre, radio) {
super(nombre);
this.radio = radio;
}
calcularArea() {
return Math.PI * this.radio ** 2;
}
dibujar() {
console.log(`Dibujando un círculo llamado ${this.nombre} con área ${this.calcularArea()}`);
}
}
// Clase derivada Rectangulo
class Rectangulo extends Figura {
constructor(nombre, ancho, alto) {
super(nombre);
this.ancho = ancho;
this.alto = alto;
}
calcularArea() {
return this.ancho * this.alto;
}
dibujar() {
console.log(`Dibujando un rectángulo llamado ${this.nombre} con área ${this.calcularArea()}`);
}
}
// Clase derivada Triangulo
class Triangulo extends Figura {
constructor(nombre, base, altura) {
super(nombre);
this.base = base;
this.altura = altura;
}
calcularArea() {
return (this.base * this.altura) / 2;
}
dibujar() {
console.log(`Dibujando un triángulo llamado ${this.nombre} con área ${this.calcularArea()}`);
}
}
// Crear objetos de cada clase de figura
const circulo1 = new Circulo('Círculo1', 5);
const rectangulo1 = new Rectangulo('Rectángulo1', 4, 6);
const triangulo1 = new Triangulo('Triángulo1', 3, 4);
// Usar los métodos calcularArea y dibujar
circulo1.dibujar(); // Output: Dibujando un círculo llamado Círculo1 con área 78.53981633974483
rectangulo1.dibujar(); // Output: Dibujando un rectángulo llamado Rectángulo1 con área 24
triangulo1.dibujar(); // Output: Dibujando un triángulo llamado Triángulo1 con área 6
Definimos la clase base Figura con métodos abstractos calcularArea() y dibujar().
Creamos clases derivadas Circulo, Rectangulo, y Triangulo, cada una implementando su propia lógica para los métodos.
Implementamos y usamos los métodos calcularArea() y dibujar() para cada tipo de figura, demostrando la capacidad del polimorfismo para manejar diferentes objetos de manera uniforme.
El polimorfismo es un concepto fundamental en la programación orientada a objetos que permite que las clases tengan diferentes implementaciones de un método bajo la misma interfaz o estructura. La palabra "polimorfismo" proviene del griego y significa "muchas formas", lo que implica que el mismo método puede actuar de manera diferente según el objeto que lo utilice. Esto es crucial para la flexibilidad y extensibilidad de un sistema, ya que permite tratar objetos de diferentes clases de manera uniforme.
Polimorfismo en tiempo de compilación (estático): Este tipo de polimorfismo se logra mediante la sobrecarga de métodos y operadores. Los métodos sobrecargados comparten el mismo nombre pero tienen diferentes firmas (tipo o número de parámetros).
Polimorfismo en tiempo de ejecución (dinámico): Este tipo de polimorfismo se logra mediante la sobrescritura de métodos. Las clases derivadas pueden tener sus propias implementaciones de un método definido en la clase base.
Supongamos que estamos creando una aplicación para gestionar diferentes tipos de cuentas de usuario (Usuario regular, Usuario Premium y Usuario Administrador). Utilizaremos el polimorfismo para definir diferentes comportamientos para cada tipo de usuario al enviar una notificación.
javascript
// Definición de la clase base Usuario
class Usuario {
constructor(nombre) {
this.nombre = nombre;
}
enviarNotificacion(mensaje) {
console.log(`${this.nombre} recibió una notificación: ${mensaje}`);
}
}
// Clase derivada UsuarioRegular
class UsuarioRegular extends Usuario {
enviarNotificacion(mensaje) {
console.log(`(Regular) ${this.nombre} recibió un correo electrónico: ${mensaje}`);
}
}
// Clase derivada UsuarioPremium
class UsuarioPremium extends Usuario {
enviarNotificacion(mensaje) {
console.log(`(Premium) ${this.nombre} recibió una notificación por SMS: ${mensaje}`);
}
}
// Clase derivada UsuarioAdministrador
class UsuarioAdministrador extends Usuario {
enviarNotificacion(mensaje) {
console.log(`(Admin) ${this.nombre} recibió una alerta en la aplicación: ${mensaje}`);
}
}
// Crear objetos de cada clase de usuario
const usuario1 = new UsuarioRegular('Ana');
const usuario2 = new UsuarioPremium('Carlos');
const usuario3 = new UsuarioAdministrador('Elena');
// Enviar notificaciones
usuario1.enviarNotificacion('Tu pedido ha sido enviado.');
// Output: (Regular) Ana recibió un correo electrónico: Tu pedido ha sido enviado.
usuario2.enviarNotificacion('Tu pedido ha sido entregado.');
// Output: (Premium) Carlos recibió una notificación por SMS: Tu pedido ha sido entregado.
usuario3.enviarNotificacion('Hay una nueva actualización disponible.');
// Output: (Admin) Elena recibió una alerta en la aplicación: Hay una nueva actualización disponible.
En este ejemplo:
Definimos una clase base Usuario con un método enviarNotificacion que se encarga de enviar una notificación al usuario.
Creamos clases derivadas UsuarioRegular, UsuarioPremium y UsuarioAdministrador, cada una con su propia implementación del método enviarNotificacion.
El método enviarNotificacion está sobrescrito en cada clase derivada para proporcionar un comportamiento específico según el tipo de usuario.
Creamos objetos de cada tipo de usuario y utilizamos el método enviarNotificacion para demostrar cómo cada usuario recibe la notificación de manera diferente.
Ahora es tu turno. Crea una clase Figura para una aplicación de dibujo con los siguientes requisitos:
Atributos: nombre.
Métodos:
calcularArea(): Método abstracto que será sobrescrito por las clases derivadas.
dibujar(): Método abstracto que será sobrescrito por las clases derivadas.
Crea clases derivadas Circulo, Rectangulo, y Triangulo, cada una con su propia implementación de calcularArea() y dibujar().
javascript
// Definición de la clase base Figura
class Figura {
constructor(nombre) {
this.nombre = nombre;
}
calcularArea() {
throw new Error("Este método debe ser implementado por una clase derivada.");
}
dibujar() {
throw new Error("Este método debe ser implementado por una clase derivada.");
}
}
// Clase derivada Circulo
class Circulo extends Figura {
constructor(nombre, radio) {
super(nombre);
this.radio = radio;
}
calcularArea() {
return Math.PI * this.radio ** 2;
}
dibujar() {
console.log(`Dibujando un círculo llamado ${this.nombre} con área ${this.calcularArea()}`);
}
}
// Clase derivada Rectangulo
class Rectangulo extends Figura {
constructor(nombre, ancho, alto) {
super(nombre);
this.ancho = ancho;
this.alto = alto;
}
calcularArea() {
return this.ancho * this.alto;
}
dibujar() {
console.log(`Dibujando un rectángulo llamado ${this.nombre} con área ${this.calcularArea()}`);
}
}
// Clase derivada Triangulo
class Triangulo extends Figura {
constructor(nombre, base, altura) {
super(nombre);
this.base = base;
this.altura = altura;
}
calcularArea() {
return (this.base * this.altura) / 2;
}
dibujar() {
console.log(`Dibujando un triángulo llamado ${this.nombre} con área ${this.calcularArea()}`);
}
}
// Crear objetos de cada clase de figura
const circulo1 = new Circulo('Círculo1', 5);
const rectangulo1 = new Rectangulo('Rectángulo1', 4, 6);
const triangulo1 = new Triangulo('Triángulo1', 3, 4);
// Usar los métodos calcularArea y dibujar
circulo1.dibujar(); // Output: Dibujando un círculo llamado Círculo1 con área 78.53981633974483
rectangulo1.dibujar(); // Output: Dibujando un rectángulo llamado Rectángulo1 con área 24
triangulo1.dibujar(); // Output: Dibujando un triángulo llamado Triángulo1 con área 6
Definimos la clase base Figura con métodos abstractos calcularArea() y dibujar().
Creamos clases derivadas Circulo, Rectangulo, y Triangulo, cada una implementando su propia lógica para los métodos.
Implementamos y usamos los métodos calcularArea() y dibujar() para cada tipo de figura, demostrando la capacidad del polimorfismo para manejar diferentes objetos de manera uniforme.
Hoy nos enfocaremos en crear y practicar con clases simples, atributos y métodos en JavaScript. Estos ejercicios te ayudarán a solidificar tu comprensión de los conceptos básicos de POO.
Crea una clase Animal con los siguientes requisitos:
Atributos: nombre, especie, edad.
Métodos:
comer: Muestra un mensaje indicando que el animal está comiendo.
dormir: Muestra un mensaje indicando que el animal está durmiendo.
hacerSonido: Muestra un mensaje indicando el sonido que hace el animal.
class Animal {
constructor(nombre, especie, edad) {
this.nombre = nombre;
this.especie = especie;
this.edad = edad;
}
comer() {
console.log(`${this.nombre} está comiendo.`);
}
dormir() {
console.log(`${this.nombre} está durmiendo.`);
}
hacerSonido() {
console.log(`${this.nombre} hace un sonido.`);
}
}
// Crear un objeto de la clase Animal
const animal1 = new Animal('Luna', 'Gato', 3);
// Usar los métodos
animal1.comer(); // Output: Luna está comiendo.
animal1.dormir(); // Output: Luna está durmiendo.
animal1.hacerSonido(); // Output: Luna hace un sonido.
Crea una clase Estudiante con los siguientes requisitos:
Atributos: nombre, matricula, carrera.
Métodos:
estudiar: Muestra un mensaje indicando que el estudiante está estudiando.
presentarExamen: Muestra un mensaje indicando que el estudiante está presentando un examen.
mostrarInformacion: Muestra la información del estudiante.
class Estudiante {
constructor(nombre, matricula, carrera) {
this.nombre = nombre;
this.matricula = matricula;
this.carrera = carrera;
}
estudiar() {
console.log(`${this.nombre} está estudiando.`);
}
presentarExamen() {
console.log(`${this.nombre} está presentando un examen.`);
}
mostrarInformacion() {
console.log(`Nombre: ${this.nombre}, Matrícula: ${this.matricula}, Carrera: ${this.carrera}`);
}
}
// Crear un objeto de la clase Estudiante
const estudiante1 = new Estudiante('Carlos', '123456', 'Ingeniería');
// Usar los métodos
estudiante1.estudiar(); // Output: Carlos está estudiando.
estudiante1.presentarExamen(); // Output: Carlos está presentando un examen.
estudiante1.mostrarInformacion(); // Output: Nombre: Carlos, Matrícula: 123456, Carrera: Ingeniería.
Crea una clase Libro con los siguientes requisitos:
Atributos: titulo, autor, paginas.
Métodos:
leerPagina: Muestra un mensaje indicando que estás leyendo una página del libro.
mostrarInformacion: Muestra la información del libro.
terminarLibro: Muestra un mensaje indicando que has terminado de leer el libro.
class Libro {
constructor(titulo, autor, paginas) {
this.titulo = titulo;
this.autor = autor;
this.paginas = paginas;
}
leerPagina() {
console.log(`Estás leyendo una página de "${this.titulo}".`);
}
mostrarInformacion() {
console.log(`Título: ${this.titulo}, Autor: ${this.autor}, Páginas: ${this.paginas}`);
}
terminarLibro() {
console.log(`Has terminado de leer "${this.titulo}".`);
}
}
// Crear un objeto de la clase Libro
const libro1 = new Libro('1984', 'George Orwell', 328);
// Usar los métodos
libro1.leerPagina(); // Output: Estás leyendo una página de "1984".
libro1.mostrarInformacion(); // Output: Título: 1984, Autor: George Orwell, Páginas: 328.
libro1.terminarLibro(); // Output: Has terminado de leer "1984".
Crea una clase Coche con los siguientes requisitos:
Atributos: marca, modelo, año.
Métodos:
arrancar: Muestra un mensaje indicando que el coche ha arrancado.
detener: Muestra un mensaje indicando que el coche se ha detenido.
mostrarInformacion: Muestra la información del coche.
class Coche {
constructor(marca, modelo, año) {
this.marca = marca;
this.modelo = modelo;
this.año = año;
}
arrancar() {
console.log(`El coche ${this.marca} ${this.modelo} ha arrancado.`);
}
detener() {
console.log(`El coche ${this.marca} ${this.modelo} se ha detenido.`);
}
mostrarInformacion() {
console.log(`Marca: ${this.marca}, Modelo: ${this.modelo}, Año: ${this.año}`);
}
}
// Crear un objeto de la clase Coche
const coche1 = new Coche('Toyota', 'Corolla', 2020);
// Usar los métodos
coche1.arrancar(); // Output: El coche Toyota Corolla ha arrancado.
coche1.detener(); // Output: El coche Toyota Corolla se ha detenido.
coche1.mostrarInformacion(); // Output: Marca: Toyota, Modelo: Corolla, Año: 2020.
Crea una clase Persona con los siguientes requisitos:
Atributos: nombre, edad, genero.
Métodos:
presentarse: Muestra un mensaje presentándose a sí mismo.
cumplirAños: Incrementa la edad en 1 y muestra un mensaje de cumpleaños.
mostrarInformacion: Muestra la información de la persona.
javascript
Copiar código
class Persona {
constructor(nombre, edad, genero) {
this.nombre = nombre;
this.edad = edad;
this.genero = genero;
}
presentarse() {
console.log(`Hola, me llamo ${this.nombre}.`);
}
cumplirAños() {
this.edad += 1;
console.log(`¡Feliz cumpleaños, ${this.nombre}! Ahora tienes ${this.edad} años.`);
}
mostrarInformacion() {
console.log(`Nombre: ${this.nombre}, Edad: ${this.edad}, Género: ${this.genero}`);
}
}
// Crear un objeto de la clase Persona
const persona1 = new Persona('Ana', 25, 'Femenino');
// Usar los métodos
persona1.presentarse(); // Output: Hola, me llamo Ana.
persona1.cumplirAños(); // Output: ¡Feliz cumpleaños, Ana! Ahora tienes 26 años.
persona1.mostrarInformacion(); // Output: Nombre: Ana, Edad: 26, Género: Femenino.
Estos ejercicios prácticos te ayudarán a reforzar los conceptos de clases, atributos y métodos en JavaScript. Practica creando más clases y objetos según tus intereses y necesidades para mejorar tus habilidades en POO. ¡Buena suerte!
Antes de hacer el cuestionario, repasemos los conceptos clave que hemos cubierto hasta ahora:
Clases y Objetos:
Clase: Plantilla o modelo que define propiedades y métodos.
Objeto: Instancia de una clase.
Atributos y Métodos:
Atributos: Propiedades de un objeto.
Métodos: Funciones asociadas a un objeto.
Encapsulamiento:
Ocultar los detalles internos y exponer solo lo necesario.
Abstracción:
Simplificar la complejidad mostrando solo los detalles esenciales.
Herencia:
Permite que una clase herede propiedades y métodos de otra.
Polimorfismo:
Permite que una misma operación se ejecute de diferentes maneras en distintas clases.
Pregunta 1: ¿Qué es una clase en POO?
a) Un tipo de dato que contiene un solo valor.
b) Una plantilla para crear objetos.
c) Una función que se ejecuta automáticamente.
d) Un método para ocultar información.
Pregunta 2: ¿Qué es un objeto en POO?
a) Una instancia de una clase.
b) Un conjunto de funciones.
c) Una variable global.
d) Una operación matemática.
Pregunta 3: ¿Qué es el encapsulamiento?
a) Proceso de heredar propiedades de una clase.
b) Técnica para ocultar los detalles internos y exponer solo lo necesario.
c) Crear múltiples instancias de una clase.
d) Definir múltiples métodos con el mismo nombre.
Pregunta 4: ¿Qué es la abstracción?
a) La capacidad de un objeto para tomar muchas formas.
b) La acción de definir clases y objetos.
c) Simplificar la complejidad mostrando solo los detalles esenciales.
d) Compartir métodos y propiedades entre clases.
Pregunta 5: ¿Cuál es la diferencia entre atributos y métodos?
a) Los atributos son funciones y los métodos son variables.
b) Los atributos son propiedades de un objeto y los métodos son funciones asociadas al objeto.
c) Los atributos son públicos y los métodos son privados.
d) No hay diferencia, son lo mismo.
Pregunta 6: ¿Qué es la herencia en POO?
a) La capacidad de definir una función dentro de una clase.
b) La técnica de ocultar los detalles internos de un objeto.
c) El mecanismo por el cual una clase puede heredar propiedades y métodos de otra clase.
d) La habilidad de un objeto para interactuar con otros objetos.
Pregunta 7: ¿Qué es el polimorfismo?
a) La capacidad de un objeto para cambiar su estado interno.
b) La capacidad de un objeto para tomar muchas formas.
c) La capacidad de una clase para heredar propiedades de otra clase.
d) La técnica de definir atributos y métodos.
b) Una plantilla para crear objetos.
a) Una instancia de una clase.
b) Técnica para ocultar los detalles internos y exponer solo lo necesario.
c) Simplificar la complejidad mostrando solo los detalles esenciales.
b) Los atributos son propiedades de un objeto y los métodos son funciones asociadas al objeto.
c) El mecanismo por el cual una clase puede heredar propiedades y métodos de otra clase.
b) La capacidad de un objeto para tomar muchas formas.
Ejercicio 1: Crear una clase Empleado con los siguientes atributos y métodos:
Atributos: nombre, puesto, salario.
Métodos: trabajar (muestra un mensaje indicando que el empleado está trabajando), recibirSalario (muestra un mensaje indicando que el empleado ha recibido su salario), mostrarInformacion (muestra la información del empleado).
Ejercicio 2: Crear una clase Producto con los siguientes atributos y métodos:
Atributos: nombre, precio, cantidad.
Métodos: comprar (reduce la cantidad en 1 y muestra un mensaje), reponer (incrementa la cantidad en un valor dado y muestra un mensaje), mostrarInformacion (muestra la información del producto).
Estos ejercicios adicionales te ayudarán a consolidar tu comprensión de los conceptos de POO. ¡Sigue practicando y buena suerte con tus estudios!
La herencia avanzada amplía los conceptos básicos de herencia en POO para abordar relaciones más complejas entre clases y la sobrecarga de métodos. Estas técnicas permiten la creación de jerarquías de clases más ricas y la personalización del comportamiento de métodos en subclases, proporcionando flexibilidad y potencia en el diseño del software.
En herencia avanzada, las relaciones entre clases pueden volverse más complejas, como en el caso de la herencia múltiple (aunque JavaScript no la soporta directamente), la herencia en profundidad (donde las subclases derivan de otras subclases), y las relaciones de composición junto con la herencia.
Herencia en Profundidad: Implica la creación de una cadena de herencia donde una subclase hereda de otra subclase, formando una jerarquía de clases.
Composición vs. Herencia: A veces, es mejor usar composición (donde un objeto contiene otros objetos) en lugar de herencia para evitar problemas como la rigidez de la jerarquía de clases o el "problema del diamante" en lenguajes con herencia múltiple.
La sobrecarga de métodos ocurre cuando una clase tiene varios métodos con el mismo nombre, pero diferentes parámetros (tipo o número). Aunque JavaScript no soporta la sobrecarga de métodos de manera nativa como otros lenguajes (por ejemplo, Java o C++), se puede simular mediante patrones de diseño, utilizando la flexibilidad de los argumentos en JavaScript.
Vamos a crear un sistema de gestión de empleados en una empresa, con una jerarquía de clases que incluye empleados, gerentes, y directores, demostrando la herencia en profundidad y la sobrecarga de métodos.
Paso 1: Definir la Clase Base Empleado
javascript
// Clase base Empleado
class Empleado {
constructor(nombre, salario) {
this.nombre = nombre;
this.salario = salario;
}
// Método para calcular el bono anual
calcularBono(bono = 0.1) {
return this.salario * bono;
}
// Método para mostrar la información del empleado
mostrarInfo() {
console.log(`Empleado: ${this.nombre}, Salario: ${this.salario}`);
}
}
Paso 2: Crear una Subclase Gerente que Herede de Empleado
javascript
// Subclase Gerente que hereda de Empleado
class Gerente extends Empleado {
constructor(nombre, salario, departamento) {
super(nombre, salario);
this.departamento = departamento;
}
// Sobrescribir el método mostrarInfo para incluir el departamento
mostrarInfo() {
super.mostrarInfo();
console.log(`Departamento: ${this.departamento}`);
}
// Sobrecarga del método calcularBono
calcularBono(bono = 0.15, bonoExtra = 0) {
return super.calcularBono(bono) + bonoExtra;
}
}
Paso 3: Crear una Subclase Director que Herede de Gerente
javascript
// Subclase Director que hereda de Gerente
class Director extends Gerente {
constructor(nombre, salario, departamento, presupuesto) {
super(nombre, salario, departamento);
this.presupuesto = presupuesto;
}
// Sobrescribir el método mostrarInfo para incluir el presupuesto
mostrarInfo() {
super.mostrarInfo();
console.log(`Presupuesto: ${this.presupuesto}`);
}
// Sobrecarga del método calcularBono con un bono adicional para directores
calcularBono(bono = 0.2, bonoExtra = 5000) {
return super.calcularBono(bono, bonoExtra) + this.presupuesto * 0.01;
}
}
javascript
// Crear un objeto de la clase Empleado
const empleado1 = new Empleado('Juan', 30000);
empleado1.mostrarInfo(); // Output: Empleado: Juan, Salario: 30000
console.log(`Bono: ${empleado1.calcularBono()}`); // Output: Bono: 3000
// Crear un objeto de la clase Gerente
const gerente1 = new Gerente('Ana', 50000, 'Ventas');
gerente1.mostrarInfo();
// Output:
// Empleado: Ana, Salario: 50000
// Departamento: Ventas
console.log(`Bono: ${gerente1.calcularBono()}`); // Output: Bono: 7500
// Crear un objeto de la clase Director
const director1 = new Director('Carlos', 80000, 'Marketing', 200000);
director1.mostrarInfo();
// Output:
// Empleado: Carlos, Salario: 80000
// Departamento: Marketing
// Presupuesto: 200000
console.log(`Bono: ${director1.calcularBono()}`); // Output: Bono: 20500
Herencia en Profundidad: Gerente hereda de Empleado, y Director hereda de Gerente, formando una cadena de herencia que muestra cómo cada clase añade y personaliza la funcionalidad.
Sobrecarga de Métodos: La clase Gerente y Director sobrecargan el método calcularBono para ofrecer diferentes cálculos basados en los parámetros adicionales, demostrando cómo se puede adaptar la funcionalidad heredada a las necesidades específicas de las subclases.
Uso de super: Utilizamos super para llamar a métodos de la clase base, extendiendo su comportamiento en las subclases.
Relaciones Complejas y Composición
A veces, es más adecuado usar composición en lugar de herencia para evitar la creación de jerarquías rígidas y para facilitar la reutilización de código.
Ejemplo de Composición en Lugar de Herencia
javascript
// Clase Departamento
class Departamento {
constructor(nombre) {
this.nombre = nombre;
}
mostrarInfo() {
console.log(`Departamento: ${this.nombre}`);
}
}
// Clase Empleado que utiliza Composición en lugar de Herencia
class EmpleadoConDepartamento {
constructor(nombre, salario, departamento) {
this.nombre = nombre;
this.salario = salario;
this.departamento = new Departamento(departamento);
}
mostrarInfo() {
console.log(`Empleado: ${this.nombre}, Salario: ${this.salario}`);
this.departamento.mostrarInfo();
}
}
// Crear un objeto de la clase EmpleadoConDepartamento
const empleadoConDepto = new EmpleadoConDepartamento('María', 40000, 'Recursos Humanos');
empleadoConDepto.mostrarInfo();
// Output:
// Empleado: María, Salario: 40000
// Departamento: Recursos Humanos
La herencia avanzada en POO permite crear sistemas más complejos y flexibles al aprovechar relaciones profundas entre clases y la sobrecarga de métodos. Sin embargo, también es importante considerar cuándo usar composición para evitar problemas comunes en jerarquías de herencia.
Crear una subclase JefeEquipo que herede de Gerente y añada un atributo equipo (array de nombres). Sobrescribe el método mostrarInfo para mostrar los nombres de los miembros del equipo.
// Subclase Gerente que hereda de Empleado
class Gerente extends Empleado {
constructor(nombre, salario, departamento) {
super(nombre, salario);
this.departamento = departamento;
}
// Sobrescribir el método mostrarInfo para incluir el departamento
mostrarInfo() {
super.mostrarInfo();
console.log(`Departamento: ${this.departamento}`);
}
// Sobrecarga del método calcularBono
calcularBono(bono = 0.15, bonoExtra = 0) {
return super.calcularBono(bono) + bonoExtra;
}
}
// Subclase JefeEquipo que hereda de Gerente
class JefeEquipo extends Gerente {
constructor(nombre, salario, departamento, equipo = []) {
super(nombre, salario, departamento);
this.equipo = equipo; // Array de nombres de los miembros del equipo
}
// Sobrescribir el método mostrarInfo para incluir los nombres del equipo
mostrarInfo() {
super.mostrarInfo();
console.log(`Equipo: ${this.equipo.join(', ')}`);
}
// Sobrecarga del método calcularBono para incluir un bono basado en el tamaño del equipo
calcularBono(bono = 0.15, bonoExtra = 1000) {
const bonoPorMiembro = 500 * this.equipo.length; // Bono adicional por miembro del equipo
return super.calcularBono(bono, bonoExtra) + bonoPorMiembro;
}
}
// Crear un objeto de la clase JefeEquipo
const jefeEquipo1 = new JefeEquipo('Laura', 60000, 'Desarrollo', ['Carlos', 'Ana', 'Pedro']);
// Mostrar información del JefeEquipo
jefeEquipo1.mostrarInfo();
// Output:
// Empleado: Laura, Salario: 60000
// Departamento: Desarrollo
// Equipo: Carlos, Ana, Pedro
// Calcular y mostrar el bono del JefeEquipo
console.log(`Bono: ${jefeEquipo1.calcularBono()}`);
// Output: Bono: 13000 (7500 + 1000 + 1500 por cada miembro)
Experimentar con la Sobrecarga de Métodos en JefeEquipo para calcular el bono basado en la cantidad de miembros del equipo.
// Clase Cliente
class Cliente {
constructor(nombre, industria) {
this.nombre = nombre;
this.industria = industria;
}
mostrarInfo() {
console.log(`Cliente: ${this.nombre}, Industria: ${this.industria}`);
}
}
// Clase Empleado
class Empleado {
constructor(nombre, rol) {
this.nombre = nombre;
this.rol = rol;
}
mostrarInfo() {
console.log(`Empleado: ${this.nombre}, Rol: ${this.rol}`);
}
}
// Clase Proyecto que usa Composición
class Proyecto {
constructor(nombre, cliente, empleados = []) {
this.nombre = nombre;
this.cliente = cliente; // Composición: Proyecto tiene un Cliente
this.empleados = empleados; // Composición: Proyecto tiene varios Empleados
}
// Método para agregar un empleado al proyecto
agregarEmpleado(empleado) {
this.empleados.push(empleado);
}
// Método para mostrar la información del proyecto
mostrarInfo() {
console.log(`Proyecto: ${this.nombre}`);
this.cliente.mostrarInfo();
console.log('Empleados en el proyecto:');
this.empleados.forEach(empleado => empleado.mostrarInfo());
}
}
//Crear y Probar un Objeto de la Clase Proyecto
javascript
// Crear un objeto de la clase Cliente
const cliente1 = new Cliente('Empresa ABC', 'Tecnología');
// Crear objetos de la clase Empleado
const empleado1 = new Empleado('Juan', 'Desarrollador');
const empleado2 = new Empleado('Sara', 'Diseñadora');
const empleado3 = new Empleado('Luis', 'Analista');
// Crear un objeto de la clase Proyecto
const proyecto1 = new Proyecto('Sistema de Gestión', cliente1, [empleado1, empleado2]);
// Agregar un nuevo empleado al proyecto
proyecto1.agregarEmpleado(empleado3);
// Mostrar información del Proyecto
proyecto1.mostrarInfo();
// Output:
// Proyecto: Sistema de Gestión
// Cliente: Empresa ABC, Industria: Tecnología
// Empleados en el proyecto:
// Empleado: Juan, Rol: Desarrollador
// Empleado: Sara, Rol: Diseñadora
// Empleado: Luis, Rol: Analista
Implementar un ejemplo de Composición donde un Proyecto tiene un Cliente y varios Empleados asociados. Define clases y métodos apropiados para gestionar esta relación.
El polimorfismo avanzado en POO abarca la capacidad de sobrescribir métodos, sobrecargar métodos y aplicar polimorfismo dinámico. Estos conceptos permiten que los objetos de diferentes clases respondan a las mismas llamadas de métodos de maneras específicas, dependiendo de la implementación de cada clase.
Sobrecarga de Métodos: Definir múltiples versiones de un método en la misma clase, pero con diferentes parámetros. Aunque JavaScript no soporta sobrecarga de métodos de manera nativa como otros lenguajes (por ejemplo, Java o C++), se puede simular.
Sobrescritura de Métodos: Modificar la implementación de un método heredado de una clase base en una subclase.
Polimorfismo Dinámico: Permite que el comportamiento de un método sea determinado en tiempo de ejecución, en lugar de en tiempo de compilación. En JavaScript, esto se logra mediante la sobrescritura de métodos en subclases.
Aunque JavaScript no soporta la sobrecarga de métodos de manera nativa, se puede simular mediante el uso de parámetros opcionales y lógica dentro del método.
javascript
class Calculadora {
// Método sumar con soporte para diferentes números de parámetros
sumar(a, b, c) {
if (c !== undefined) {
return a + b + c;
} else {
return a + b;
}
}
}
// Crear una instancia de Calculadora
const calculadora = new Calculadora();
console.log(calculadora.sumar(1, 2)); // Output: 3
console.log(calculadora.sumar(1, 2, 3)); // Output: 6
La sobrescritura de métodos es cuando una subclase redefine un método heredado de su superclase. Esto permite que las subclases personalicen o extiendan la funcionalidad de los métodos de la clase base.
javascript
// Clase base Animal
class Animal {
hacerSonido() {
console.log("El animal hace un sonido");
}
}
// Subclase Perro que sobrescribe el método hacerSonido
class Perro extends Animal {
hacerSonido() {
console.log("El perro ladra");
}
}
// Subclase Gato que sobrescribe el método hacerSonido
class Gato extends Animal {
hacerSonido() {
console.log("El gato maúlla");
}
}
// Crear instancias de Perro y Gato
const perro = new Perro();
const gato = new Gato();
perro.hacerSonido(); // Output: El perro ladra
gato.hacerSonido(); // Output: El gato maúlla
El polimorfismo dinámico permite que el comportamiento de los métodos sea determinado en tiempo de ejecución, basándose en el tipo de objeto que invoca el método.
javascript
// Clase base Empleado
class Empleado {
constructor(nombre, salario) {
this.nombre = nombre;
this.salario = salario;
}
// Método calcularBono
calcularBono() {
return this.salario * 0.1;
}
// Método mostrarInfo
mostrarInfo() {
console.log(`${this.nombre} tiene un salario de ${this.salario}`);
}
}
// Subclase Gerente que sobrescribe calcularBono
class Gerente extends Empleado {
calcularBono() {
return this.salario * 0.2;
}
}
// Subclase Director que sobrescribe calcularBono
class Director extends Empleado {
calcularBono() {
return this.salario * 0.3 + 5000;
}
}
// Función que aplica polimorfismo dinámico
function mostrarBono(empleado) {
console.log(`${empleado.nombre} recibirá un bono de ${empleado.calcularBono()}`);
}
// Crear instancias de Empleado, Gerente y Director
const empleado1 = new Empleado('Juan', 30000);
const gerente1 = new Gerente('Ana', 50000);
const director1 = new Director('Carlos', 80000);
// Usar la función mostrarBono para demostrar polimorfismo dinámico
mostrarBono(empleado1); // Output: Juan recibirá un bono de 3000
mostrarBono(gerente1); // Output: Ana recibirá un bono de 10000
mostrarBono(director1); // Output: Carlos recibirá un bono de 29000
Crear una clase Vehiculo con un método mover(). Sobrescribe este método en subclases Coche, Bicicleta, y Barco para que cada subclase tenga una implementación específica del método mover().
Simular la sobrecarga de métodos en una clase Matematica con un método multiplicar que acepte dos o tres parámetros.
Crear una función que acepte un array de objetos Vehiculo y use polimorfismo dinámico para llamar al método mover() de cada uno, mostrando cómo cada vehículo se mueve de manera diferente.
1. Sobrescritura de Métodos en Subclases de Vehiculo
javascript
// Clase base Vehiculo
class Vehiculo {
mover() {
console.log("El vehículo se mueve");
}
}
// Subclase Coche que sobrescribe mover()
class Coche extends Vehiculo {
mover() {
console.log("El coche está conduciendo en la carretera");
}
}
// Subclase Bicicleta que sobrescribe mover()
class Bicicleta extends Vehiculo {
mover() {
console.log("La bicicleta está pedaleando en el carril bici");
}
}
// Subclase Barco que sobrescribe mover()
class Barco extends Vehiculo {
mover() {
console.log("El barco está navegando en el mar");
}
}
// Crear instancias de las subclases
const coche = new Coche();
const bicicleta = new Bicicleta();
const barco = new Barco();
// Mostrar cómo se mueven los diferentes vehículos
coche.mover(); // Output: El coche está conduciendo en la carretera
bicicleta.mover(); // Output: La bicicleta está pedaleando en el carril bici
barco.mover(); // Output: El barco está navegando en el mar
2. Simulación de Sobrecarga de Métodos en Matematica
javascript
// Clase Matematica con simulación de sobrecarga en multiplicar
class Matematica {
multiplicar(a, b, c) {
if (c !== undefined) {
return a * b * c;
} else {
return a * b;
}
}
}
// Crear una instancia de Matematica
const matematica = new Matematica();
console.log(matematica.multiplicar(2, 3)); // Output: 6
console.log(matematica.multiplicar(2, 3, 4)); // Output: 24
3. Uso de Polimorfismo Dinámico con Vehiculo
javascript
// Función que usa polimorfismo dinámico para mover diferentes vehículos
function moverVehiculos(vehiculos) {
vehiculos.forEach(vehiculo => {
vehiculo.mover();
});
}
// Crear un array de objetos Vehiculo (Coche, Bicicleta, Barco)
const vehiculos = [new Coche(), new Bicicleta(), new Barco()];
// Mover todos los vehículos usando polimorfismo dinámico
moverVehiculos(vehiculos);
// Output:
// El coche está conduciendo en la carretera
// La bicicleta está pedaleando en el carril bici
// El barco está navegando en el mar
El polimorfismo avanzado es una técnica poderosa en POO que permite escribir código más flexible y reutilizable. La sobrescritura de métodos, la simulación de sobrecarga de métodos y el polimorfismo dinámico son herramientas esenciales para crear sistemas más dinámicos y adaptables.
¡Practica estos conceptos para afianzar tu comprensión y aplicarlos en proyectos más complejos!
Clases Abstractas:
No se pueden instanciar directamente.
Pueden contener métodos abstractos (sin implementación) y métodos concretos (con implementación).
Se utilizan como base para otras clases.
Interfaces:
Son una colección de métodos que una clase debe implementar.
En JavaScript, no hay soporte directo para interfaces, pero se puede simular con clases abstractas o convenciones.
Instanciación:
Clases Abstractas: No se pueden instanciar directamente.
Interfaces: No se pueden instanciar, solo se implementan.
Herencia/Implementación:
Clases Abstractas: Una clase puede heredar de una sola clase abstracta.
Interfaces: Una clase puede implementar múltiples interfaces.
Métodos:
Clases Abstractas: Pueden tener métodos con implementación.
Interfaces: No tienen implementación de métodos (en lenguajes que soportan interfaces directamente).
Aunque JavaScript no soporta directamente las clases abstractas como otros lenguajes (por ejemplo, Java), podemos simularlas usando clases base con métodos que lanzan errores si no son implementados por subclases.
// Definición de una clase abstracta Animal
class Animal {
constructor(nombre) {
if (this.constructor === Animal) {
throw new Error('No se puede instanciar una clase abstracta.');
}
this.nombre = nombre;
}
// Método abstracto
hacerSonido() {
throw new Error('Debe implementar el método hacerSonido.');
}
}
// Definición de la clase Perro que extiende Animal
class Perro extends Animal {
hacerSonido() {
console.log(`${this.nombre} ladra.`);
}
}
// Definición de la clase Gato que extiende Animal
class Gato extends Animal {
hacerSonido() {
console.log(`${this.nombre} maúlla.`);
}
}
// Crear objetos de las clases derivadas
const perro = new Perro('Rex');
const gato = new Gato('Misu');
// Usar los métodos
perro.hacerSonido(); // Output: Rex ladra.
gato.hacerSonido(); // Output: Misu maúlla.
En este ejemplo:
La clase Animal actúa como una clase abstracta.
Si intentamos crear una instancia de Animal, se lanzará un error.
Los métodos abstractos (hacerSonido) deben ser implementados por las subclases (Perro y Gato).
Ejemplo en JavaScript: Simulando Interfaces
Como JavaScript no tiene soporte nativo para interfaces, podemos usar clases y convenciones para simular su comportamiento.
// Definición de la "interfaz" Vehiculo
class Vehiculo {
// Método "interfaz" debe ser implementado
arrancar() {
throw new Error('Debe implementar el método arrancar.');
}
// Método "interfaz" debe ser implementado
detener() {
throw new Error('Debe implementar el método detener.');
}
}
// Clase Coche que implementa la "interfaz" Vehiculo
class Coche extends Vehiculo {
arrancar() {
console.log('El coche ha arrancado.');
}
detener() {
console.log('El coche se ha detenido.');
}
}
// Clase Bicicleta que implementa la "interfaz" Vehiculo
class Bicicleta extends Vehiculo {
arrancar() {
console.log('La bicicleta ha empezado a moverse.');
}
detener() {
console.log('La bicicleta se ha detenido.');
}
}
// Crear objetos de las clases
const coche = new Coche();
const bicicleta = new Bicicleta();
// Usar los métodos
coche.arrancar(); // Output: El coche ha arrancado.
coche.detener(); // Output: El coche se ha detenido.
bicicleta.arrancar(); // Output: La bicicleta ha empezado a moverse.
bicicleta.detener(); // Output: La bicicleta se ha detenido.
En este ejemplo:
La clase Vehiculo actúa como una "interfaz".
Las clases Coche y Bicicleta implementan los métodos arrancar y detener definidos en la "interfaz".
Clases Abstractas:
Cuando se tiene una funcionalidad base común y se quiere asegurar que ciertas clases deben implementar métodos específicos.
Cuando se desea proporcionar alguna implementación común a las subclases.
Interfaces (simuladas en JavaScript):
Cuando se desea definir un contrato que varias clases deben seguir sin proporcionar ninguna implementación común.
Cuando se necesita soportar múltiples herencias de comportamiento.
Ejercicio
Crea una clase abstracta Instrumento con un método abstracto tocar. Luego, crea dos subclases Guitarra y Piano que implementen el método tocar.
// Definición de la clase abstracta Instrumento
class Instrumento {
constructor(nombre) {
if (this.constructor === Instrumento) {
throw new Error('No se puede instanciar una clase abstracta.');
}
this.nombre = nombre;
}
// Método abstracto
tocar() {
throw new Error('Debe implementar el método tocar.');
}
}
// Clase Guitarra que extiende Instrumento
class Guitarra extends Instrumento {
tocar() {
console.log(`La ${this.nombre} está siendo tocada.`);
}
}
// Clase Piano que extiende Instrumento
class Piano extends Instrumento {
tocar() {
console.log(`El ${this.nombre} está siendo tocado.`);
}
}
// Crear objetos de las clases derivadas
const guitarra = new Guitarra('guitarra');
const piano = new Piano('piano');
// Usar los métodos
guitarra.tocar(); // Output: La guitarra está siendo tocada.
piano.tocar(); // Output: El piano está siendo tocado.
Estos ejercicios y ejemplos te ayudarán a comprender mejor las diferencias y usos de las clases abstractas e interfaces en la Programación Orientada a Objetos con JavaScript. ¡Sigue practicando y explorando! Mañana continuaremos con más principios de diseño.
Herencia y composición son dos formas de reutilizar código en la Programación Orientada a Objetos (POO).
Herencia: Permite que una clase (subclase) herede propiedades y métodos de otra clase (superclase). Es útil cuando hay una clara relación jerárquica entre las clases (por ejemplo, un perro es un animal).
Composición: Consiste en construir clases a partir de otras clases, es decir, una clase contiene instancias de otras clases. Es útil cuando se desea crear clases complejas a partir de componentes más pequeños y reutilizables (por ejemplo, un coche tiene un motor).
Herencia:
Representa una relación "es-un".
Facilita la reutilización de código a través de la herencia.
Puede llevar a un acoplamiento fuerte entre las clases.
Puede resultar en jerarquías de clases complejas y difíciles de mantener.
Composición:
Representa una relación "tiene-un".
Facilita la reutilización de código a través de la agregación de componentes.
Promueve un acoplamiento más flexible y modular.
Facilita el mantenimiento y la escalabilidad del sistema.
Supongamos que tenemos una clase Animal y queremos crear una subclase Perro que herede de Animal.
// Definición de la clase base Animal
class Animal {
constructor(nombre) {
this.nombre = nombre;
}
hacerSonido() {
console.log(`${this.nombre} hace un sonido.`);
}
}
// Definición de la clase derivada Perro
class Perro extends Animal {
hacerSonido() {
console.log(`${this.nombre} ladra.`);
}
}
// Crear un objeto de la clase Perro
const perro = new Perro('Rex');
perro.hacerSonido(); // Output: Rex ladra.
En este ejemplo:
Perro hereda de Animal, lo que significa que Perro es un tipo de Animal.
Perro sobrescribe el método hacerSonido.
Supongamos que queremos crear una clase Coche que tenga un motor (Motor), ruedas (Rueda), y un sistema de entretenimiento (Entretenimiento).
// Definición de la clase Motor
class Motor {
encender() {
console.log('El motor está encendido.');
}
apagar() {
console.log('El motor está apagado.');
}
}
// Definición de la clase Rueda
class Rueda {
inflar() {
console.log('La rueda está inflada.');
}
desinflar() {
console.log('La rueda está desinflada.');
}
}
// Definición de la clase Entretenimiento
class Entretenimiento {
encender() {
console.log('El sistema de entretenimiento está encendido.');
}
apagar() {
console.log('El sistema de entretenimiento está apagado.');
}
}
// Definición de la clase Coche que usa composición
class Coche {
constructor() {
this.motor = new Motor();
this.ruedas = [new Rueda(), new Rueda(), new Rueda(), new Rueda()];
this.entretenimiento = new Entretenimiento();
}
encenderCoche() {
this.motor.encender();
this.entretenimiento.encender();
this.ruedas.forEach(rueda => rueda.inflar());
console.log('El coche está encendido.');
}
apagarCoche() {
this.motor.apagar();
this.entretenimiento.apagar();
this.ruedas.forEach(rueda => rueda.desinflar());
console.log('El coche está apagado.');
}
}
// Crear un objeto de la clase Coche
const coche = new Coche();
coche.encenderCoche();
// Output:
// El motor está encendido.
// El sistema de entretenimiento está encendido.
// La rueda está inflada.
// La rueda está inflada.
// La rueda está inflada.
// La rueda está inflada.
// El coche está encendido.
coche.apagarCoche();
// Output:
// El motor está apagado.
// El sistema de entretenimiento está apagado.
// La rueda está desinflada.
// La rueda está desinflada.
// La rueda está desinflada.
// La rueda está desinflada.
// El coche está apagado.
En este ejemplo:
La clase Coche está compuesta de instancias de Motor, Rueda, y Entretenimiento.
Esto permite construir la funcionalidad del coche a partir de componentes reutilizables.
Cuándo Usar Composición en Lugar de Herencia
Preferir Composición sobre Herencia:
Cuando la relación no es naturalmente jerárquica.
Cuando se desea mayor flexibilidad y modularidad.
Cuando se desea evitar las limitaciones de la herencia múltiple (no soportada directamente en muchos lenguajes).
Usar Herencia:
Cuando hay una clara relación "es-un".
Cuando se necesita reutilizar código común en subclases.
Crea una clase Casa que tenga Puertas, Ventanas, y Habitaciones. Implementa los métodos abrirPuertas, cerrarVentanas, y encenderLuces.
Clase Puerta:
Métodos: abrir, cerrar.
Clase Ventana:
Métodos: abrir, cerrar.
Clase Habitacion:
Métodos: encenderLuz, apagarLuz.
Clase Casa:
Atributos: puertas, ventanas, habitaciones.
Métodos: abrirPuertas, cerrarVentanas, encenderLuces.
// Definición de la clase Puerta
class Puerta {
abrir() {
console.log('La puerta está abierta.');
}
cerrar() {
console.log('La puerta está cerrada.');
}
}
// Definición de la clase Ventana
class Ventana {
abrir() {
console.log('La ventana está abierta.');
}
cerrar() {
console.log('La ventana está cerrada.');
}
}
// Definición de la clase Habitacion
class Habitacion {
encenderLuz() {
console.log('La luz de la habitación está encendida.');
}
apagarLuz() {
console.log('La luz de la habitación está apagada.');
}
}
// Definición de la clase Casa que usa composición
class Casa {
constructor() {
this.puertas = [new Puerta(), new Puerta(), new Puerta()];
this.ventanas = [new Ventana(), new Ventana(), new Ventana(), new Ventana()];
this.habitaciones = [new Habitacion(), new Habitacion(), new Habitacion()];
}
abrirPuertas() {
this.puertas.forEach(puerta => puerta.abrir());
}
cerrarVentanas() {
this.ventanas.forEach(ventana => ventana.cerrar());
}
encenderLuces() {
this.habitaciones.forEach(habitacion => habitacion.encenderLuz());
}
}
// Crear un objeto de la clase Casa
const casa = new Casa();
casa.abrirPuertas();
// Output:
// La puerta está abierta.
// La puerta está abierta.
// La puerta está abierta.
casa.cerrarVentanas();
// Output:
// La ventana está cerrada.
// La ventana está cerrada.
// La ventana está cerrada.
// La ventana está cerrada.
casa.encenderLuces();
// Output:
// La luz de la habitación está encendida.
// La luz de la habitación está encendida.
// La luz de la habitación está encendida.
Estos ejercicios y ejemplos te ayudarán a entender mejor cuándo usar composición y cuándo usar herencia en la Programación Orientada a Objetos. ¡Sigue practicando y explorando! Mañana continuaremos con más principios de diseño.
Los principios SOLID son un conjunto de cinco principios de diseño que guían a los desarrolladores a crear software más flexible, escalable, y fácil de mantener. Estos principios fueron formulados por Robert C. Martin, también conocido como "Uncle Bob". En esta primera parte, nos enfocaremos en los dos primeros principios: Single Responsibility Principle (SRP) y Open/Closed Principle (OCP).
El principio de responsabilidad única (SRP) establece que una clase debe tener una sola razón para cambiar, lo que significa que debe tener una única responsabilidad o propósito. Este principio promueve la cohesión en las clases y facilita el mantenimiento y la reutilización del código.
Ejemplo de Violación de SRP
javascript
class Reporte {
constructor(datos) {
this.datos = datos;
}
// Método para generar el reporte
generarReporte() {
// Lógica para generar el reporte
console.log("Generando reporte con los datos:", this.datos);
}
// Método para imprimir el reporte
imprimirReporte() {
// Lógica para imprimir el reporte
console.log("Imprimiendo reporte...");
}
// Método para guardar el reporte en un archivo
guardarReporte(rutaArchivo) {
// Lógica para guardar el reporte en un archivo
console.log(`Guardando el reporte en ${rutaArchivo}`);
}
}
En este ejemplo, la clase Reporte tiene múltiples responsabilidades: generar, imprimir y guardar un reporte. Esto viola el principio SRP.
Refactorización para Cumplir SRP
javascript
class Reporte {
constructor(datos) {
this.datos = datos;
}
// Método para generar el reporte
generarReporte() {
console.log("Generando reporte con los datos:", this.datos);
}
}
class Impresora {
imprimir(reporte) {
console.log("Imprimiendo reporte...");
}
}
class Guardador {
guardar(reporte, rutaArchivo) {
console.log(`Guardando el reporte en ${rutaArchivo}`);
}
}
// Uso de las clases refactorizadas
const reporte = new Reporte("Datos importantes");
const impresora = new Impresora();
const guardador = new Guardador();
reporte.generarReporte();
impresora.imprimir(reporte);
guardador.guardar(reporte, "reporte.txt");
Ahora, cada clase tiene una única responsabilidad: Reporte genera el reporte, Impresora lo imprime, y Guardador lo guarda, cumpliendo con SRP.
El principio de abierto/cerrado (OCP) establece que una clase debe estar abierta para su extensión pero cerrada para su modificación. Esto significa que deberías poder extender el comportamiento de una clase sin modificar su código fuente original.
Ejemplo de Violación de OCP
javascript
class Calculadora {
calcular(operacion, a, b) {
if (operacion === "sumar") {
return a + b;
} else if (operacion === "restar") {
return a - b;
} else if (operacion === "multiplicar") {
return a * b;
} else if (operacion === "dividir") {
return a / b;
}
}
}
const calculadora = new Calculadora();
console.log(calculadora.calcular("sumar", 5, 3)); // Output: 8
En este ejemplo, cada vez que se necesite una nueva operación, tendríamos que modificar la clase Calculadora, violando el principio OCP.
Refactorización para Cumplir OCP
javascript
// Clase base Operacion
class Operacion {
calcular(a, b) {
throw new Error("Este método debe ser implementado por subclases");
}
}
// Subclases para cada operación
class Sumar extends Operacion {
calcular(a, b) {
return a + b;
}
}
class Restar extends Operacion {
calcular(a, b) {
return a - b;
}
}
class Calculadora {
realizarCalculo(operacion, a, b) {
return operacion.calcular(a, b);
}
}
// Uso de las clases refactorizadas
const calculadora = new Calculadora();
console.log(calculadora.realizarCalculo(new Sumar(), 5, 3)); // Output: 8
console.log(calculadora.realizarCalculo(new Restar(), 5, 3)); // Output: 2
Ahora, la clase Calculadora está cerrada para modificaciones pero abierta para extensiones, ya que podemos añadir nuevas operaciones creando nuevas subclases de Operacion.
En esta segunda parte, cubriremos los tres principios restantes: Liskov Substitution Principle (LSP), Interface Segregation Principle (ISP), y Dependency Inversion Principle (DIP).
El principio de sustitución de Liskov (LSP) establece que las subclases deben ser sustituibles por sus clases base sin alterar el correcto funcionamiento del programa. En otras palabras, si S es una subclase de T, entonces los objetos de tipo T en un programa deberían poder ser reemplazados por objetos de tipo S sin modificar el comportamiento esperado.
javascript
class Rectangulo {
constructor(ancho, alto) {
this.ancho = ancho;
this.alto = alto;
}
calcularArea() {
return this.ancho * this.alto;
}
}
class Cuadrado extends Rectangulo {
constructor(lado) {
super(lado, lado);
}
setAncho(ancho) {
this.ancho = this.alto = ancho;
}
setAlto(alto) {
this.ancho = this.alto = alto;
}
}
function imprimirArea(rectangulo) {
rectangulo.setAncho(5);
rectangulo.setAlto(4);
console.log(rectangulo.calcularArea());
}
const rectangulo = new Rectangulo(2, 3);
const cuadrado = new Cuadrado(5);
imprimirArea(rectangulo); // Output: 20
imprimirArea(cuadrado); // Output: 16 (incorrecto)
En este ejemplo, la subclase Cuadrado rompe el LSP porque no se comporta de la misma manera que Rectangulo. Cuando tratamos Cuadrado como un Rectangulo, obtenemos resultados incorrectos.
Para cumplir con LSP, Cuadrado no debería heredar de Rectangulo si no puede cumplir con su comportamiento esperado. En su lugar, podemos replantear el diseño:
javascript
class Cuadrado {
constructor(lado) {
this.lado = lado;
}
calcularArea() {
return this.lado * this.lado;
}
}
// Uso adecuado sin herencia incorrecta
const cuadrado = new Cuadrado(5);
console.log(cuadrado.calcularArea()); // Output: 25
El principio de segregación de interfaces (ISP) establece que los clientes no deberían verse forzados a depender de interfaces que no utilizan. Es mejor tener muchas interfaces específicas y pequeñas que una única interfaz general y grande.
javascript
class Trabajador {
trabajar() {
// Implementación
}
comer() {
// Implementación
}
dormir() {
// Implementación
}
}
class Robot extends Trabajador {
dormir() {
// Los robots no duermen, pero aún tienen que implementar este método
throw new Error("Los robots no duermen");
}
}
Aquí, Robot se ve forzado a implementar un método dormir que no necesita, violando ISP.
javascript
// Interfaces específicas
class Trabajador {
trabajar() {
// Implementación
}
}
class SerHumano extends Trabajador {
comer() {
// Implementación
}
dormir() {
// Implementación
}
}
class Robot extends Trabajador {
// Sin necesidad de implementar métodos innecesarios
}
// Uso adecuado
const humano = new SerHumano();
const robot = new Robot();
humano.trabajar();
humano.dormir();
robot.trabajar();
Ahora, Robot solo depende de la interfaz que realmente utiliza.
El principio de inversión de dependencias (DIP) establece que las clases de alto nivel no deberían depender de clases de bajo nivel, sino de abstracciones. Además, las abstracciones no deberían depender de los detalles; los detalles deberían depender de las abstracciones.
Ejemplo de Violación de DIP
javascript
class Motor {
encender() {
console.log("Motor encendido");
}
}
class Coche {
constructor() {
this.motor = new Motor();
}
arrancar() {
this.motor.encender();
}
}
Aquí, Coche depende directamente de la clase Motor, lo que dificulta cambiar el tipo de motor en el futuro.
javascript
// Abstracción del Motor
class IMotor {
encender() {
throw new Error("Este método debe ser implementado por subclases");
}
}
// Implementaciones de motores específicos
class MotorGasolina extends IMotor {
encender() {
console.log("Motor de gasolina encendido");
}
}
class MotorElectrico extends IMotor {
encender() {
console.log("Motor eléctrico encendido");
}
}
// Clase Coche que depende de la abstracción
class Coche {
constructor(motor) {
this.motor = motor;
}
arrancar() {
this.motor.encender();
}
}
// Uso adecuado
const cocheGasolina = new Coche(new MotorGasolina());
cocheGasolina.arrancar(); // Output: Motor de gasolina encendido
const cocheElectrico = new Coche(new MotorElectrico());
cocheElectrico.arrancar(); // Output: Motor eléctrico encendido
Ahora, Coche depende de la abstracción IMotor, lo que facilita cambiar el tipo de motor sin modificar la clase Coche.
Los principios SOLID son fundamentales para escribir código robusto, mantenible y extensible. Aplicando estos principios, puedes diseñar sistemas de software que sean más fáciles de entender, modificar y escalar. A medida que practiques y apliques estos principios, mejorarás significativamente la calidad de tu código.
¡Esto concluye nuestra sesión sobre los principios SOLID! Continúa practicando estos conceptos en tus proyectos para internalizarlos completamente.
Hoy es un día dedicado a repasar los conceptos avanzados que hemos cubierto en los últimos días y a poner a prueba tu comprensión con un cuestionario. Repasaremos los principios SOLID y los conceptos de polimorfismo avanzado, herencia avanzada, sobrecarga y sobrescritura de métodos.
Single Responsibility Principle (SRP): Una clase debe tener una única responsabilidad, o razón para cambiar.
Open/Closed Principle (OCP): Las clases deben estar abiertas para extensión pero cerradas para modificación.
Liskov Substitution Principle (LSP): Las subclases deben poder reemplazar a sus clases base sin alterar el comportamiento del programa.
Interface Segregation Principle (ISP): Los clientes no deberían estar forzados a depender de interfaces que no utilizan.
Dependency Inversion Principle (DIP): Las clases de alto nivel no deben depender de clases de bajo nivel; ambas deben depender de abstracciones.
Sobrecarga de Métodos: Crear múltiples métodos con el mismo nombre pero con diferentes parámetros. Aunque JavaScript no lo soporta de manera nativa, se puede simular.
Sobrescritura de Métodos: Permitir que una subclase provea una implementación específica de un método que ya existe en su superclase.
Polimorfismo Dinámico: La habilidad de invocar métodos en objetos de subclases a través de referencias a la clase base, permitiendo que el comportamiento específico se determine en tiempo de ejecución.
Herencia en Profundidad: Crear jerarquías de clases donde una subclase hereda de otra subclase.
Composición vs. Herencia: Usar composición cuando una relación "tiene un" es más adecuada que una relación "es un" para evitar problemas de rigidez en las jerarquías de herencia.
Ahora, pongamos a prueba lo que has aprendido. A continuación, te presento un conjunto de preguntas de opción múltiple y ejercicios de código para que evalúes tu comprensión.
¿Qué principio SOLID se viola si una clase tiene múltiples responsabilidades?
a) Open/Closed Principle
b) Liskov Substitution Principle
c) Single Responsibility Principle
d) Interface Segregation Principle
¿Cuál de las siguientes afirmaciones describe mejor el principio Open/Closed?
a) Una clase debe tener una sola responsabilidad.
b) Una clase debe estar abierta a modificación y cerrada a extensión.
c) Una clase debe estar abierta a extensión pero cerrada a modificación.
d) Una clase debe implementar múltiples interfaces pequeñas.
Si una subclase no puede sustituir a su superclase sin romper la funcionalidad del programa, ¿qué principio SOLID se está violando?
a) Dependency Inversion Principle
b) Liskov Substitution Principle
c) Single Responsibility Principle
d) Interface Segregation Principle
En el contexto de la herencia, ¿qué significa sobrescribir un método?
a) Crear un nuevo método con el mismo nombre pero diferente número de parámetros.
b) Redefinir un método heredado en una subclase para cambiar su comportamiento.
c) Crear un método con el mismo nombre en la misma clase pero diferente retorno.
d) Definir un método sin implementación en una clase base.
¿Qué es la sobrecarga de métodos?
a) Definir varios métodos con el mismo nombre pero diferentes firmas en la misma clase.
b) Reutilizar un método de una clase base en una subclase.
c) Evitar la implementación de métodos en clases abstractas.
d) Crear métodos que solo pueden ser llamados desde la clase base.
Refactoriza el siguiente código para que cumpla con el principio de responsabilidad única (SRP):
javascript
class Usuario {
constructor(nombre, email, password) {
this.nombre = nombre;
this.email = email;
this.password = password;
}
registrar() {
console.log(`Registrando usuario: ${this.nombre}`);
this.enviarEmail();
}
enviarEmail() {
console.log(`Enviando correo de confirmación a ${this.email}`);
}
}
Corrige el siguiente código para que cumpla con el principio Liskov Substitution (LSP):
javascript
Copiar código
class Pajaro {
volar() {
console.log("El pájaro está volando");
}
}
class Pinguino extends Pajaro {
volar() {
throw new Error("Los pingüinos no pueden volar");
}
}
const pinguino = new Pinguino();
pinguino.volar(); // Error: Los pingüinos no pueden volar
Aplica el principio de inversión de dependencias (DIP) al siguiente código:
javascript
class MySQLDatabase {
conectar() {
console.log("Conectando a la base de datos MySQL");
}
}
class Aplicacion {
constructor() {
this.database = new MySQLDatabase();
}
iniciar() {
this.database.conectar();
}
}
const app = new Aplicacion();
app.iniciar(); // Output: Conectando a la base de datos MySQL
Parte 1: Preguntas de Opción Múltiple
c) Single Responsibility Principle
c) Una clase debe estar abierta a extensión pero cerrada a modificación.
b) Liskov Substitution Principle
b) Redefinir un método heredado en una subclase para cambiar su comportamiento.
a) Definir varios métodos con el mismo nombre pero diferentes firmas en la misma clase.
Parte 2: Ejercicios de Código
1 Refactorización SRP:
javascript
class Usuario {
constructor(nombre, email, password) {
this.nombre = nombre;
this.email = email;
this.password = password;
}
registrar() {
console.log(`Registrando usuario: ${this.nombre}`);
}
}
class EmailService {
enviarEmail(email) {
console.log(`Enviando correo de confirmación a ${email}`);
}
}
const usuario = new Usuario('Juan', 'juan@example.com', 'password123');
const emailService = new EmailService();
usuario.registrar();
emailService.enviarEmail(usuario.email);
2 Cumplimiento de LSP:
javascript
class Ave {
moverse() {
console.log("El ave se está moviendo");
}
}
class Pinguino extends Ave {
moverse() {
console.log("El pingüino está caminando o nadando");
}
}
const pinguino = new Pinguino();
pinguino.moverse(); // Output: El pingüino está caminando o nadando
3 Cumplimiento de DIP:
javascript
class Database {
conectar() {
throw new Error("Este método debe ser implementado por subclases");
}
}
class MySQLDatabase extends Database {
conectar() {
console.log("Conectando a la base de datos MySQL");
}
}
class Aplicacion {
constructor(database) {
this.database = database;
}
iniciar() {
this.database.conectar();
}
}
const database = new MySQLDatabase();
const app = new Aplicacion(database);
app.iniciar(); // Output: Conectando a la base de datos MySQL
Este repaso y cuestionario deberían ayudarte a consolidar tu comprensión de los conceptos avanzados de POO y los principios SOLID. Continúa practicando para dominar estos principios y aplicarlos de manera efectiva en tus proyectos de software. ¡Buen trabajo!
Los patrones de diseño son soluciones reutilizables y probadas a problemas comunes que surgen en el diseño de software. Estos patrones son descripciones o plantillas que se pueden aplicar a situaciones específicas para resolver problemas de diseño de forma eficaz. Se dividen en tres categorías principales:
Patrones Creacionales: Se enfocan en la forma en que se crean los objetos.
Patrones Estructurales: Se enfocan en la composición de clases u objetos.
Patrones de Comportamiento: Se enfocan en la interacción y responsabilidad entre clases y objetos.
Reutilización de Soluciones: Proporcionan soluciones probadas y comprobadas a problemas comunes, evitando la necesidad de reinventar la rueda.
Estandarización: Ayudan a estandarizar el diseño de software, facilitando la comunicación entre desarrolladores.
Mantenimiento y Escalabilidad: Facilitan el mantenimiento y la escalabilidad del código al seguir estructuras claras y bien definidas.
Buenas Prácticas: Promueven buenas prácticas de diseño y programación, mejorando la calidad del software.
El patrón Singleton es un patrón creacional que garantiza que una clase solo tenga una instancia y proporciona un punto de acceso global a esa instancia.
// Implementación del patrón Singleton en JavaScript
class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
Singleton.instance = this;
}
someMethod() {
console.log('Método de la instancia Singleton');
}
}
// Intentar crear múltiples instancias
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // Output: true
instance1.someMethod(); // Output: Método de la instancia Singleton
instance2.someMethod(); // Output: Método de la instancia Singleton
En este ejemplo:
La clase Singleton se asegura de que solo exista una instancia de sí misma.
Cualquier intento de crear una nueva instancia devolverá la instancia existente.
Identifica un Problema Común: Piensa en un problema común que has enfrentado en tus proyectos de software.
Selecciona un Patrón: Selecciona un patrón de diseño que pueda resolver ese problema.
Implementa el Patrón: Implementa el patrón de diseño en JavaScript para resolver el problema identificado.
Ejemplo de Ejercicio
Problema Común: Necesitas asegurar que una clase de configuración solo tenga una instancia para evitar conflictos en la configuración.
Patrón Seleccionado: Singleton
Implementación del Patrón:
class Configuracion {
constructor() {
if (Configuracion.instance) {
return Configuracion.instance;
}
this.settings = {};
Configuracion.instance = this;
}
set(key, value) {
this.settings[key] = value;
}
get(key) {
return this.settings[key];
}
}
// Crear una única instancia de Configuracion
const config1 = new Configuracion();
const config2 = new Configuracion();
config1.set('theme', 'dark');
console.log(config2.get('theme')); // Output: dark
console.log(config1 === config2); // Output: true
En este ejercicio:
Implementamos el patrón Singleton para la clase Configuracion.
Aseguramos que solo haya una instancia de Configuracion.
La configuración establecida en config1 es accesible a través de config2, demostrando que ambos son la misma instancia.
Los patrones de diseño son herramientas poderosas en el desarrollo de software que permiten reutilizar soluciones probadas, mejorar la comunicación entre desarrolladores y facilitar el mantenimiento del código. A lo largo de esta semana, exploraremos varios patrones de diseño específicos y cómo implementarlos en JavaScript. ¡Sigue practicando y explorando!
El patrón Singleton es un patrón de diseño creacional que asegura que una clase tenga solo una instancia y proporciona un punto de acceso global a esa instancia. Es útil cuando se necesita exactamente un objeto para coordinar acciones en todo el sistema.
Control de Acceso a la Única Instancia: Permite un control estricto sobre cómo y cuándo se crea la única instancia de la clase.
Reducción de Uso de Memoria: Evita la creación de múltiples instancias de un objeto, reduciendo el uso de memoria.
Consistencia Global: Asegura que todos los componentes del sistema utilicen la misma instancia, garantizando consistencia.
Vamos a crear una clase Logger que sigue el patrón Singleton para asegurar que solo haya una instancia de logger en la aplicación.
javascript
Copiar código
class Logger {
constructor() {
if (Logger.instance) {
return Logger.instance;
}
// Inicializar el estado del logger
this.logs = [];
Logger.instance = this;
}
// Método para agregar un log
log(message) {
const timestamp = new Date().toISOString();
this.logs.push(`[${timestamp}] ${message}`);
console.log(`[${timestamp}] ${message}`);
}
// Método para obtener todos los logs
getLogs() {
return this.logs;
}
}
// Crear instancias de Logger
const logger1 = new Logger();
const logger2 = new Logger();
// Agregar logs
logger1.log('Primera entrada de log.');
logger2.log('Segunda entrada de log.');
// Comprobar que ambas instancias son iguales
console.log(logger1 === logger2); // Output: true
// Obtener logs de ambas instancias
console.log(logger1.getLogs());
console.log(logger2.getLogs());
En este ejemplo:
Logger se asegura de que solo haya una instancia mediante el uso de la propiedad estática Logger.instance.
Cualquier intento de crear una nueva instancia devuelve la instancia existente.
Los métodos log y getLogs permiten agregar y recuperar logs, demostrando que todas las instancias comparten el mismo estado.
Crea una clase Configuracion que siga el patrón Singleton. La clase debe permitir establecer y obtener configuraciones de la aplicación.
Clase Configuracion:
Método setConfig(key, value): Establece una configuración.
Método getConfig(key): Obtiene una configuración.
class Configuracion {
constructor() {
if (Configuracion.instance) {
return Configuracion.instance;
}
// Inicializar el estado de configuraciones
this.settings = {};
Configuracion.instance = this;
}
// Método para establecer una configuración
setConfig(key, value) {
this.settings[key] = value;
}
// Método para obtener una configuración
getConfig(key) {
return this.settings[key];
}
}
// Crear instancias de Configuracion
const config1 = new Configuracion();
const config2 = new Configuracion();
// Establecer configuraciones
config1.setConfig('theme', 'dark');
config2.setConfig('language', 'es');
// Comprobar que ambas instancias son iguales
console.log(config1 === config2); // Output: true
// Obtener configuraciones de ambas instancias
console.log(config1.getConfig('theme')); // Output: dark
console.log(config2.getConfig('language')); // Output: es
Manejo de Configuración: Asegurar que solo haya una instancia de configuración global para la aplicación.
Registro de Log: Mantener una única instancia de logger para registrar eventos de la aplicación.
Conexión a Bases de Datos: Garantizar que solo haya una instancia de conexión a la base de datos.
Gestión de Recursos: Controlar el acceso a recursos compartidos, como impresoras o archivos.
El patrón Singleton es una herramienta poderosa cuando se necesita asegurar que solo haya una instancia de una clase en todo el sistema. Sin embargo, debe usarse con cuidado, ya que puede llevar a problemas de prueba y dependencia si no se maneja correctamente. ¡Sigue practicando con este patrón y aplícalo en tus proyectos para ver sus beneficios! Mañana continuaremos con otro patrón de diseño.
El patrón Factory Method es un patrón de diseño creacional que define una interfaz para crear objetos en una clase, pero permite a las subclases alterar el tipo de objetos que se crearán. En lugar de instanciar objetos directamente usando el operador new, una fábrica delega la responsabilidad de la creación de objetos a métodos especializados.
Desacoplamiento: Desacopla el código de creación de objetos del código que usa esos objetos.
Extensibilidad: Facilita la extensión del código al permitir la adición de nuevas clases de productos sin modificar el código existente.
Control: Proporciona más control sobre el proceso de creación de objetos.
Implementación del Patrón Factory Method en JavaScript
Vamos a implementar una fábrica para crear diferentes tipos de Transporte (por ejemplo, Coche y Bicicleta).
Interfaz Transporte:
Método mover(): Debe ser implementado por todas las subclases.
Clase Coche:
Implementa Transporte.
Método mover(): Imprime un mensaje indicando que el coche se está moviendo.
Clase Bicicleta:
Implementa Transporte.
Método mover(): Imprime un mensaje indicando que la bicicleta se está moviendo.
Clase TransporteFactory:
Método crearTransporte(tipo): Devuelve una instancia de Coche o Bicicleta según el tipo proporcionado.
// Interfaz Transporte
class Transporte {
mover() {
throw new Error('Método mover() debe ser implementado.');
}
}
// Clase Coche que implementa Transporte
class Coche extends Transporte {
mover() {
console.log('El coche se está moviendo.');
}
}
// Clase Bicicleta que implementa Transporte
class Bicicleta extends Transporte {
mover() {
console.log('La bicicleta se está moviendo.');
}
}
// Clase TransporteFactory
class TransporteFactory {
crearTransporte(tipo) {
if (tipo === 'coche') {
return new Coche();
} else if (tipo === 'bicicleta') {
return new Bicicleta();
} else {
throw new Error('Tipo de transporte no soportado.');
}
}
}
// Uso de la fábrica
const factory = new TransporteFactory();
const coche = factory.crearTransporte('coche');
const bicicleta = factory.crearTransporte('bicicleta');
coche.mover(); // Output: El coche se está moviendo.
bicicleta.mover(); // Output: La bicicleta se está moviendo.
En este ejemplo:
La clase Transporte actúa como una interfaz con el método mover que debe ser implementado por todas las subclases.
Coche y Bicicleta son clases concretas que implementan Transporte.
TransporteFactory es la fábrica que crea instancias de Coche o Bicicleta según el tipo proporcionado.
Crea una Fábrica de Animales:
Interfaz Animal:
Método hacerSonido().
Clase Perro:
Implementa Animal.
Método hacerSonido(): Imprime "El perro ladra".
Clase Gato:
Implementa Animal.
Método hacerSonido(): Imprime "El gato maúlla".
Clase AnimalFactory:
Método crearAnimal(tipo): Devuelve una instancia de Perro o Gato.
// Interfaz Animal
class Animal {
hacerSonido() {
throw new Error('Método hacerSonido() debe ser implementado.');
}
}
// Clase Perro que implementa Animal
class Perro extends Animal {
hacerSonido() {
console.log('El perro ladra.');
}
}
// Clase Gato que implementa Animal
class Gato extends Animal {
hacerSonido() {
console.log('El gato maúlla.');
}
}
// Clase AnimalFactory
class AnimalFactory {
crearAnimal(tipo) {
if (tipo === 'perro') {
return new Perro();
} else if (tipo === 'gato') {
return new Gato();
} else {
throw new Error('Tipo de animal no soportado.');
}
}
}
// Uso de la fábrica
const animalFactory = new AnimalFactory();
const perro = animalFactory.crearAnimal('perro');
const gato = animalFactory.crearAnimal('gato');
perro.hacerSonido(); // Output: El perro ladra.
gato.hacerSonido(); // Output: El gato maúlla.
En este ejercicio:
Animal actúa como una interfaz con el método hacerSonido.
Perro y Gato son clases concretas que implementan Animal.
AnimalFactory es la fábrica que crea instancias de Perro o Gato según el tipo proporcionado.
Caso de Uso Común del Patrón Factory Method
Gestión de Conexiones de Base de Datos: Crear diferentes tipos de conexiones de base de datos según el tipo de base de datos (MySQL, PostgreSQL, MongoDB, etc.).
Creación de Interfaces Gráficas: Crear diferentes tipos de elementos de interfaz gráfica (botones, ventanas, menús) según el tipo de plataforma (Windows, macOS, Linux).
Manejo de Documentos: Crear diferentes tipos de documentos (PDF, Word, Excel) según el tipo de archivo requerido.
El patrón Factory Method es una herramienta poderosa para desacoplar el código de creación de objetos del código que usa esos objetos, mejorando la flexibilidad y extensibilidad del software. ¡Sigue practicando con este patrón y aplícalo en tus proyectos para ver sus beneficios! Mañana continuaremos con otro patrón de diseño.
El patrón Observer es un patrón de diseño de comportamiento que define una relación de dependencia uno-a-muchos entre objetos, de manera que cuando un objeto cambia de estado, todos sus dependientes son notificados y actualizados automáticamente. Este patrón es útil para implementar sistemas de notificación y suscripción.
Desacoplamiento: Desacopla los objetos que producen eventos de los objetos que reaccionan a esos eventos.
Flexibilidad: Permite agregar nuevos observadores sin modificar el código del sujeto.
Reusabilidad: Facilita la reutilización del código al permitir la suscripción de múltiples observadores a un mismo sujeto.
Vamos a implementar un ejemplo simple de un patrón Observer utilizando una clase Subject (Sujeto) y una clase Observer (Observador).
Clase Subject:
Métodos:
subscribe(observer): Añade un observador a la lista de observadores.
unsubscribe(observer): Elimina un observador de la lista.
notify(data): Notifica a todos los observadores sobre un cambio de estado.
Clase Observer:
Método:
update(data): Define la acción que el observador debe realizar cuando es notificado.
// Clase Subject
class Subject {
constructor() {
this.observers = [];
}
// Añade un observador
subscribe(observer) {
this.observers.push(observer);
}
// Elimina un observador
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
// Notifica a todos los observadores
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
// Clase Observer
class Observer {
constructor(name) {
this.name = name;
}
// Define la acción a realizar cuando es notificado
update(data) {
console.log(`${this.name} ha recibido la actualización: ${data}`);
}
}
// Crear instancias de Subject y Observer
const subject = new Subject();
const observer1 = new Observer('Observador 1');
const observer2 = new Observer('Observador 2');
// Suscribir observadores
subject.subscribe(observer1);
subject.subscribe(observer2);
// Notificar a los observadores
subject.notify('Evento 1');
// Output:
// Observador 1 ha recibido la actualización: Evento 1
// Observador 2 ha recibido la actualización: Evento 1
// Desuscribir un observador
subject.unsubscribe(observer1);
// Notificar nuevamente a los observadores
subject.notify('Evento 2');
// Output:
// Observador 2 ha recibido la actualización: Evento 2
En este ejemplo:
Subject mantiene una lista de observadores y proporciona métodos para suscribir, desuscribir y notificar a los observadores.
Observer define el método update que se llama cuando el sujeto notifica a los observadores.
Crea una aplicación simple de chat donde los usuarios puedan suscribirse a un canal de chat y recibir notificaciones cuando se envían mensajes.
Clase ChatChannel (Sujeto):
Métodos:
subscribe(user): Añade un usuario a la lista de suscriptores.
unsubscribe(user): Elimina un usuario de la lista.
notify(message): Notifica a todos los usuarios sobre un nuevo mensaje.
sendMessage(message): Envía un mensaje y notifica a los usuarios.
Clase User (Observador):
Método:
update(message): Define la acción que el usuario debe realizar cuando recibe un mensaje.
// Clase ChatChannel (Subject)
class ChatChannel {
constructor() {
this.users = [];
}
// Añade un usuario a la lista de suscriptores
subscribe(user) {
this.users.push(user);
}
// Elimina un usuario de la lista de suscriptores
unsubscribe(user) {
this.users = this.users.filter(usr => usr !== user);
}
// Notifica a todos los usuarios sobre un nuevo mensaje
notify(message) {
this.users.forEach(user => user.update(message));
}
// Envía un mensaje y notifica a los usuarios
sendMessage(message) {
console.log(`Mensaje enviado: ${message}`);
this.notify(message);
}
}
// Clase User (Observer)
class User {
constructor(name) {
this.name = name;
}
// Define la acción a realizar cuando recibe un mensaje
update(message) {
console.log(`${this.name} ha recibido el mensaje: ${message}`);
}
}
// Crear instancias de ChatChannel y User
const chatChannel = new ChatChannel();
const user1 = new User('Usuario 1');
const user2 = new User('Usuario 2');
// Suscribir usuarios al canal de chat
chatChannel.subscribe(user1);
chatChannel.subscribe(user2);
// Enviar mensajes y notificar a los usuarios
chatChannel.sendMessage('Hola a todos');
// Output:
// Mensaje enviado: Hola a todos
// Usuario 1 ha recibido el mensaje: Hola a todos
// Usuario 2 ha recibido el mensaje: Hola a todos
// Desuscribir un usuario del canal de chat
chatChannel.unsubscribe(user1);
// Enviar otro mensaje y notificar a los usuarios restantes
chatChannel.sendMessage('Hola de nuevo');
// Output:
// Mensaje enviado: Hola de nuevo
// Usuario 2 ha recibido el mensaje: Hola de nuevo
En este ejercicio:
ChatChannel actúa como el sujeto que mantiene una lista de usuarios y notifica cuando se envían mensajes.
User actúa como observador que recibe notificaciones de mensajes enviados en el canal de chat.
Casos de Uso Comunes del Patrón Observer
Sistemas de Eventos y Delegados: Manejo de eventos en interfaces gráficas de usuario.
Sistemas de Notificación: En aplicaciones de mensajería, correos electrónicos y notificaciones push.
Actualización Automática: Sincronización automática de datos entre diferentes partes de una aplicación.
El patrón Observer es una herramienta poderosa para implementar sistemas de notificación y suscripción, permitiendo desacoplar objetos que producen eventos de los objetos que reaccionan a esos eventos. ¡Sigue practicando con este patrón y aplícalo en tus proyectos para ver sus beneficios! Mañana continuaremos con otro patrón de diseño.
El patrón Decorator es un patrón de diseño estructural que permite añadir funcionalidades a un objeto de manera dinámica. Los decoradores proporcionan una alternativa flexible a la herencia para extender la funcionalidad de los objetos. Este patrón es útil cuando se necesita agregar responsabilidades a objetos individuales sin afectar otros objetos de la misma clase.
Flexibilidad: Permite extender la funcionalidad de un objeto sin modificar su código original.
Reusabilidad: Facilita la reutilización de código al combinar comportamientos de forma dinámica.
Desacoplamiento: Desacopla las responsabilidades adicionales de la clase principal.
Vamos a implementar un ejemplo simple de un patrón Decorator utilizando una clase base Café y varios decoradores para añadir diferentes ingredientes (como leche y azúcar).
Clase Café:
Método costo(): Devuelve el costo del café.
Método descripcion(): Devuelve la descripción del café.
Clase CaféDecorador:
Constructor: Recibe un objeto Café.
Método costo(): Llama al método costo del objeto Café.
Método descripcion(): Llama al método descripcion del objeto Café.
Clase Leche (Decorador):
Extiende CaféDecorador.
Añade el costo y la descripción de la leche al café.
Clase Azúcar (Decorador):
Extiende CaféDecorador.
Añade el costo y la descripción del azúcar al café.
// Clase base Café
class Cafe {
costo() {
return 5;
}
descripcion() {
return 'Café';
}
}
// Clase Decoradora base
class CafeDecorador {
constructor(cafe) {
this.cafe = cafe;
}
costo() {
return this.cafe.costo();
}
descripcion() {
return this.cafe.descripcion();
}
}
// Decorador Leche
class Leche extends CafeDecorador {
costo() {
return this.cafe.costo() + 1;
}
descripcion() {
return `${this.cafe.descripcion()} con leche`;
}
}
// Decorador Azúcar
class Azucar extends CafeDecorador {
costo() {
return this.cafe.costo() + 0.5;
}
descripcion() {
return `${this.cafe.descripcion()} con azúcar`;
}
}
// Uso de los decoradores
let cafe = new Cafe();
console.log(`${cafe.descripcion()}: $${cafe.costo()}`); // Output: Café: $5
cafe = new Leche(cafe);
console.log(`${cafe.descripcion()}: $${cafe.costo()}`); // Output: Café con leche: $6
cafe = new Azucar(cafe);
console.log(`${cafe.descripcion()}: $${cafe.costo()}`); // Output: Café con leche con azúcar: $6.5
En este ejemplo:
Cafe es la clase base que define un café simple.
CafeDecorador es la clase base para todos los decoradores que extenderán la funcionalidad de Cafe.
Leche y Azucar son decoradores concretos que añaden costo y descripción adicionales al café.
Crea una clase base Pizza y varios decoradores para añadir diferentes ingredientes (como pepperoni y champiñones).
Clase Pizza:
Método costo(): Devuelve el costo de la pizza.
Método descripcion(): Devuelve la descripción de la pizza.
Clase PizzaDecorador:
Constructor: Recibe un objeto Pizza.
Método costo(): Llama al método costo del objeto Pizza.
Método descripcion(): Llama al método descripcion del objeto Pizza.
Clase Pepperoni (Decorador):
Extiende PizzaDecorador.
Añade el costo y la descripción del pepperoni a la pizza.
Clase Champiñones (Decorador):
Extiende PizzaDecorador.
Añade el costo y la descripción de los champiñones a la pizza.
// Clase base Pizza
class Pizza {
costo() {
return 8;
}
descripcion() {
return 'Pizza básica';
}
}
// Clase Decoradora base
class PizzaDecorador {
constructor(pizza) {
this.pizza = pizza;
}
costo() {
return this.pizza.costo();
}
descripcion() {
return this.pizza.descripcion();
}
}
// Decorador Pepperoni
class Pepperoni extends PizzaDecorador {
costo() {
return this.pizza.costo() + 2;
}
descripcion() {
return `${this.pizza.descripcion()} con pepperoni`;
}
}
// Decorador Champiñones
class Champiñones extends PizzaDecorador {
costo() {
return this.pizza.costo() + 1.5;
}
descripcion() {
return `${this.pizza.descripcion()} con champiñones`;
}
}
// Uso de los decoradores
let pizza = new Pizza();
console.log(`${pizza.descripcion()}: $${pizza.costo()}`); // Output: Pizza básica: $8
pizza = new Pepperoni(pizza);
console.log(`${pizza.descripcion()}: $${pizza.costo()}`); // Output: Pizza básica con pepperoni: $10
pizza = new Champiñones(pizza);
console.log(`${pizza.descripcion()}: $${pizza.costo()}`); // Output: Pizza básica con pepperoni con champiñones: $11.5
En este ejercicio:
Pizza es la clase base que define una pizza simple.
PizzaDecorador es la clase base para todos los decoradores que extenderán la funcionalidad de Pizza.
Pepperoni y Champiñones son decoradores concretos que añaden costo y descripción adicionales a la pizza.
Casos de Uso Comunes del Patrón Decorator
Interfaces Gráficas de Usuario (GUI): Añadir funcionalidad a los componentes de la interfaz (por ejemplo, añadir bordes, scrollbars, etc.).
Streams de Entrada/Salida: Añadir funcionalidades adicionales a los streams (por ejemplo, buffering, cifrado, compresión).
Características de Objetos: Añadir características adicionales a objetos existentes (por ejemplo, roles y permisos de usuarios).
El patrón Decorator es una herramienta poderosa para extender la funcionalidad de los objetos de manera flexible y desacoplada. ¡Sigue practicando con este patrón y aplícalo en tus proyectos para ver sus beneficios! Mañana continuaremos con otro patrón de diseño.
Hoy vamos a practicar con los patrones de diseño que hemos aprendido: Singleton, Factory Method, Observer y Decorator. A continuación, encontrarás ejercicios simples que te ayudarán a consolidar tu comprensión de estos patrones.
Objetivo: Crear una clase DatabaseConnection que implemente el patrón Singleton.
Requisitos:
La clase debe asegurar que solo exista una instancia de conexión a la base de datos.
Debe tener un método connect que imprima un mensaje indicando que se ha realizado la conexión.
// Implementación del patrón Singleton para DatabaseConnection
class DatabaseConnection {
constructor() {
if (DatabaseConnection.instance) {
return DatabaseConnection.instance;
}
DatabaseConnection.instance = this;
}
connect() {
console.log('Conexión a la base de datos establecida.');
}
}
// Crear instancias de DatabaseConnection
const db1 = new DatabaseConnection();
const db2 = new DatabaseConnection();
db1.connect(); // Output: Conexión a la base de datos establecida.
db2.connect(); // Output: Conexión a la base de datos establecida.
// Verificar que ambas instancias son iguales
console.log(db1 === db2); // Output: true
Objetivo: Crear una fábrica para crear diferentes tipos de Notificaciones.
Requisitos:
Crear una interfaz Notificacion con un método enviar.
Crear las clases EmailNotificacion y SMSNotificacion que implementen Notificacion.
Crear una clase NotificacionFactory con un método crearNotificacion(tipo) que devuelva una instancia de EmailNotificacion o SMSNotificacion.
// Interfaz Notificacion
class Notificacion {
enviar() {
throw new Error('Método enviar() debe ser implementado.');
}
}
// Clase EmailNotificacion que implementa Notificacion
class EmailNotificacion extends Notificacion {
enviar() {
console.log('Enviando notificación por email.');
}
}
// Clase SMSNotificacion que implementa Notificacion
class SMSNotificacion extends Notificacion {
enviar() {
console.log('Enviando notificación por SMS.');
}
}
// Clase NotificacionFactory
class NotificacionFactory {
crearNotificacion(tipo) {
if (tipo === 'email') {
return new EmailNotificacion();
} else if (tipo === 'sms') {
return new SMSNotificacion();
} else {
throw new Error('Tipo de notificación no soportado.');
}
}
}
// Uso de la fábrica
const factory = new NotificacionFactory();
const emailNotificacion = factory.crearNotificacion('email');
const smsNotificacion = factory.crearNotificacion('sms');
emailNotificacion.enviar(); // Output: Enviando notificación por email.
smsNotificacion.enviar(); // Output: Enviando notificación por SMS.
Objetivo: Implementar un sistema de alerta meteorológica utilizando el patrón Observer.
Requisitos:
Crear una clase WeatherStation que actúe como el sujeto.
Crear una clase WeatherAlert que actúe como el observador.
La WeatherStation debe notificar a los observadores cuando cambie el estado del clima.
// Clase WeatherStation (Subject)
class WeatherStation {
constructor() {
this.observers = [];
this.weather = '';
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify() {
this.observers.forEach(observer => observer.update(this.weather));
}
setWeather(weather) {
this.weather = weather;
this.notify();
}
}
// Clase WeatherAlert (Observer)
class WeatherAlert {
constructor(name) {
this.name = name;
}
update(weather) {
console.log(`${this.name} ha recibido la alerta: El clima ha cambiado a ${weather}`);
}
}
// Crear instancias de WeatherStation y WeatherAlert
const weatherStation = new WeatherStation();
const alert1 = new WeatherAlert('Alerta 1');
const alert2 = new WeatherAlert('Alerta 2');
// Suscribir alertas
weatherStation.subscribe(alert1);
weatherStation.subscribe(alert2);
// Cambiar el clima y notificar a las alertas
weatherStation.setWeather('soleado');
// Output:
// Alerta 1 ha recibido la alerta: El clima ha cambiado a soleado
// Alerta 2 ha recibido la alerta: El clima ha cambiado a soleado
// Desuscribir una alerta
weatherStation.unsubscribe(alert1);
// Cambiar el clima nuevamente y notificar a las alertas restantes
weatherStation.setWeather('lluvioso');
// Output:
// Alerta 2 ha recibido la alerta: El clima ha cambiado a lluvioso
Objetivo: Crear una clase base Torta y decoradores para añadir diferentes ingredientes (como Fresas y Crema).
Requisitos:
Crear una clase base Torta con métodos costo y descripcion.
Crear una clase decoradora base TortaDecorador que implemente Torta.
Crear clases decoradoras Fresas y Crema que extiendan TortaDecorador y añadan costo y descripción adicionales.
// Clase base Torta
class Torta {
costo() {
return 10;
}
descripcion() {
return 'Torta básica';
}
}
// Clase Decoradora base
class TortaDecorador {
constructor(torta) {
this.torta = torta;
}
costo() {
return this.torta.costo();
}
descripcion() {
return this.torta.descripcion();
}
}
// Decorador Fresas
class Fresas extends TortaDecorador {
costo() {
return this.torta.costo() + 3;
}
descripcion() {
return `${this.torta.descripcion()} con fresas`;
}
}
// Decorador Crema
class Crema extends TortaDecorador {
costo() {
return this.torta.costo() + 2;
}
descripcion() {
return `${this.torta.descripcion()} con crema`;
}
}
// Uso de los decoradores
let torta = new Torta();
console.log(`${torta.descripcion()}: $${torta.costo()}`); // Output: Torta básica: $10
torta = new Fresas(torta);
console.log(`${torta.descripcion()}: $${torta.costo()}`); // Output: Torta básica con fresas: $13
torta = new Crema(torta);
console.log(`${torta.descripcion()}: $${torta.costo()}`); // Output: Torta básica con fresas con crema: $15
Estos ejercicios te ayudarán a practicar y consolidar tu comprensión de los patrones de diseño en JavaScript. ¡Sigue practicando y aplicando estos patrones en tus proyectos para ver sus beneficios! Mañana continuaremos con más conceptos avanzados en diseño de software.
Durante esta semana, hemos cubierto varios patrones de diseño importantes: Singleton, Factory Method, Observer y Decorator. Vamos a repasarlos brevemente antes de hacer el cuestionario.
Singleton:
Asegura que una clase tenga solo una instancia y proporciona un punto de acceso global a esa instancia.
Ejemplo: Gestión de conexiones de base de datos.
Factory Method:
Define una interfaz para crear un objeto, pero permite a las subclases alterar el tipo de objetos que se crearán.
Ejemplo: Creación de diferentes tipos de notificaciones (email, SMS).
Observer:
Define una relación de dependencia uno-a-muchos entre objetos, de manera que cuando un objeto cambia de estado, todos sus dependientes son notificados y actualizados automáticamente.
Ejemplo: Sistema de alerta meteorológica.
Decorator:
Permite añadir funcionalidades a un objeto de manera dinámica.
Ejemplo: Añadir ingredientes a una torta (fresas, crema).
Pregunta 1: ¿Cuál es el propósito del patrón Singleton?
a) Crear múltiples instancias de una clase.
b) Asegurar que una clase tenga solo una instancia.
c) Proveer una interfaz común para diferentes clases.
d) Permitir que un objeto notifique a otros objetos sobre cambios de estado.
Pregunta 2: ¿Qué patrón de diseño define una interfaz para crear un objeto, pero permite a las subclases alterar el tipo de objetos que se crearán?
a) Singleton
b) Factory Method
c) Observer
d) Decorator
Pregunta 3: ¿Qué patrón de diseño establece una relación de dependencia uno-a-muchos entre objetos?
a) Singleton
b) Factory Method
c) Observer
d) Decorator
Pregunta 4: ¿Cuál es el beneficio principal del patrón Decorator?
a) Asegura una única instancia de una clase.
b) Proporciona un punto de acceso global a una instancia.
c) Permite añadir funcionalidades a un objeto de manera dinámica.
d) Define una interfaz para crear objetos.
Pregunta 5: En el patrón Factory Method, ¿qué método es responsable de la creación de objetos?
a) crear()
b) new()
c) build()
d) createFactory()
Pregunta 6: ¿Cuál de los siguientes es un ejemplo del uso del patrón Observer?
a) Sistema de gestión de configuración.
b) Sistema de registro de log.
c) Sistema de notificación de eventos en una aplicación de chat.
d) Sistema de conexión a bases de datos.
Pregunta 7: ¿Cuál es la principal diferencia entre los patrones Singleton y Factory Method?
a) Singleton crea múltiples instancias mientras que Factory Method crea una sola instancia.
b) Singleton asegura una única instancia mientras que Factory Method permite la creación de diferentes tipos de objetos.
c) Singleton y Factory Method son idénticos.
d) Singleton y Factory Method son utilizados en la misma situación.
b) Asegurar que una clase tenga solo una instancia.
b) Factory Method
c) Observer
c) Permite añadir funcionalidades a un objeto de manera dinámica.
a) crear()
c) Sistema de notificación de eventos en una aplicación de chat.
b) Singleton asegura una única instancia mientras que Factory Method permite la creación de diferentes tipos de objetos.
Ejercicios Prácticos Adicionales
Ejercicio 1: Implementar el patrón Singleton para una clase Configuracion que almacene configuraciones globales.
class Configuracion {
constructor() {
if (Configuracion.instance) {
return Configuracion.instance;
}
this.settings = {};
Configuracion.instance = this;
}
setConfig(key, value) {
this.settings[key] = value;
}
getConfig(key) {
return this.settings[key];
}
}
// Uso del Singleton
const config1 = new Configuracion();
const config2 = new Configuracion();
config1.setConfig('theme', 'dark');
console.log(config2.getConfig('theme')); // Output: dark
console.log(config1 === config2); // Output: true
Ejercicio 2: Implementar el patrón Factory Method para una clase AnimalFactory que cree diferentes tipos de animales (Perro y Gato).
class Animal {
hacerSonido() {
throw new Error('Método hacerSonido() debe ser implementado.');
}
}
class Perro extends Animal {
hacerSonido() {
console.log('El perro ladra.');
}
}
class Gato extends Animal {
hacerSonido() {
console.log('El gato maúlla.');
}
}
class AnimalFactory {
crearAnimal(tipo) {
if (tipo === 'perro') {
return new Perro();
} else if (tipo === 'gato') {
return new Gato();
} else {
throw new Error('Tipo de animal no soportado.');
}
}
}
// Uso de la fábrica
const factory = new AnimalFactory();
const perro = factory.crearAnimal('perro');
const gato = factory.crearAnimal('gato');
perro.hacerSonido(); // Output: El perro ladra.
gato.hacerSonido(); // Output: El gato maúlla.
Ejercicio 3: Implementar el patrón Observer para una clase Blog que notifique a los suscriptores cuando se publique un nuevo artículo.
// Clase Blog (Subject)
class Blog {
constructor() {
this.subscribers = [];
}
subscribe(subscriber) {
this.subscribers.push(subscriber);
}
unsubscribe(subscriber) {
this.subscribers = this.subscribers.filter(sub => sub !== subscriber);
}
notify(article) {
this.subscribers.forEach(subscriber => subscriber.update(article));
}
publish(article) {
console.log(`Artículo publicado: ${article}`);
this.notify(article);
}
}
// Clase Subscriber (Observer)
class Subscriber {
constructor(name) {
this.name = name;
}
update(article) {
console.log(`${this.name} ha recibido el artículo: ${article}`);
}
}
// Crear instancias de Blog y Subscriber
const blog = new Blog();
const subscriber1 = new Subscriber('Suscriptor 1');
const subscriber2 = new Subscriber('Suscriptor 2');
// Suscribir a los suscriptores
blog.subscribe(subscriber1);
blog.subscribe(subscriber2);
// Publicar un artículo y notificar a los suscriptores
blog.publish('Nuevo artículo sobre JavaScript');
// Output:
// Artículo publicado: Nuevo artículo sobre JavaScript
// Suscriptor 1 ha recibido el artículo: Nuevo artículo sobre JavaScript
// Suscriptor 2 ha recibido el artículo: Nuevo artículo sobre JavaScript
Ejercicio 4: Implementar el patrón Decorator para una clase Bebida y decoradores Menta y Hielo.
// Clase base Bebida
class Bebida {
costo() {
return 5;
}
descripcion() {
return 'Bebida básica';
}
}
// Clase Decoradora base
class BebidaDecorador {
constructor(bebida) {
this.bebida = bebida;
}
costo() {
return this.bebida.costo();
}
descripcion() {
return this.bebida.descripcion();
}
}
// Decorador Menta
class Menta extends BebidaDecorador {
costo() {
return this.bebida.costo() + 1;
}
descripcion() {
return `${this.bebida.descripcion()} con menta`;
}
}
// Decorador Hielo
class Hielo extends BebidaDecorador {
costo() {
return this.bebida.costo() + 0.5;
}
descripcion() {
return `${this.bebida.descripcion()} con hielo`;
}
}
// Uso de los decoradores
let bebida = new Bebida();
console.log(`${bebida.descripcion()}: $${bebida.costo()}`); // Output: Bebida básica: $5
bebida = new Menta(bebida);
console.log(`${bebida.descripcion()}: $${bebida.costo()}`); // Output: Bebida básica con menta: $6
bebida = new Hielo(bebida);
console.log(`${bebida.descripcion()}: $${bebida.costo()}`); // Output: Bebida básica con menta con hielo: $6.5
Estos ejercicios y cuestionarios te ayudarán a consolidar tu comprensión de los patrones de diseño en JavaScript. ¡Sigue practicando y aplicando estos patrones en tus proyectos para ver sus beneficios! Mañana continuaremos con más conceptos avanzados en diseño de software.
La refactorización es el proceso de mejorar el diseño y la estructura del código existente sin cambiar su comportamiento externo. El objetivo es hacer que el código sea más legible, mantenible y extensible. La refactorización a menudo implica la aplicación de principios de diseño de software y patrones de diseño para optimizar el código.
Eliminar Redundancias: Evitar la duplicación de código.
Mejorar la Legibilidad: Hacer el código más fácil de leer y entender.
Reducir el Acoplamiento: Minimizar las dependencias entre las clases y módulos.
Aumentar la Cohesión: Asegurar que una clase o módulo tenga una única responsabilidad clara.
Aplicar Patrones de Diseño: Usar patrones de diseño para resolver problemas comunes de manera efectiva.
Supongamos que tenemos el siguiente código:
// Clase Pedido que no sigue los principios SOLID
class Pedido {
constructor(producto, cantidad, precio) {
this.producto = producto;
this.cantidad = cantidad;
this.precio = precio;
}
calcularTotal() {
return this.cantidad * this.precio;
}
imprimirPedido() {
console.log(`Pedido: ${this.cantidad} x ${this.producto} - Total: $${this.calcularTotal()}`);
}
enviarEmailConfirmacion() {
console.log(`Enviando email de confirmación para ${this.cantidad} x ${this.producto}`);
}
}
// Uso de la clase Pedido
const pedido = new Pedido('Laptop', 2, 1000);
pedido.imprimirPedido();
pedido.enviarEmailConfirmacion();
Este código viola el principio de responsabilidad única (SRP) porque la clase Pedido tiene múltiples responsabilidades: calcular el total, imprimir el pedido y enviar un email de confirmación. Vamos a refactorizar este código para mejorar su diseño.
Aplicar SRP: Separar las responsabilidades en diferentes clases.
Usar Patrones de Diseño: Aplicar el patrón Singleton para la gestión de emails.
// Clase Pedido que sigue SRP
class Pedido {
constructor(producto, cantidad, precio) {
this.producto = producto;
this.cantidad = cantidad;
this.precio = precio;
}
calcularTotal() {
return this.cantidad * this.precio;
}
}
// Clase ImpresoraDePedidos
class ImpresoraDePedidos {
imprimir(pedido) {
console.log(`Pedido: ${pedido.cantidad} x ${pedido.producto} - Total: $${pedido.calcularTotal()}`);
}
}
// Clase GestorDeEmails que sigue el patrón Singleton
class GestorDeEmails {
constructor() {
if (GestorDeEmails.instance) {
return GestorDeEmails.instance;
}
GestorDeEmails.instance = this;
}
enviarConfirmacion(pedido) {
console.log(`Enviando email de confirmación para ${pedido.cantidad} x ${pedido.producto}`);
}
}
// Uso de las clases refactorizadas
const pedido = new Pedido('Laptop', 2, 1000);
const impresora = new ImpresoraDePedidos();
const gestorEmails = new GestorDeEmails();
impresora.imprimir(pedido);
gestorEmails.enviarConfirmacion(pedido);
En esta refactorización:
La clase Pedido ahora solo se encarga de gestionar la información del pedido y calcular el total.
La clase ImpresoraDePedidos se encarga de imprimir el pedido.
La clase GestorDeEmails se encarga de enviar el email de confirmación y sigue el patrón Singleton para asegurar que solo haya una instancia.
Código Inicial: Refactoriza el siguiente código para aplicar principios de POO y mejorar su diseño.
class Usuario {
constructor(nombre, email, password) {
this.nombre = nombre;
this.email = email;
this.password = password;
}
registrar() {
console.log(`Registrando usuario ${this.nombre}`);
// Lógica de registro
}
enviarEmailBienvenida() {
console.log(`Enviando email de bienvenida a ${this.email}`);
// Lógica para enviar email
}
cambiarPassword(nuevaPassword) {
this.password = nuevaPassword;
console.log(`Password cambiada para ${this.nombre}`);
}
}
// Uso de la clase Usuario
const usuario = new Usuario('Juan', 'juan@example.com', '123456');
usuario.registrar();
usuario.enviarEmailBienvenida();
usuario.cambiarPassword('654321');
Refactorización Aplicada:
// Clase Usuario que sigue SRP
class Usuario {
constructor(nombre, email, password) {
this.nombre = nombre;
this.email = email;
this.password = password;
}
cambiarPassword(nuevaPassword) {
this.password = nuevaPassword;
console.log(`Password cambiada para ${this.nombre}`);
}
}
// Clase RegistroDeUsuarios
class RegistroDeUsuarios {
registrar(usuario) {
console.log(`Registrando usuario ${usuario.nombre}`);
// Lógica de registro
}
}
// Clase GestorDeEmails que sigue el patrón Singleton
class GestorDeEmails {
constructor() {
if (GestorDeEmails.instance) {
return GestorDeEmails.instance;
}
GestorDeEmails.instance = this;
}
enviarEmailBienvenida(usuario) {
console.log(`Enviando email de bienvenida a ${usuario.email}`);
// Lógica para enviar email
}
}
// Uso de las clases refactorizadas
const usuario = new Usuario('Juan', 'juan@example.com', '123456');
const registro = new RegistroDeUsuarios();
const gestorEmails = new GestorDeEmails();
registro.registrar(usuario);
gestorEmails.enviarEmailBienvenida(usuario);
usuario.cambiarPassword('654321');
En esta refactorización:
La clase Usuario ahora solo se encarga de gestionar la información del usuario y cambiar la contraseña.
La clase RegistroDeUsuarios se encarga de registrar al usuario.
La clase GestorDeEmails se encarga de enviar el email de bienvenida y sigue el patrón Singleton.
La refactorización es una práctica esencial para mantener el código limpio, legible y mantenible. Aplicar principios de POO y patrones de diseño durante la refactorización mejora significativamente la calidad del software. ¡Sigue practicando estas técnicas y aplícalas en tus proyectos para ver sus beneficios! Mañana continuaremos con más conceptos avanzados en diseño de software.
Las pruebas unitarias son pruebas automatizadas que validan el comportamiento de pequeñas unidades de código, como funciones o métodos. En la Programación Orientada a Objetos (POO), estas pruebas se centran en clases y métodos. Las pruebas unitarias ayudan a asegurar que el código funciona como se espera y a detectar errores en etapas tempranas del desarrollo.
Detectar Errores Tempranamente: Las pruebas unitarias permiten identificar y corregir errores en las primeras etapas del desarrollo.
Facilitar el Refactoring: Al tener pruebas unitarias, puedes refactorizar el código con confianza, sabiendo que cualquier cambio que rompa la funcionalidad existente será detectado rápidamente.
Mejorar la Documentación: Las pruebas unitarias actúan como una forma de documentación que muestra cómo se espera que el código se comporte.
Asegurar la Calidad del Código: Mantienen un alto nivel de calidad del código al asegurar que las funciones individuales funcionen correctamente.
Facilitar el Desarrollo Colaborativo: En equipos de desarrollo, las pruebas unitarias aseguran que las contribuciones de diferentes desarrolladores no introduzcan errores en el código existente.
Jest: Un popular framework de pruebas para JavaScript.
Mocha: Un framework flexible para pruebas de JavaScript.
Chai: Una biblioteca de aserciones que se usa a menudo con Mocha.
Vamos a implementar pruebas unitarias para una clase Calculadora usando Jest.
Instalar Jest:
Asegúrate de tener Node.js y npm instalados.
Ejecuta npm init -y para inicializar un proyecto npm.
Ejecuta npm install --save-dev jest para instalar Jest como una dependencia de desarrollo.
Configurar Jest:
Añade la siguiente sección a tu package.json:
"scripts": {
"test": "jest"
}
Implementar la Clase Calculadora:
// Archivo calculadora.js
class Calculadora {
sumar(a, b) {
return a + b;
}
restar(a, b) {
return a - b;
}
multiplicar(a, b) {
return a * b;
}
dividir(a, b) {
if (b === 0) {
throw new Error('No se puede dividir por cero');
}
return a / b;
}
}
module.exports = Calculadora;
Escribir Pruebas Unitarias:
// Archivo calculadora.test.js
const Calculadora = require('./calculadora');
test('suma dos números', () => {
const calculadora = new Calculadora();
expect(calculadora.sumar(1, 2)).toBe(3);
});
test('resta dos números', () => {
const calculadora = new Calculadora();
expect(calculadora.restar(5, 3)).toBe(2);
});
test('multiplica dos números', () => {
const calculadora = new Calculadora();
expect(calculadora.multiplicar(2, 3)).toBe(6);
});
test('divide dos números', () => {
const calculadora = new Calculadora();
expect(calculadora.dividir(6, 3)).toBe(2);
});
test('lanza error al dividir por cero', () => {
const calculadora = new Calculadora();
expect(() => calculadora.dividir(6, 0)).toThrow('No se puede dividir por cero');
});
Ejecutar las Pruebas:
Ejecuta npm test para correr las pruebas con Jest.
Ejercicio
Implementa pruebas unitarias para una clase Banco que tenga métodos para depositar, retirar y consultar saldo.
Implementar la Clase Banco:
// Archivo banco.js
class Banco {
constructor() {
this.saldo = 0;
}
depositar(cantidad) {
if (cantidad <= 0) {
throw new Error('La cantidad debe ser mayor que cero');
}
this.saldo += cantidad;
}
retirar(cantidad) {
if (cantidad > this.saldo) {
throw new Error('Fondos insuficientes');
}
this.saldo -= cantidad;
}
consultarSaldo() {
return this.saldo;
}
}
module.exports = Banco;
Escribir Pruebas Unitarias:
// Archivo banco.test.js
const Banco = require('./banco');
test('deposita una cantidad', () => {
const banco = new Banco();
banco.depositar(100);
expect(banco.consultarSaldo()).toBe(100);
});
test('lanza error al depositar una cantidad negativa', () => {
const banco = new Banco();
expect(() => banco.depositar(-50)).toThrow('La cantidad debe ser mayor que cero');
});
test('retira una cantidad', () => {
const banco = new Banco();
banco.depositar(100);
banco.retirar(50);
expect(banco.consultarSaldo()).toBe(50);
});
test('lanza error al retirar una cantidad mayor al saldo', () => {
const banco = new Banco();
banco.depositar(100);
expect(() => banco.retirar(150)).toThrow('Fondos insuficientes');
});
test('consulta el saldo', () => {
const banco = new Banco();
banco.depositar(100);
expect(banco.consultarSaldo()).toBe(100);
});
Ejecutar las Pruebas:
Ejecuta npm test para correr las pruebas con Jest.
Las pruebas unitarias son fundamentales para asegurar la calidad del código en la Programación Orientada a Objetos. Ayudan a detectar errores tempranamente, facilitan el refactoring y mejoran la documentación del código. ¡Sigue practicando y aplicando pruebas unitarias en tus proyectos para ver sus beneficios! Mañana continuaremos con más conceptos avanzados en diseño de software.
El manejo de excepciones es una técnica que permite gestionar errores y condiciones excepcionales en un programa de manera controlada. En lugar de permitir que los errores causen la terminación abrupta del programa, el manejo de excepciones permite que el programa reaccione de manera adecuada, proporcionando mensajes de error útiles, limpiando recursos y manteniendo la estabilidad del sistema.
Estabilidad: Mantiene el programa en ejecución incluso cuando ocurren errores.
Depuración: Proporciona información detallada sobre los errores para facilitar la depuración.
Mantenimiento: Facilita el mantenimiento del código al centralizar la lógica de manejo de errores.
Experiencia del Usuario: Mejora la experiencia del usuario al proporcionar mensajes de error claros y útiles.
En JavaScript, el manejo de excepciones se realiza principalmente utilizando los bloques try, catch, finally y el objeto throw.
try: Contiene el código que puede generar una excepción.
catch: Captura y maneja la excepción.
finally: Código que se ejecuta siempre, independientemente de si se lanzó una excepción o no.
throw: Lanza una excepción.
Ejemplo Básico de Manejo de Excepciones
function dividir(a, b) {
if (b === 0) {
throw new Error('No se puede dividir por cero');
}
return a / b;
}
try {
console.log(dividir(10, 2)); // Output: 5
console.log(dividir(10, 0)); // Lanza una excepción
} catch (error) {
console.error('Error:', error.message); // Output: Error: No se puede dividir por cero
} finally {
console.log('Operación de división completada'); // Se ejecuta siempre
}
En este ejemplo:
La función dividir lanza una excepción si se intenta dividir por cero.
El bloque try contiene llamadas a la función dividir.
El bloque catch captura y maneja la excepción.
El bloque finally se ejecuta siempre, independientemente de si se lanzó una excepción o no.
Ejercicio: Manejo de Excepciones en una Clase Banco
Vamos a mejorar la clase Banco del ejercicio anterior añadiendo manejo de excepciones para casos como depósitos y retiros inválidos.
Clase Banco Mejorada:
class Banco {
constructor() {
this.saldo = 0;
}
depositar(cantidad) {
if (cantidad <= 0) {
throw new Error('La cantidad debe ser mayor que cero');
}
this.saldo += cantidad;
}
retirar(cantidad) {
if (cantidad > this.saldo) {
throw new Error('Fondos insuficientes');
}
this.saldo -= cantidad;
}
consultarSaldo() {
return this.saldo;
}
}
Uso de la Clase Banco con Manejo de Excepciones:
try {
const banco = new Banco();
banco.depositar(100);
console.log('Saldo después del depósito:', banco.consultarSaldo()); // Output: 100
banco.retirar(50);
console.log('Saldo después del retiro:', banco.consultarSaldo()); // Output: 50
banco.retirar(60); // Lanza una excepción
} catch (error) {
console.error('Error:', error.message); // Output: Error: Fondos insuficientes
} finally {
console.log('Operaciones del banco completadas'); // Se ejecuta siempre
}
try {
const banco = new Banco();
banco.depositar(-50); // Lanza una excepción
} catch (error) {
console.error('Error:', error.message); // Output: Error: La cantidad debe ser mayor que cero
} finally {
console.log('Operaciones del banco completadas'); // Se ejecuta siempre
}
Lanzar Excepciones Adecuadas: Usa tipos específicos de excepciones para diferentes errores.
Proporcionar Mensajes Útiles: Los mensajes de error deben ser claros y proporcionar información útil para la depuración.
No Capturar Excepciones Genéricas: Evita capturar todas las excepciones con un único bloque catch. Captura excepciones específicas siempre que sea posible.
Liberar Recursos: Usa el bloque finally para liberar recursos (como conexiones de bases de datos) que deben cerrarse independientemente de si se lanzó una excepción.
Documentar Excepciones: Documenta las excepciones que tus funciones y métodos pueden lanzar.
Ejercicio Adicional: Clase Calculadora con Manejo de Excepciones
Implementa la Clase Calculadora con Manejo de Excepciones:
class Calculadora {
sumar(a, b) {
return a + b;
}
restar(a, b) {
return a - b;
}
multiplicar(a, b) {
return a * b;
}
dividir(a, b) {
if (b === 0) {
throw new Error('No se puede dividir por cero');
}
return a / b;
}
}
Uso de la Clase Calculadora con Manejo de Excepciones:
try {
const calculadora = new Calculadora();
console.log('Suma:', calculadora.sumar(10, 5)); // Output: 15
console.log('Resta:', calculadora.restar(10, 5)); // Output: 5
console.log('Multiplicación:', calculadora.multiplicar(10, 5)); // Output: 50
console.log('División:', calculadora.dividir(10, 5)); // Output: 2
console.log('División por cero:', calculadora.dividir(10, 0)); // Lanza una excepción
} catch (error) {
console.error('Error:', error.message); // Output: Error: No se puede dividir por cero
} finally {
console.log('Operaciones de la calculadora completadas'); // Se ejecuta siempre
}
El manejo de excepciones es una práctica esencial para crear software robusto y mantenible. Permite gestionar errores de manera controlada, proporcionando una mejor experiencia al usuario y facilitando la depuración y el mantenimiento del código. ¡Sigue practicando estas técnicas y aplícalas en tus proyectos para ver sus beneficios! Mañana continuaremos con más conceptos avanzados en diseño de software.
Vamos a desarrollar un sistema de gestión de biblioteca utilizando los conceptos de Programación Orientada a Objetos (POO) que hemos aprendido hasta ahora. Este proyecto incluirá el uso de patrones de diseño, manejo de excepciones, y pruebas unitarias.
Gestión de Libros:
Añadir nuevos libros.
Consultar libros disponibles.
Marcar libros como prestados y devueltos.
Gestión de Usuarios:
Registrar nuevos usuarios.
Consultar usuarios registrados.
Préstamo de Libros:
Permitir que los usuarios presten y devuelvan libros.
Registrar la fecha de préstamo y devolución.
Manejo de Excepciones:
Manejar errores como intentar prestar un libro que no está disponible.
Pruebas Unitarias:
Implementar pruebas unitarias para las funcionalidades clave del sistema.
Clases Principales:
Libro: Representa un libro en la biblioteca.
Usuario: Representa un usuario de la biblioteca.
Biblioteca: Gestiona la colección de libros y usuarios, y maneja los préstamos.
Patrones de Diseño Utilizados:
Singleton: Para la clase GestorDeEmails.
Observer: Para notificar a los usuarios cuando se registra un préstamo.
Factory Method: Para crear diferentes tipos de libros.
// Archivo libro.js
class Libro {
constructor(titulo, autor) {
this.titulo = titulo;
this.autor = autor;
this.disponible = true;
}
prestar() {
if (!this.disponible) {
throw new Error(`El libro "${this.titulo}" no está disponible`);
}
this.disponible = false;
}
devolver() {
this.disponible = true;
}
estaDisponible() {
return this.disponible;
}
}
module.exports = Libro;
// Archivo usuario.js
class Usuario {
constructor(nombre) {
this.nombre = nombre;
this.librosPrestados = [];
}
prestarLibro(libro) {
libro.prestar();
this.librosPrestados.push(libro);
}
devolverLibro(libro) {
libro.devolver();
this.librosPrestados = this.librosPrestados.filter(l => l !== libro);
}
obtenerLibrosPrestados() {
return this.librosPrestados;
}
}
module.exports = Usuario;
Clase Biblioteca
// Archivo biblioteca.js
const Libro = require('./libro');
const Usuario = require('./usuario');
class Biblioteca {
constructor() {
this.libros = [];
this.usuarios = [];
}
añadirLibro(libro) {
this.libros.push(libro);
}
registrarUsuario(usuario) {
this.usuarios.push(usuario);
}
prestarLibro(usuario, tituloLibro) {
const libro = this.libros.find(libro => libro.titulo === tituloLibro && libro.estaDisponible());
if (!libro) {
throw new Error(`El libro "${tituloLibro}" no está disponible o no existe`);
}
usuario.prestarLibro(libro);
}
devolverLibro(usuario, tituloLibro) {
const libro = usuario.obtenerLibrosPrestados().find(libro => libro.titulo === tituloLibro);
if (!libro) {
throw new Error(`El usuario no tiene el libro "${tituloLibro}" prestado`);
}
usuario.devolverLibro(libro);
}
consultarLibrosDisponibles() {
return this.libros.filter(libro => libro.estaDisponible());
}
consultarUsuarios() {
return this.usuarios;
}
}
module.exports = Biblioteca;
Pruebas Unitarias
Instalar Jest:
Ejecuta npm install --save-dev jest para instalar Jest.
Configurar Jest:
Añade la siguiente sección a tu package.json:
"scripts": {
"test": "jest"
}
Escribir Pruebas Unitarias:
// Archivo biblioteca.test.js
const Libro = require('./libro');
const Usuario = require('./usuario');
const Biblioteca = require('./biblioteca');
test('añadir y consultar libros disponibles', () => {
const biblioteca = new Biblioteca();
const libro1 = new Libro('1984', 'George Orwell');
const libro2 = new Libro('Brave New World', 'Aldous Huxley');
biblioteca.añadirLibro(libro1);
biblioteca.añadirLibro(libro2);
expect(biblioteca.consultarLibrosDisponibles()).toEqual([libro1, libro2]);
});
test('registrar y consultar usuarios', () => {
const biblioteca = new Biblioteca();
const usuario1 = new Usuario('Alice');
const usuario2 = new Usuario('Bob');
biblioteca.registrarUsuario(usuario1);
biblioteca.registrarUsuario(usuario2);
expect(biblioteca.consultarUsuarios()).toEqual([usuario1, usuario2]);
});
test('prestar y devolver libro', () => {
const biblioteca = new Biblioteca();
const libro = new Libro('1984', 'George Orwell');
const usuario = new Usuario('Alice');
biblioteca.añadirLibro(libro);
biblioteca.registrarUsuario(usuario);
biblioteca.prestarLibro(usuario, '1984');
expect(libro.estaDisponible()).toBe(false);
biblioteca.devolverLibro(usuario, '1984');
expect(libro.estaDisponible()).toBe(true);
});
test('manejar errores al prestar un libro no disponible', () => {
const biblioteca = new Biblioteca();
const libro = new Libro('1984', 'George Orwell');
const usuario = new Usuario('Alice');
biblioteca.añadirLibro(libro);
biblioteca.registrarUsuario(usuario);
biblioteca.prestarLibro(usuario, '1984');
expect(() => biblioteca.prestarLibro(usuario, '1984')).toThrow('El libro "1984" no está disponible');
});
Ejecutar las Pruebas:
Ejecuta npm test para correr las pruebas con Jest.
Este proyecto completo te permite aplicar todos los conceptos de POO, patrones de diseño, manejo de excepciones y pruebas unitarias que hemos cubierto hasta ahora. Sigue practicando y expandiendo este proyecto para incluir más funcionalidades y mejorar tu comprensión. ¡Felicidades por completar este ejercicio completo! Mañana continuaremos con más conceptos avanzados en diseño de software.
La optimización de código se centra en mejorar la eficiencia y el rendimiento del software. Esto puede implicar la reducción del tiempo de ejecución, el uso de memoria, o el tamaño del código. Optimizar el código es crucial para aplicaciones que requieren un alto rendimiento y una experiencia de usuario fluida.
Algoritmos y Estructuras de Datos Eficientes:
Elegir algoritmos y estructuras de datos adecuadas para la tarea puede marcar una gran diferencia en el rendimiento.
Ejemplo: Usar una tabla hash para búsquedas rápidas en lugar de una lista.
Minimización de Operaciones Costosas:
Evitar operaciones innecesarias o redundantes dentro de bucles o funciones frecuentemente llamadas.
Ejemplo: Calcular una vez fuera de un bucle en lugar de dentro del bucle.
Optimización de Bucles:
Reducir el número de iteraciones y evitar cálculos innecesarios dentro de los bucles.
Ejemplo: Usar bucles for en lugar de forEach en JavaScript si se necesita rendimiento máximo.
Uso Eficiente de la Memoria:
Liberar memoria no utilizada y evitar la creación innecesaria de objetos temporales.
Ejemplo: Usar el patrón de objeto flyweight para reducir el uso de memoria.
Lazy Loading y Caching:
Cargar datos o inicializar objetos solo cuando sea necesario.
Almacenar resultados de cálculos costosos para evitar recalcular.
Ejemplo: Cachear resultados de funciones costosas.
Paralelización y Concurrencia:
Utilizar técnicas de programación concurrente para aprovechar mejor los recursos del hardware.
Ejemplo: Usar Web Workers en JavaScript para realizar tareas en segundo plano.
Vamos a optimizar una función que encuentra el elemento más frecuente en un array.
Código Inicial:
function elementoMasFrecuente(arr) {
let maxElement = arr[0];
let maxCount = 0;
for (let i = 0; i < arr.length; i++) {
let count = 0;
for (let j = 0; j < arr.length; j++) {
if (arr[i] === arr[j]) {
count++;
}
}
if (count > maxCount) {
maxCount = count;
maxElement = arr[i];
}
}
return maxElement;
}
// Ejemplo de uso
const arr = [1, 3, 2, 3, 4, 3, 5, 1];
console.log(elementoMasFrecuente(arr)); // Output: 3
Usar una tabla hash para contar las frecuencias en lugar de un bucle anidado.
Evitar la recalculación de frecuencias.
function elementoMasFrecuenteOptimizado(arr) {
const frecuencia = {};
let maxElement = arr[0];
let maxCount = 0;
for (const num of arr) {
frecuencia[num] = (frecuencia[num] || 0) + 1;
if (frecuencia[num] > maxCount) {
maxCount = frecuencia[num];
maxElement = num;
}
}
return maxElement;
}
// Ejemplo de uso
const arr = [1, 3, 2, 3, 4, 3, 5, 1];
console.log(elementoMasFrecuenteOptimizado(arr)); // Output: 3
En este ejemplo:
La función original utiliza un bucle anidado, lo que resulta en una complejidad de tiempo O(n^2).
La versión optimizada utiliza una tabla hash para contar las frecuencias, reduciendo la complejidad de tiempo a O(n).
Vamos a optimizar la clase Biblioteca que hemos implementado anteriormente para mejorar su rendimiento.
Optimización de Búsqueda de Libros:
Código Inicial:
prestarLibro(usuario, tituloLibro) {
const libro = this.libros.find(libro => libro.titulo === tituloLibro && libro.estaDisponible());
if (!libro) {
throw new Error(`El libro "${tituloLibro}" no está disponible o no existe`);
}
usuario.prestarLibro(libro);
}
Optimización:
Usar un mapa (diccionario) para almacenar los libros por título para una búsqueda más rápida.
class Biblioteca {
constructor() {
this.libros = [];
this.librosPorTitulo = new Map();
this.usuarios = [];
}
añadirLibro(libro) {
this.libros.push(libro);
if (!this.librosPorTitulo.has(libro.titulo)) {
this.librosPorTitulo.set(libro.titulo, []);
}
this.librosPorTitulo.get(libro.titulo).push(libro);
}
prestarLibro(usuario, tituloLibro) {
const librosConTitulo = this.librosPorTitulo.get(tituloLibro);
if (!librosConTitulo) {
throw new Error(`El libro "${tituloLibro}" no existe`);
}
const libro = librosConTitulo.find(libro => libro.estaDisponible());
if (!libro) {
throw new Error(`El libro "${tituloLibro}" no está disponible`);
}
usuario.prestarLibro(libro);
}
devolverLibro(usuario, tituloLibro) {
const libro = usuario.obtenerLibrosPrestados().find(libro => libro.titulo === tituloLibro);
if (!libro) {
throw new Error(`El usuario no tiene el libro "${tituloLibro}" prestado`);
}
usuario.devolverLibro(libro);
}
consultarLibrosDisponibles() {
return this.libros.filter(libro => libro.estaDisponible());
}
registrarUsuario(usuario) {
this.usuarios.push(usuario);
}
consultarUsuarios() {
return this.usuarios;
}
}
// Ejemplo de uso
const biblioteca = new Biblioteca();
const libro1 = new Libro('1984', 'George Orwell');
const libro2 = new Libro('1984', 'George Orwell');
const usuario = new Usuario('Alice');
biblioteca.añadirLibro(libro1);
biblioteca.añadirLibro(libro2);
biblioteca.registrarUsuario(usuario);
biblioteca.prestarLibro(usuario, '1984');
console.log(libro1.estaDisponible()); // Output: false
biblioteca.devolverLibro(usuario, '1984');
console.log(libro1.estaDisponible()); // Output: true
En esta optimización:
Hemos añadido un mapa librosPorTitulo para una búsqueda más rápida de libros por título.
La función prestarLibro ahora utiliza este mapa para encontrar libros disponibles de manera eficiente.
La optimización de código es esencial para mejorar el rendimiento y la eficiencia de las aplicaciones. Usar algoritmos y estructuras de datos eficientes, minimizar operaciones costosas, y manejar adecuadamente la memoria son algunas de las técnicas clave. ¡Sigue practicando estas técnicas y aplícalas en tus proyectos para ver sus beneficios! Mañana continuaremos con más conceptos avanzados en diseño de software.
Vamos a analizar el diseño de un proyecto real: un Sistema de Gestión de Tareas. Este sistema permite a los usuarios crear, actualizar y eliminar tareas, así como asignar prioridades y plazos. También se pueden agrupar las tareas en proyectos.
Gestión de Tareas:
Crear, actualizar y eliminar tareas.
Asignar prioridades (baja, media, alta).
Asignar fechas límite.
Gestión de Proyectos:
Crear, actualizar y eliminar proyectos.
Agrupar tareas en proyectos.
Usuarios:
Registro e inicio de sesión de usuarios.
Asignación de tareas a usuarios.
Notificaciones:
Notificar a los usuarios sobre tareas pendientes y fechas límite próximas.
Diagrama de Clases
plaintext:
Usuario
--------
- nombre: string
- email: string
- password: string
+ registrar()
+ iniciarSesion()
+ asignarTarea(tarea: Tarea)
Tarea
-----
- titulo: string
- descripcion: string
- prioridad: string
- fechaLimite: Date
- completada: boolean
+ crear()
+ actualizar()
+ eliminar()
+ marcarCompleta()
Proyecto
--------
- nombre: string
- descripcion: string
- tareas: Tarea[]
+ crear()
+ actualizar()
+ eliminar()
+ añadirTarea(tarea: Tarea)
+ eliminarTarea(tarea: Tarea)
Notificacion
------------
- mensaje: string
- fecha: Date
+ enviar()
SistemaGestionTareas
--------------------
- usuarios: Usuario[]
- proyectos: Proyecto[]
+ registrarUsuario(usuario: Usuario)
+ iniciarSesion(email: string, password: string)
+ crearProyecto(proyecto: Proyecto)
+ crearTarea(tarea: Tarea, proyecto: Proyecto)
+ notificarUsuarios()
Vamos a implementar las clases principales del sistema: Usuario, Tarea, Proyecto, Notificacion y SistemaGestionTareas.
Clase Usuario
javascript
class Usuario {
constructor(nombre, email, password) {
this.nombre = nombre;
this.email = email;
this.password = password;
this.tareas = [];
}
registrar() {
console.log(`Usuario ${this.nombre} registrado con éxito`);
}
iniciarSesion() {
console.log(`Usuario ${this.nombre} inició sesión`);
}
asignarTarea(tarea) {
this.tareas.push(tarea);
console.log(`Tarea "${tarea.titulo}" asignada a ${this.nombre}`);
}
}
Clase Tarea
javascript
class Tarea {
constructor(titulo, descripcion, prioridad, fechaLimite) {
this.titulo = titulo;
this.descripcion = descripcion;
this.prioridad = prioridad;
this.fechaLimite = fechaLimite;
this.completada = false;
}
crear() {
console.log(`Tarea "${this.titulo}" creada`);
}
actualizar(titulo, descripcion, prioridad, fechaLimite) {
this.titulo = titulo || this.titulo;
this.descripcion = descripcion || this.descripcion;
this.prioridad = prioridad || this.prioridad;
this.fechaLimite = fechaLimite || this.fechaLimite;
console.log(`Tarea "${this.titulo}" actualizada`);
}
eliminar() {
console.log(`Tarea "${this.titulo}" eliminada`);
}
marcarCompleta() {
this.completada = true;
console.log(`Tarea "${this.titulo}" marcada como completada`);
}
}
Clase Proyecto
javascript
class Proyecto {
constructor(nombre, descripcion) {
this.nombre = nombre;
this.descripcion = descripcion;
this.tareas = [];
}
crear() {
console.log(`Proyecto "${this.nombre}" creado`);
}
actualizar(nombre, descripcion) {
this.nombre = nombre || this.nombre;
this.descripcion = descripcion || this.descripcion;
console.log(`Proyecto "${this.nombre}" actualizado`);
}
eliminar() {
console.log(`Proyecto "${this.nombre}" eliminado`);
}
añadirTarea(tarea) {
this.tareas.push(tarea);
console.log(`Tarea "${tarea.titulo}" añadida al proyecto "${this.nombre}"`);
}
eliminarTarea(tarea) {
this.tareas = this.tareas.filter(t => t !== tarea);
console.log(`Tarea "${tarea.titulo}" eliminada del proyecto "${this.nombre}"`);
}
}
Clase Notificacion
javascript
class Notificacion {
constructor(mensaje, fecha) {
this.mensaje = mensaje;
this.fecha = fecha;
}
enviar() {
console.log(`Notificación enviada: ${this.mensaje}`);
}
}
Clase SistemaGestionTareas
javascript
class SistemaGestionTareas {
constructor() {
this.usuarios = [];
this.proyectos = [];
}
registrarUsuario(usuario) {
this.usuarios.push(usuario);
usuario.registrar();
}
iniciarSesion(email, password) {
const usuario = this.usuarios.find(u => u.email === email && u.password === password);
if (usuario) {
usuario.iniciarSesion();
return usuario;
} else {
console.log('Email o contraseña incorrectos');
return null;
}
}
crearProyecto(proyecto) {
this.proyectos.push(proyecto);
proyecto.crear();
}
crearTarea(tarea, proyecto) {
proyecto.añadirTarea(tarea);
tarea.crear();
}
notificarUsuarios() {
this.usuarios.forEach(usuario => {
usuario.tareas.forEach(tarea => {
if (!tarea.completada && tarea.fechaLimite <= new Date()) {
const notificacion = new Notificacion(`La tarea "${tarea.titulo}" está pendiente`, new Date());
notificacion.enviar();
}
});
});
}
}
javascript
// Crear instancia del sistema de gestión de tareas
const sistema = new SistemaGestionTareas();
// Registrar usuarios
const usuario1 = new Usuario('Alice', 'alice@example.com', 'password123');
const usuario2 = new Usuario('Bob', 'bob@example.com', 'password456');
sistema.registrarUsuario(usuario1);
sistema.registrarUsuario(usuario2);
// Iniciar sesión
const usuarioSesion = sistema.iniciarSesion('alice@example.com', 'password123');
// Crear un proyecto
const proyecto = new Proyecto('Proyecto 1', 'Descripción del proyecto 1');
sistema.crearProyecto(proyecto);
// Crear tareas
const tarea1 = new Tarea('Tarea 1', 'Descripción de la tarea 1', 'Alta', new Date('2023-12-31'));
const tarea2 = new Tarea('Tarea 2', 'Descripción de la tarea 2', 'Media', new Date('2023-11-30'));
sistema.crearTarea(tarea1, proyecto);
sistema.crearTarea(tarea2, proyecto);
// Asignar tareas a un usuario
usuario1.asignarTarea(tarea1);
usuario1.asignarTarea(tarea2);
// Notificar usuarios sobre tareas pendientes
sistema.notificarUsuarios();
Principios SOLID:
Single Responsibility Principle (SRP): Cada clase tiene una responsabilidad única, como Usuario para manejar la información del usuario, Tarea para manejar la información de la tarea, etc.
Open/Closed Principle (OCP): El sistema permite agregar nuevas funcionalidades (como nuevos tipos de notificaciones) sin modificar el código existente.
Liskov Substitution Principle (LSP): No es relevante en este diseño específico, pero en general, las subclases deberían poder reemplazar a sus superclases sin alterar el comportamiento correcto del sistema.
Interface Segregation Principle (ISP): No se aplica directamente en este ejemplo, pero las interfaces más pequeñas y específicas podrían aplicarse si el sistema crece.
Dependency Inversion Principle (DIP): Las clases de alto nivel (SistemaGestionTareas) no dependen de clases de bajo nivel (Usuario, Tarea), sino de abstracciones.
Patrones de Diseño:
Singleton: Podría aplicarse a una clase GestorDeNotificaciones para manejar el envío de notificaciones de manera centralizada.
Observer: Podría aplicarse para notificar a los usuarios sobre cambios en las tareas.
Factory Method: Podría aplicarse si se requiere crear diferentes tipos de tareas o proyectos dinámicamente.
Manejo de Excepciones:
El sistema maneja excepciones comunes como intentar prestar un libro no disponible o inexistente.
Podría mejorarse con más validaciones y manejo de errores específicos.
Pruebas Unitarias:
Implementar pruebas unitarias para cada clase y método clave para asegurar la correcta funcionalidad del sistema.
El análisis y diseño de un proyecto real como el Sistema de Gestión de Tareas nos permite aplicar todos los conceptos de POO, patrones de diseño y buenas prácticas de desarrollo. Optimizar y refactorizar el código, manejar excepciones adecuadamente y escribir pruebas unitarias son esenciales para crear un sistema robusto y mantenible. ¡Sigue practicando y aplicando estos conceptos en tus proyectos para mejorar tus habilidades como desarrollador! Mañana continuaremos con más conceptos avanzados en diseño de software.
Hoy es un día de revisión y resolución de dudas sobre todos los temas que hemos cubierto. A continuación, se presenta un resumen de los temas clave que hemos estudiado y una serie de preguntas y respuestas para ayudarte a prepararte para el examen.
Clases y Objetos: Una clase es una plantilla para crear objetos, que son instancias de la clase.
Atributos y Métodos: Los atributos son variables que pertenecen a una clase o a un objeto, y los métodos son funciones que pertenecen a una clase o a un objeto.
Abstracción: Ocultar los detalles complejos y mostrar solo la información esencial.
Encapsulamiento: Proteger los datos dentro de una clase mediante la restricción del acceso a sus atributos.
Herencia: Permitir que una clase (subclase) herede propiedades y métodos de otra clase (superclase).
Polimorfismo: Permitir que una misma operación se comporte de diferentes maneras en distintas clases.
Single Responsibility Principle (SRP): Una clase debe tener una sola responsabilidad.
Open/Closed Principle (OCP): Las entidades de software deben estar abiertas para extensión, pero cerradas para modificación.
Liskov Substitution Principle (LSP): Las subclases deben poder sustituir a sus superclases.
Interface Segregation Principle (ISP): Los clientes no deben depender de interfaces que no usan.
Dependency Inversion Principle (DIP): Los módulos de alto nivel no deben depender de módulos de bajo nivel, sino de abstracciones.
Singleton: Asegura que una clase tenga solo una instancia y proporciona un punto de acceso global a esa instancia.
Factory Method: Define una interfaz para crear un objeto, pero permite a las subclases alterar el tipo de objetos que se crearán.
Observer: Define una relación de dependencia uno-a-muchos entre objetos, de manera que cuando un objeto cambia de estado, todos sus dependientes son notificados y actualizados automáticamente.
Decorator: Permite añadir funcionalidades a un objeto de manera dinámica.
Try/Catch/Finally: Estructuras de control para manejar errores y excepciones de manera controlada.
Throw: Utilizado para lanzar una excepción de manera explícita.
Pruebas Automatizadas: Validan el comportamiento de pequeñas unidades de código.
Herramientas: Jest, Mocha, Chai.
Preguntas de Teoría
¿Qué es una clase en POO?
Respuesta: Una clase es una plantilla para crear objetos que define un conjunto de atributos y métodos.
¿Qué es la encapsulación y por qué es importante?
Respuesta: La encapsulación es un principio de POO que consiste en restringir el acceso a los atributos de una clase, permitiendo acceder a ellos solo a través de métodos. Es importante porque protege los datos y asegura la integridad del objeto.
Explique el Principio de Responsabilidad Única (SRP).
Respuesta: El SRP establece que una clase debe tener una sola responsabilidad o motivo para cambiar. Esto facilita el mantenimiento y la evolución del código.
¿Cuál es la diferencia entre herencia y composición?
Respuesta: La herencia es una relación "es-un" donde una clase (subclase) hereda atributos y métodos de otra clase (superclase). La composición es una relación "tiene-un" donde una clase se compone de una o más instancias de otras clases.
¿Qué es el polimorfismo en POO?
Respuesta: El polimorfismo permite que una misma operación se comporte de diferentes maneras en distintas clases. Se puede lograr mediante sobrecarga de métodos o sobreescritura de métodos.
Preguntas de Código
Implemente una clase Vehiculo con una subclase Coche y un método mover().
javascript
class Vehiculo {
mover() {
console.log('El vehículo se está moviendo.');
}
}
class Coche extends Vehiculo {
mover() {
console.log('El coche se está moviendo.');
}
}
const miCoche = new Coche();
miCoche.mover(); // Output: El coche se está moviendo.
Implemente el patrón Singleton para una clase Configuracion.
javascript
class Configuracion {
constructor() {
if (Configuracion.instance) {
return Configuracion.instance;
}
this.config = {};
Configuracion.instance = this;
}
setConfig(key, value) {
this.config[key] = value;
}
getConfig(key) {
return this.config[key];
}
}
const config1 = new Configuracion();
config1.setConfig('theme', 'dark');
const config2 = new Configuracion();
console.log(config2.getConfig('theme')); // Output: dark
console.log(config1 === config2); // Output: true
Escriba una prueba unitaria para el método sumar() de una clase Calculadora usando Jest.
javascript
// Archivo calculadora.js
class Calculadora {
sumar(a, b) {
return a + b;
}
}
module.exports = Calculadora;
// Archivo calculadora.test.js
const Calculadora = require('./calculadora');
test('sumar dos números', () => {
const calculadora = new Calculadora();
expect(calculadora.sumar(1, 2)).toBe(3);
});
Implemente el patrón Factory Method para crear diferentes tipos de animales (Perro, Gato).
javascript
class Animal {
hacerSonido() {
throw new Error('Método hacerSonido() debe ser implementado.');
}
}
class Perro extends Animal {
hacerSonido() {
console.log('El perro ladra.');
}
}
class Gato extends Animal {
hacerSonido() {
console.log('El gato maúlla.');
}
}
class AnimalFactory {
crearAnimal(tipo) {
if (tipo === 'perro') {
return new Perro();
} else if (tipo === 'gato') {
return new Gato();
} else {
throw new Error('Tipo de animal no soportado.');
}
}
}
const factory = new AnimalFactory();
const perro = factory.crearAnimal('perro');
const gato = factory.crearAnimal('gato');
perro.hacerSonido(); // Output: El perro ladra.
gato.hacerSonido(); // Output: El gato maúlla.
Maneje una excepción lanzada al intentar dividir por cero en una clase Calculadora.
javascript
class Calculadora {
dividir(a, b) {
if (b === 0) {
throw new Error('No se puede dividir por cero');
}
return a / b;
}
}
try {
const calculadora = new Calculadora();
console.log(calculadora.dividir(10, 2)); // Output: 5
console.log(calculadora.dividir(10, 0)); // Lanza una excepción
} catch (error) {
console.error('Error:', error.message); // Output: Error: No se puede dividir por cero
}
Si tienes alguna duda específica sobre los temas cubiertos o necesitas más ejemplos para aclarar conceptos, no dudes en preguntar. Aquí hay algunas preguntas comunes:
¿Cómo elegir entre herencia y composición?
Respuesta: Usa herencia cuando hay una relación "es-un" clara y composición cuando hay una relación "tiene-un". La composición es generalmente más flexible y preferida para reducir el acoplamiento entre clases.
¿Cuándo usar el patrón Singleton?
Respuesta: Usa el patrón Singleton cuando necesites asegurar que solo exista una única instancia de una clase en toda la aplicación, como en el caso de la configuración global o la conexión a una base de datos.
¿Qué diferencia hay entre throw y return en el manejo de excepciones?
Respuesta: throw se usa para lanzar una excepción y detener la ejecución normal del programa, pasando el control al bloque catch más cercano. return simplemente devuelve un valor desde una función y no maneja excepciones.
¿Cómo se implementa la inyección de dependencias en JavaScript?
Respuesta: La inyección de dependencias puede implementarse pasando dependencias (como objetos o funciones) a través del constructor o métodos de una clase. Esto mejora la testabilidad y reduce el acoplamiento.
Practica implementando los conceptos en un proyecto pequeño o en ejercicios adicionales:
Implementa un sistema de gestión de inventarios que permita agregar, eliminar y consultar productos, usando los principios SOLID y patrones de diseño adecuados.
Crea un sistema de notificaciones donde diferentes tipos de notificaciones (email, SMS) se envían a los usuarios usando el patrón Observer.
Desarrolla una aplicación de calendario que gestione eventos y recordatorios, aplicando el manejo de excepciones y pruebas unitarias para asegurar la funcionalidad correcta.
¡Buena suerte en tu examen y sigue practicando para consolidar tu comprensión de la Programación Orientada a Objetos y los patrones de diseño!
Para la simulación del examen, vamos a plantear una serie de preguntas teóricas y prácticas que cubren los temas que hemos estudiado. Responde a cada una de ellas para poner a prueba tu comprensión y preparación.
Pregunta 1: ¿Qué es la abstracción en POO y por qué es importante?
Pregunta 2: Explica el principio de Open/Closed y proporciona un ejemplo.
Pregunta 3: Describe cómo el patrón Singleton asegura que solo exista una instancia de una clase.
Pregunta 4: ¿Cuál es la diferencia entre los patrones Factory Method y Abstract Factory?
Pregunta 5: ¿Cómo se maneja una excepción en JavaScript? Proporciona un ejemplo.
Pregunta 6: Implementa una clase Empleado con atributos nombre, edad y salario. Incluye un método incrementarSalario(porcentaje) que aumente el salario del empleado en un porcentaje dado.
Pregunta 7: Usando el patrón Observer, crea un sistema de notificación donde un objeto Subject notifique a sus Observers cuando cambie su estado.
Pregunta 8: Refactoriza la siguiente clase para seguir el principio de Single Responsibility Principle (SRP).
javascript
class Reporte {
constructor(datos) {
this.datos = datos;
}
generarPDF() {
// Lógica para generar PDF
}
enviarEmail(email) {
// Lógica para enviar el reporte por email
}
guardarEnDisco(ruta) {
// Lógica para guardar el reporte en disco
}
}
Pregunta 9: Escribe una prueba unitaria para el método dividir de la siguiente clase Calculadora usando Jest.
javascript
class Calculadora {
dividir(a, b) {
if (b === 0) {
throw new Error('No se puede dividir por cero');
}
return a / b;
}
}
Pregunta 10: Implementa el patrón Decorator para una clase Bebida que permita añadir ingredientes como Leche y Azúcar.
Pregunta 1: La abstracción en POO es el proceso de simplificar un sistema complejo escondiendo detalles innecesarios y exponiendo solo la información relevante. Es importante porque reduce la complejidad del sistema y facilita la comprensión y el uso del código.
Pregunta 2: El principio de Open/Closed establece que las entidades de software (clases, módulos, funciones, etc.) deben estar abiertas para su extensión pero cerradas para su modificación. Esto significa que podemos extender el comportamiento de una entidad sin modificar su código fuente.
Ejemplo:
javascript
// Clase base
class Figura {
calcularArea() {
throw new Error("Método calcularArea() debe ser implementado.");
}
}
// Clase derivada para el círculo
class Circulo extends Figura {
constructor(radio) {
super();
this.radio = radio;
}
calcularArea() {
return Math.PI * this.radio * this.radio;
}
}
// Clase derivada para el rectángulo
class Rectangulo extends Figura {
constructor(ancho, alto) {
super();
this.ancho = ancho;
this.alto = alto;
}
calcularArea() {
return this.ancho * this.alto;
}
}
Pregunta 3: El patrón Singleton asegura que solo exista una instancia de una clase mediante:
Declaración de una variable estática para almacenar la instancia única.
Haciendo que el constructor de la clase sea privado o protegido.
Proporcionando un método público para acceder a la instancia única.
javascript
class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
Singleton.instance = this;
// Inicialización de la instancia
}
static getInstance() {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}
Pregunta 4: El patrón Factory Method define una interfaz para crear objetos, pero permite a las subclases alterar el tipo de objetos que se crearán. Abstract Factory, en cambio, es un patrón creacional que permite a las subclases crear familias de objetos relacionados sin especificar sus clases concretas.
Pregunta 5: En JavaScript, las excepciones se manejan utilizando los bloques try, catch y finally.
javascript
try {
// Código que puede lanzar una excepción
let result = dividir(10, 0);
} catch (error) {
// Código para manejar la excepción
console.error('Error:', error.message);
} finally {
// Código que siempre se ejecuta
console.log('Operación finalizada.');
}
function dividir(a, b) {
if (b === 0) {
throw new Error('No se puede dividir por cero');
}
return a / b;
}
Pregunta 6:
javascript
class Empleado {
constructor(nombre, edad, salario) {
this.nombre = nombre;
this.edad = edad;
this.salario = salario;
}
incrementarSalario(porcentaje) {
this.salario += (this.salario * porcentaje) / 100;
}
}
// Ejemplo de uso
const empleado = new Empleado('Juan', 30, 50000);
empleado.incrementarSalario(10);
console.log(empleado.salario); // Output: 55000
Pregunta 7:
javascript
class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
constructor(nombre) {
this.nombre = nombre;
}
update(data) {
console.log(`${this.nombre} ha recibido la notificación: ${data}`);
}
}
// Ejemplo de uso
const subject = new Subject();
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify('Nuevo evento');
// Output:
// Observer 1 ha recibido la notificación: Nuevo evento
// Observer 2 ha recibido la notificación: Nuevo evento
Pregunta 8:
javascript
class Reporte {
constructor(datos) {
this.datos = datos;
}
}
class GeneradorPDF {
generarPDF(datos) {
// Lógica para generar PDF
}
}
class EmailService {
enviarEmail(email, datos) {
// Lógica para enviar el reporte por email
}
}
class Almacenamiento {
guardarEnDisco(ruta, datos) {
// Lógica para guardar el reporte en disco
}
}
Pregunta 9:
javascript
// Archivo calculadora.test.js
const Calculadora = require('./calculadora');
test('divide dos números', () => {
const calculadora = new Calculadora();
expect(calculadora.dividir(10, 2)).toBe(5);
});
test('lanza error al dividir por cero', () => {
const calculadora = new Calculadora();
expect(() => calculadora.dividir(10, 0)).toThrow('No se puede dividir por cero');
});
Pregunta 10:
javascript
class Bebida {
costo() {
return 5;
}
descripcion() {
return 'Bebida básica';
}
}
class BebidaDecorador {
constructor(bebida) {
this.bebida = bebida;
}
costo() {
return this.bebida.costo();
}
descripcion() {
return this.bebida.descripcion();
}
}
class Leche extends BebidaDecorador {
costo() {
return this.bebida.costo() + 1;
}
descripcion() {
return `${this.bebida.descripcion()} con leche`;
}
}
class Azucar extends BebidaDecorador {
costo() {
return this.bebida.costo() + 0.5;
}
descripcion() {
return `${this.bebida.descripcion()} con azúcar`;
}
}
// Ejemplo de uso
let bebida = new Bebida();
console.log(`${bebida.descripcion()}: $${bebida.costo()}`); // Output: Bebida básica: $5
bebida = new Leche(bebida);
console.log(`${bebida.descripcion()}: $${bebida.costo()}`); // Output: Bebida básica con leche: $6
bebida = new Azucar(bebida);
console.log(`${bebida.descripcion()}: $${bebida.costo()}`); // Output: Bebida básica con leche con azúcar: $6.5
Este examen simulado cubre una amplia gama de conceptos y prácticas en la Programación Orientada a Objetos, principios SOLID, patrones de diseño, manejo de excepciones y pruebas unitarias. Practica resolviendo estos ejercicios y revisa las respuestas para asegurarte de comprender cada concepto a fondo. ¡Buena suerte en tu examen real!
Hoy vamos a hacer un repaso general de todos los conceptos que hemos aprendido y proporcionar algunas estrategias para abordar el examen real con confianza. Vamos a revisar brevemente los temas clave, hacer un último ejercicio integral y ofrecer consejos prácticos para el examen.
Programación Orientada a Objetos (POO)
Clases y Objetos
Atributos y Métodos
Encapsulamiento
Herencia
Polimorfismo
Abstracción
Principios de Diseño SOLID
Single Responsibility Principle (SRP)
Open/Closed Principle (OCP)
Liskov Substitution Principle (LSP)
Interface Segregation Principle (ISP)
Dependency Inversion Principle (DIP)
Patrones de Diseño
Singleton
Factory Method
Observer
Decorator
Manejo de Excepciones
Try/Catch/Finally
Throw
Pruebas Unitarias
Jest, Mocha, Chai
Lee todas las preguntas primero: Esto te ayudará a gestionar mejor tu tiempo y a identificar las preguntas que puedes responder con facilidad.
Empieza por lo que sabes: Responde primero las preguntas que te resulten más fáciles para ganar confianza y asegurar puntos.
Gestiona tu tiempo: No pases demasiado tiempo en una sola pregunta. Si te quedas atascado, pasa a la siguiente y vuelve más tarde si tienes tiempo.
Revisa tu trabajo: Si tienes tiempo al final, revisa tus respuestas para corregir errores y asegurar que has respondido todas las preguntas.
Mantén la calma: Respira profundamente y mantén la calma. La ansiedad puede nublar tu juicio y afectar tu rendimiento.
Vamos a hacer un ejercicio integral que combine varios de los conceptos que hemos aprendido.
Ejercicio: Sistema de Reserva de Salas
Desarrolla un sistema de reserva de salas utilizando POO, principios SOLID, patrones de diseño, manejo de excepciones y pruebas unitarias.
Requisitos:
Clases:
Sala: Representa una sala que se puede reservar.
Usuario: Representa un usuario que puede reservar una sala.
Reserva: Representa una reserva de una sala por un usuario.
SistemaReservaSalas: Gestiona las reservas de salas.
Funcionalidades:
Los usuarios pueden crear una cuenta.
Los usuarios pueden reservar salas si están disponibles.
Los usuarios pueden cancelar reservas.
Notificar a los usuarios sobre reservas exitosas y cancelaciones.
Manejar excepciones como intentar reservar una sala ya reservada.
Implementación
Clase Sala
javascript
class Sala {
constructor(nombre) {
this.nombre = nombre;
this.disponible = true;
}
reservar() {
if (!this.disponible) {
throw new Error(`La sala "${this.nombre}" no está disponible`);
}
this.disponible = false;
}
cancelarReserva() {
this.disponible = true;
}
estaDisponible() {
return this.disponible;
}
}
Clase Usuario
javascript
class Usuario {
constructor(nombre, email) {
this.nombre = nombre;
this.email = email;
}
recibirNotificacion(mensaje) {
console.log(`Notificación para ${this.nombre}: ${mensaje}`);
}
}
Clase Reserva
javascript
class Reserva {
constructor(usuario, sala, fecha) {
this.usuario = usuario;
this.sala = sala;
this.fecha = fecha;
}
}
Clase SistemaReservaSalas
javascript
class SistemaReservaSalas {
constructor() {
this.usuarios = [];
this.salas = [];
this.reservas = [];
}
registrarUsuario(usuario) {
this.usuarios.push(usuario);
console.log(`Usuario ${usuario.nombre} registrado con éxito`);
}
añadirSala(sala) {
this.salas.push(sala);
console.log(`Sala ${sala.nombre} añadida con éxito`);
}
reservarSala(usuario, nombreSala, fecha) {
const sala = this.salas.find(sala => sala.nombre === nombreSala);
if (!sala) {
throw new Error(`La sala "${nombreSala}" no existe`);
}
if (!sala.estaDisponible()) {
throw new Error(`La sala "${nombreSala}" no está disponible`);
}
const reserva = new Reserva(usuario, sala, fecha);
sala.reservar();
this.reservas.push(reserva);
usuario.recibirNotificacion(`Reserva exitosa de la sala "${nombreSala}" para el ${fecha}`);
}
cancelarReserva(usuario, nombreSala) {
const reserva = this.reservas.find(reserva => reserva.usuario === usuario && reserva.sala.nombre === nombreSala);
if (!reserva) {
throw new Error(`No existe una reserva de la sala "${nombreSala}" para el usuario ${usuario.nombre}`);
}
reserva.sala.cancelarReserva();
this.reservas = this.reservas.filter(r => r !== reserva);
usuario.recibirNotificacion(`Reserva de la sala "${nombreSala}" cancelada con éxito`);
}
}
Pruebas Unitarias
Instalar Jest:
Ejecuta npm install --save-dev jest
Configurar Jest:
Añade la siguiente sección a tu package.json:
json
"scripts": {
"test": "jest"
}
Escribir Pruebas Unitarias:
javascript
// Archivo sistemaReservaSalas.test.js
const Sala = require('./sala');
const Usuario = require('./usuario');
const Reserva = require('./reserva');
const SistemaReservaSalas = require('./sistemaReservaSalas');
test('reservar y cancelar sala', () => {
const sistema = new SistemaReservaSalas();
const sala = new Sala('Sala 1');
const usuario = new Usuario('Juan', 'juan@example.com');
sistema.añadirSala(sala);
sistema.registrarUsuario(usuario);
sistema.reservarSala(usuario, 'Sala 1', '2023-12-31');
expect(sala.estaDisponible()).toBe(false);
sistema.cancelarReserva(usuario, 'Sala 1');
expect(sala.estaDisponible()).toBe(true);
});
test('manejar error al reservar una sala ya reservada', () => {
const sistema = new SistemaReservaSalas();
const sala = new Sala('Sala 1');
const usuario1 = new Usuario('Juan', 'juan@example.com');
const usuario2 = new Usuario('Ana', 'ana@example.com');
sistema.añadirSala(sala);
sistema.registrarUsuario(usuario1);
sistema.registrarUsuario(usuario2);
sistema.reservarSala(usuario1, 'Sala 1', '2023-12-31');
expect(() => sistema.reservarSala(usuario2, 'Sala 1', '2023-12-31')).toThrow('La sala "Sala 1" no está disponible');
});
Estrategias de Repaso
Revisar Notas y Materiales: Asegúrate de revisar tus notas y materiales de estudio.
Practicar Ejercicios: Vuelve a hacer ejercicios anteriores y asegúrate de entender las soluciones.
Resolver Ejercicios Nuevos: Busca o inventa nuevos ejercicios para resolver.
Repasar Preguntas Teóricas: Revisa las preguntas teóricas y asegúrate de poder explicar los conceptos claramente.
Simulación de Examen: Simula un examen con tiempo limitado para practicar la gestión del tiempo y la presión del examen.
Has llegado al final de tu preparación para el examen. Has cubierto una amplia gama de temas en Programación Orientada a Objetos, principios SOLID, patrones de diseño, manejo de excepciones y pruebas unitarias. Confía en tus conocimientos, sigue practicando, y asegúrate de estar bien descansado antes del examen.
¡Buena suerte en tu examen real y sigue adelante con tu aprendizaje y desarrollo como programador!