Gestor OOP en Python: Proyecto Completo | Capítulo 32

En este capítulo, nos sumergimos en un proyecto práctico que consolida los fundamentos de la programación orientada a objetos (OOP) en Python. Construiremos un gestor de tareas completo, una aplicación que permite crear, gestionar y priorizar tareas de manera eficiente. Usaremos clases como Tarea y TaskManager, incorporaremos herencia para diferenciar tipos de tareas (por ejemplo, tareas urgentes o recurrentes), implementaremos properties para un control seguro de los atributos, y definiremos métodos dunder como __repr__ para una representación clara de los objetos. Este proyecto no solo refuerza conceptos clave de OOP, sino que te equipa con habilidades para diseñar sistemas modulares y escalables, similares a herramientas cotidianas como listas de tareas en apps móviles.

Construyendo la Clase Base: Tarea

Comencemos por el núcleo de nuestro gestor: la clase Tarea. Imagina que una tarea es como una nota adhesiva en tu escritorio – tiene un título, una descripción, una fecha límite y un estado (pendiente o completada). En OOP, encapsulamos estos elementos en una clase para que sean reutilizables y fáciles de manejar.

La clase Tarea servirá como base para otros tipos de tareas más especializadas, gracias a la herencia. Antes de codificar, explico cada parte: usaremos un inicializador (__init__) para configurar los atributos, properties para acceder y modificarlos de forma controlada (evitando valores inválidos, como una fecha límite en el pasado), y el dunder __repr__ para que, al imprimir un objeto, veamos una representación legible en lugar de algo críptico como <Tarea object at 0x123>.

Aquí está el código inicial para Tarea. Guarda esto en un archivo llamado gestor_tareas.py.

import datetime  # Para manejar fechas de manera precisa

class Tarea:
    def __init__(self, titulo, descripcion, fecha_limite=None):
        """Inicializa una tarea con título, descripción y fecha límite opcional."""
        self._titulo = titulo  # Atributo privado para encapsulación
        self._descripcion = descripcion
        self._fecha_limite = fecha_limite if fecha_limite else datetime.date.today() + datetime.timedelta(days=7)
        self._completada = False  # Por defecto, la tarea está pendiente

    @property
    def titulo(self):
        """Property para obtener el título de la tarea."""
        return self._titulo

    @titulo.setter
    def titulo(self, nuevo_titulo):
        """Setter para validar y actualizar el título (debe ser no vacío)."""
        if not nuevo_titulo:
            raise ValueError("El título no puede estar vacío.")
        self._titulo = nuevo_titulo

    @property
    def descripcion(self):
        """Property para obtener la descripción."""
        return self._descripcion

    @descripcion.setter
    def descripcion(self, nueva_descripcion):
        """Setter para actualizar la descripción."""
        self._descripcion = nueva_descripcion

    @property
    def fecha_limite(self):
        """Property para obtener la fecha límite."""
        return self._fecha_limite

    @fecha_limite.setter
    def fecha_limite(self, nueva_fecha):
        """Setter para validar que la fecha límite no sea en el pasado."""
        if nueva_fecha < datetime.date.today():
            raise ValueError("La fecha límite no puede ser en el pasado.")
        self._fecha_limite = nueva_fecha

    @property
    def completada(self):
        """Property para verificar si la tarea está completada."""
        return self._completada

    def marcar_completada(self):
        """Método para marcar la tarea como completada."""
        self._completada = True

    def __repr__(self):
        """Representación legible del objeto Tarea."""
        estado = "Completada" if self._completada else "Pendiente"
        return f"Tarea('{self._titulo}', '{self._descripcion}', límite: {self._fecha_limite}, estado: {estado})"
Python

Para ejecutar esto, crea una instancia en el mismo archivo o en un script separado. Por ejemplo, agrega al final de gestor_tareas.py:

if __name__ == "__main__":
    tarea = Tarea("Comprar leche", "Ir al supermercado")
    print(tarea)  # Salida: Tarea('Comprar leche', 'Ir al supermercado', límite: [fecha futura], estado: Pendiente)
Python

Ejecútalo con python gestor_tareas.py. Observa cómo las properties protegen los atributos: intenta tarea.titulo = "" y verás un error, lo cual es como un guardián que asegura la integridad de tus datos.

Extendiendo con Herencia: Tipos de Tareas Especializadas

La herencia es como heredar rasgos familiares: una clase hija hereda comportamientos de la base pero puede agregar o modificarlos. Crearemos dos subclases de TareaTareaUrgente (con prioridad alta y notificaciones) y TareaRecurrente (que se repite automáticamente al completarse).

Explico paso a paso: la herencia permite reutilizar código de Tarea sin duplicarlo. Sobrescribiremos __init__ para agregar atributos específicos y __repr__ para una representación personalizada. Esto hace nuestro gestor más flexible, como diferenciar tareas diarias de emergencias en una app real.

Agrega esto al mismo archivo gestor_tareas.py:

class TareaUrgente(Tarea):
    def __init__(self, titulo, descripcion, fecha_limite=None, prioridad="Alta"):
        """Inicializa una tarea urgente con prioridad adicional."""
        super().__init__(titulo, descripcion, fecha_limite)  # Llama al init de la clase base
        self._prioridad = prioridad  # Atributo específico para urgencia

    @property
    def prioridad(self):
        """Property para obtener la prioridad."""
        return self._prioridad

    @prioridad.setter
    def prioridad(self, nueva_prioridad):
        """Setter para validar niveles de prioridad (Alta, Media, Baja)."""
        if nueva_prioridad not in ["Alta", "Media", "Baja"]:
            raise ValueError("Prioridad inválida. Debe ser Alta, Media o Baja.")
        self._prioridad = nueva_prioridad

    def notificar(self):
        """Método específico: Simula una notificación para tareas urgentes."""
        print(f"Notificación: ¡Tarea urgente '{self.titulo}' con prioridad {self.prioridad} pendiente!")

    def __repr__(self):
        """Representación personalizada para TareaUrgente."""
        return f"TareaUrgente('{self.titulo}', '{self.descripcion}', límite: {self.fecha_limite}, prioridad: {self.prioridad}, estado: {'Completada' if self.completada else 'Pendiente'})"

class TareaRecurrente(Tarea):
    def __init__(self, titulo, descripcion, fecha_limite=None, intervalo_dias=7):
        """Inicializa una tarea recurrente con intervalo de repetición."""
        super().__init__(titulo, descripcion, fecha_limite)
        self._intervalo_dias = intervalo_dias  # Días para repetir la tarea

    @property
    def intervalo_dias(self):
        """Property para obtener el intervalo de repetición."""
        return self._intervalo_dias

    @intervalo_dias.setter
    def intervalo_dias(self, nuevo_intervalo):
        """Setter para validar que el intervalo sea positivo."""
        if nuevo_intervalo <= 0:
            raise ValueError("El intervalo debe ser un número positivo de días.")
        self._intervalo_dias = nuevo_intervalo

    def marcar_completada(self):
        """Sobrescribe para resetear la fecha límite al completarse."""
        super().marcar_completada()  # Llama al método base
        self._completada = False  # Reinicia el estado
        self._fecha_limite += datetime.timedelta(days=self._intervalo_dias)  # Avanza la fecha

    def __repr__(self):
        """Representación personalizada para TareaRecurrente."""
        return f"TareaRecurrente('{self.titulo}', '{self.descripcion}', límite: {self.fecha_limite}, intervalo: {self.intervalo_dias} días, estado: {'Completada' if self.completada else 'Pendiente'})"
Python

Prueba agregando al final: tarea_urgente = TareaUrgente("Llamar al médico", "Cita urgente") ; print(tarea_urgente) ; tarea_urgente.notificar(). Ejecuta con python gestor_tareas.py. Nota cómo la herencia mantiene el código DRY (Don’t Repeat Yourself), evitando copiar métodos como marcar_completada.

Integrando Todo: La Clase TaskManager

Ahora, unimos todo en TaskManager, que actúa como el “jefe” de las tareas – una lista inteligente que agrega, elimina y lista tareas. Es como un cuaderno digital que organiza tus notas adhesivas.

Explico: esta clase no hereda, pero compone (usa) objetos de Tarea y sus subclases. Usaremos una lista interna para almacenar tareas, métodos para manipularla, y __repr__ para una vista general.

Agrega esto a gestor_tareas.py:

class TaskManager:
    def __init__(self):
        """Inicializa el gestor con una lista vacía de tareas."""
        self._tareas = []  # Lista privada para almacenar tareas

    def agregar_tarea(self, tarea):
        """Agrega una tarea al gestor."""
        if not isinstance(tarea, Tarea):
            raise ValueError("Solo se pueden agregar objetos de tipo Tarea o sus subclases.")
        self._tareas.append(tarea)

    def eliminar_tarea(self, titulo):
        """Elimina una tarea por título."""
        self._tareas = [t for t in self._tareas if t.titulo != titulo]

    def listar_tareas(self):
        """Devuelve una lista de representaciones de tareas."""
        return [repr(t) for t in self._tareas]

    def __repr__(self):
        """Representación del gestor con conteo de tareas."""
        return f"TaskManager con {len(self._tareas)} tareas."
Python

Para un ejemplo completo, agrega:

if __name__ == "__main__":
    manager = TaskManager()
    tarea1 = Tarea("Estudiar Python", "Capítulo 32")
    tarea2 = TareaUrgente("Pagar facturas", "Antes de fin de mes")
    tarea3 = TareaRecurrente("Hacer ejercicio", "Rutina diaria", intervalo_dias=1)
    
    manager.agregar_tarea(tarea1)
    manager.agregar_tarea(tarea2)
    manager.agregar_tarea(tarea3)
    
    print(manager)  # Salida: TaskManager con 3 tareas.
    print(manager.listar_tareas())  # Lista las representaciones de cada tarea
    
    tarea3.marcar_completada()  # Demuestra la recurrencia
    print(tarea3)  # Muestra la fecha actualizada
Python

Ejecuta con python gestor_tareas.py. Has construido un sistema completo: crea tareas variadas, agrégalas al manager, y manipúlalas. Si intentas agregar algo no válido, las validaciones (de properties y métodos) lo previenen.

Optimizando y Reflexionando sobre el Diseño OOP

Hemos optimizado el código para legibilidad y eficiencia: atributos privados evitan accesos directos, properties aseguran validaciones, y dunders mejoran la depuración. Piensa en esto como un edificio: Tarea es la fundación, herencia agrega pisos especializados, y TaskManager es el techo que lo une todo.

Si expandes esto, podrías agregar persistencia (guardar en archivos), pero por ahora, domina estos bloques. Prueba modificando: crea una subclase nueva, como TareaColaborativa con un atributo para colaboradores.

Resumen del capítulo

  • Clase Tarea base: Definimos una clase fundamental con atributos encapsulados, properties para control seguro (getters y setters con validaciones), y __repr__ para representación legible.
  • Herencia aplicada: Creamos subclases TareaUrgente y TareaRecurrente que heredan de Tarea, agregando comportamientos específicos como notificaciones o recurrencia automática, demostrando reutilización de código.
  • Properties en acción: Usamos decoradores @property y setters para manejar accesos a atributos, previniendo errores como fechas inválidas o títulos vacíos, similar a reglas de negocio en apps reales.
  • Dunders implementados: __repr__ personalizado en cada clase proporciona salidas claras y útiles, facilitando la depuración y comprensión de objetos.
  • Clase TaskManager: Integra todo en un gestor que maneja colecciones de tareas, con métodos para agregar, eliminar y listar, culminando en un proyecto OOP completo y funcional.
  • Ejecución y pruebas: Instrucciones claras para correr el código con python gestor_tareas.py, asegurando que puedas experimentar y dominar cada concepto paso a paso.

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio