Aplicar composición en programación orientada a objetos permite escribir código más claro, reutilizable y seguro. Aquí verás cómo delegar el manejo de logs en una clase especializada (LogTracker) e integrarlo en Habit, evitando estados globales y reforzando la encapsulación.
¿Qué es la composición y cómo se diferencia de la herencia?
La composición es un patrón de diseño donde un objeto delega responsabilidades a otros objetos especializados, en lugar de heredarlas. Con la analogía del coche: con herencia dirías “un coche es un vehículo con motor y ruedas”; con composición, “un coche tiene un motor, un conjunto de ruedas y un sistema de frenos”.
Con herencia, una clase “es un” tipo base.
Con composición, una clase “tiene un” conjunto de capacidades.
Beneficios: separación de responsabilidades, reutilización entre contextos, y encapsulación del estado privado.
No todo lo que tiene motor es coche. La composición evita acoplar funciones solo por compartir rasgos.
¿Qué habilidades y keywords se practican?
Patrón de diseño: composición y delegación de responsabilidades.
Encapsulación de estado privado y acceso controlado.
Validación de datos y manejo de errores con null.
Eliminación de arreglo global y de habit id counter no usado.
Métodos clave: addLog, getLogs, register check in, render habits.
¿Cómo construir LogTracker para manejar logs?
Se crea la clase LogTracker encargada del sistema de logs. Mantiene un array privado de fechas y expone dos métodos: addLog(date) para validar y agregar, y getLogs() para devolver una copia del historial. Si la fecha no es válida (no es string o su longitud es distinta de 10), retorna null. Así se protege el estado y se estandariza el flujo de error.
classLogTracker{private dates:string[]=[];addLog(date:string):string|null{if(typeof date !=='string'|| date.length!==10)returnnull;this.dates.push(date);return date;}getLogs():string[]{return[...this.dates];// copia para evitar mutaciones externas}}
Estado privado: el array de fechas solo se modifica dentro de la clase.
Validación simple: tipo string y longitud de 10 caracteres.
Copia defensiva en getLogs para impedir modificaciones externas al array interno.
¿Cómo integrar la composición en Habit y actualizar el flujo?
La clase Habit no maneja logs directamente. En su lugar, delegará en una instancia privada de LogTracker (un tracker especializado). Se añaden métodos que usan el tracker para registrar y consultar. Luego se actualizan funciones externas para consumir esa delegación y se eliminan estructuras globales.
classHabit{private id:string;private tracker:LogTracker;constructor(id:string){this.id= id;this.tracker=newLogTracker();// composición: "tiene un" LogTracker}registerCheckIn(date:string):{ habitId:string; date:string}|null{const created =this.tracker.addLog(date);if(!created)returnnull;// si la fecha es inválidareturn{ habitId:this.id, date: created };}getLogs():string[]{returnthis.tracker.getLogs();}}
Campo privado: tracker mantiene la instancia de LogTracker.
Constructor: instancia con new LogTracker() para habilitar la delegación.
Método: register check in usa tracker.addLog(date), retorna objeto { habitId, date } o null.
Método: get logs delega a tracker.getLogs().
¿Qué cambios aplicar en las funciones externas?
En la función de log del hábito, delegar: habit.registerCheckIn(fecha).
Eliminar el arreglo global de logs.
Eliminar habit id counter que ya no se usa.
En render habits, obtener logs con habit.getLogs().
functionlogHabit(habit:Habit, date:string){return habit.registerCheckIn(date);}functionrenderHabits(habits:Habit[]){for(const habit of habits){const logs = habit.getLogs();// ...pintar logs del hábito}}
Resultado: cada hábito gestiona sus propios registros.
Datos relacionados viven juntos: Habit y su LogTracker.
Efecto colateral: menos acoplamiento, mayor claridad y reutilización.
¿Te animas a refactorizar otra parte con composición y delegación? Comparte tu solución y cuéntanos cómo lo resolviste.
De acuerdo a lo aprendido y con unos cuantos tropezones, logré implementar
La clase HabitList, en la cual, nos desprenderemos del array global habits, donde también buscaremos los hábitos por el id.
Añadimos un TryCatch en la función addHabit
En la función log habit usamos el método findById
En la función renderHabits,attachCheckboxListeners, setupModalListeners e initApp, reemplazamos las variables Habits por myHabits que hacian referencia al arreglo global.
// APLICANDO EL CONCEPTO DE COMPOSICIÓN, HAREMOS LO MISMO PARA EL ARRAY DE HABITOS classHabitList{//PRIVATIZAMOS EL ARRAY PARA QUE NADIE DESDE AFUERA PUEDA HACER UN .PUSH() ACCIDENTAL #items =[] #maxItems =5// MÉTODO PARA AÑADIR DE FORMA SEGURAadd(habit){if(this.#items.length>=this.#maxItems){thrownewError(`Has alcanzado el límite máximo de ${this.#maxItems} hábitos`)}this.#items.push(habit)return habit
}// MÉTODO PARA BUSCAR UN HÁBITO POR SU IDfindById(id){returnthis.#items.find(h=> h.id===id )}// GET PARA DARLE A LA INTERFAZ UNA COPIA SEGURA PARA RENDERIZARgetall(){return[...this.#items]}//GETTER PARA SABER CUÁNTOS HÁBITOS VANgetcount(){returnthis.#items.length}}const myHabits=newHabitList()//AÑADIMOS UN TRY CATCH EN LA FUNCIÓN addHabittry{const habit =newHabit(name.trim(), frequency);// USANDO LA CLASE: Intentamos añadirlo a nuestra lista myHabits.add(habit)return habit;}catch(error){// Si la lista está llena, el método .add() lanzará el error y caerá aquíshowMessage(error.message,'error')returnnull}functionlogHabit(habitId, date){// USANDO LA CLASE: Le pedimos a la lista que busque el hábito por IDconst habit = myHabits.findById(habitId);if(!habit){showMessage('Hábito no encontrado','error');returnnull;}return habit.registerCheckIn(date)}
En este sentido, seria lo mismo si extiendo la clase para hacer uso de los metodos y propiedades dentro de mi clase principal?