IF0100 - Programación OO II

Unidad 1: Programación Orientada a Objetos

Clase 4: Clases Abstractas e Interfaces

Jueves, 26 de febrero de 2026 Semana 4 - Jueves (60 minutos) 30 min Teoría 30 min Práctica

E1: Modelado del Dominio - Entrega Hoy (26/02/2026)

Objetivos de Aprendizaje

Al finalizar esta clase, serás capaz de:

  • Entender qué son las clases abstractas
  • Crear clases abstractas con ABC y @abstractmethod
  • Definir interfaces para estandarizar comportamiento
  • Implementar el patrón Repository para TaskFlow

Clases Abstractas (15 min)

Una clase abstracta es una clase que no puede instanciarse directamente. Sirve como "molde" para otras clases.

Analogía: No puedes crear un "Animal" genérico, pero sí un "Perro" o un "Gato". Animal sería abstracta.

¿Por qué usar clases abstractas?

  • Forzar a las clases hijas a implementar ciertos métodos
  • Definir una interfaz común
  • Evitar instanciar clases incompletas
# Sin clases abstractas - PROBLEMA
class Animal:
    def hacer_sonido(self):
        # ¡No implementado!
        pass

class Perro(Animal):
    def hacer_sonido(self):
        return "Guau"

class Gato(Animal):
    # Olvidé implementar hacer_sonido!
    pass

# Funciona pero puede causar errores
gato = Gato()
print(gato.hacer_sonido())  # None - ¿Error silencioso?

ABC y @abstractmethod (25 min)

Python usa el módulo abc (Abstract Base Classes) para crear clases abstractas.

from abc import ABC, abstractmethod

class Animal(ABC):  # Hereda de ABC
    """Clase abstracta - no se puede instanciar."""
    
    def __init__(self, nombre):
        self.nombre = nombre
    
    @abstractmethod
    def hacer_sonido(self):
        """Método abstracto - DEBE implementarse en la hija."""
        pass
    
    @abstractmethod
    def moverse(self):
        """Otro método abstracto."""
        pass
    
    def dormir(self):
        """Método normal - opcional implementar en hija."""
        return f"{self.nombre} está durmiendo"


class Perro(Animal):
    def hacer_sonido(self):
        return "¡Guau!"
    
    def moverse(self):
        return f"{self.nombre} está corriendo en 4 patas"


class Gato(Animal):
    def hacer_sonido(self):
        return "¡Miau!"
    
    def moverse(self):
        return f"{self.nombre} está saltando silenciosamente"


# Uso
# animal = Animal("Genérico")  # ❌ Error: No se puede instanciar clase abstracta

perro = Perro("Fido")
gato = Gato("Michi")

print(perro.hacer_sonido())   # "¡Guau!"
print(gato.hacer_sonido())    # "¡Miau!"
print(perro.dormir())         # "Fido está durmiendo" (heredado)

# Polimorfismo
animales = [perro, gato]
for a in animales:
    print(f"{a.nombre}: {a.hacer_sonido()}")
Regla: Una clase hija DEBE implementar todos los métodos abstractos del padre, o también será abstracta y no podrá instanciarse.

Interfaces y Protocol (10 min)

Una interfaz define qué debe hacer una clase, no cómo. En Python moderno usamos Protocol.

from typing import Protocol

class Guardable(Protocol):
    """Interfaz para objetos que se pueden guardar."""
    
    def guardar(self) -> None:
        """Guardar el objeto."""
        ...
    
    def cargar(self, id: int) -> None:
        """Cargar el objeto por ID."""
        ...

# Cualquier clase que implemente guardar() y cargar()
# automáticamente cumple con la interfaz Guardable

class Usuario:
    def __init__(self, username):
        self.username = username
    
    def guardar(self):
        print(f"Guardando usuario {self.username}")
    
    def cargar(self, id):
        print(f"Cargando usuario con ID {id}")

class Proyecto:
    def __init__(self, nombre):
        self.nombre = nombre
    
    def guardar(self):
        print(f"Guardando proyecto {self.nombre}")
    
    def cargar(self, id):
        print(f"Cargando proyecto con ID {id}")

# Función que acepta cualquier objeto "Guardable"
def procesar_guardado(objeto: Guardable):
    objeto.guardar()

# Uso
u = Usuario("juan")
p = Proyecto("Website")

procesar_guardado(u)  # ✅ Funciona
procesar_guardado(p)  # ✅ Funciona

Ejercicio: Repository Pattern (20 min)

Implementa el patrón Repository para TaskFlow usando clases abstractas.

CRUD: Sigla para Create, Read, Update, Delete (Crear, Leer, Actualizar, Eliminar), que representa las cuatro operaciones básicas de la gestión de datos.
# repository.py
from abc import ABC, abstractmethod
from typing import List, Optional

class Usuario:
    """Modelo simple de usuario."""
    def __init__(self, id: int, username: str, email: str):
        self.id = id
        self.username = username
        self.email = email
    
    def __repr__(self):
        return f"Usuario({self.id}, {self.username})"


class Repository(ABC):
    """
    Interfaz abstracta para repositorios.
    Define las operaciones CRUD que debe implementar cualquier repositorio.
    """
    
    @abstractmethod
    def crear(self, usuario: Usuario) -> Usuario:
        """Crea un nuevo usuario."""
        pass
    
    @abstractmethod
    def obtener_por_id(self, id: int) -> Optional[Usuario]:
        """Obtiene un usuario por ID."""
        pass
    
    @abstractmethod
    def obtener_todos(self) -> List[Usuario]:
        """Obtiene todos los usuarios."""
        pass
    
    @abstractmethod
    def actualizar(self, usuario: Usuario) -> Usuario:
        """Actualiza un usuario existente."""
        pass
    
    @abstractmethod
    def eliminar(self, id: int) -> bool:
        """Elimina un usuario por ID."""
        pass


class RepositorioEnMemoria(Repository):
    """
    Implementación del repositorio en memoria (para pruebas).
    """
    
    def __init__(self):
        self._usuarios = {}
        self._contador_id = 1
    
    def crear(self, usuario: Usuario) -> Usuario:
        usuario.id = self._contador_id
        self._usuarios[usuario.id] = usuario
        self._contador_id += 1
        return usuario
    
    def obtener_por_id(self, id: int) -> Optional[Usuario]:
        return self._usuarios.get(id)
    
    def obtener_todos(self) -> List[Usuario]:
        return list(self._usuarios.values())
    
    def actualizar(self, usuario: Usuario) -> Usuario:
        if usuario.id not in self._usuarios:
            raise ValueError(f"Usuario {usuario.id} no encontrado")
        self._usuarios[usuario.id] = usuario
        return usuario
    
    def eliminar(self, id: int) -> bool:
        if id in self._usuarios:
            del self._usuarios[id]
            return True
        return False


class RepositorioArchivo(Repository):
    """
    Implementación del repositorio en archivo (simulado).
    """
    
    def __init__(self, archivo: str):
        self._archivo = archivo
        self._usuarios = {}
        self._contador_id = 1
        self._cargar_desde_archivo()
    
    def _cargar_desde_archivo(self):
        """Simula cargar desde archivo."""
        # En un caso real: leer JSON/CSV
        pass
    
    def _guardar_en_archivo(self):
        """Simula guardar en archivo."""
        # En un caso real: escribir JSON/CSV
        pass
    
    def crear(self, usuario: Usuario) -> Usuario:
        usuario.id = self._contador_id
        self._usuarios[usuario.id] = usuario
        self._contador_id += 1
        self._guardar_en_archivo()
        return usuario
    
    def obtener_por_id(self, id: int) -> Optional[Usuario]:
        return self._usuarios.get(id)
    
    def obtener_todos(self) -> List[Usuario]:
        return list(self._usuarios.values())
    
    def actualizar(self, usuario: Usuario) -> Usuario:
        if usuario.id not in self._usuarios:
            raise ValueError(f"Usuario {usuario.id} no encontrado")
        self._usuarios[usuario.id] = usuario
        self._guardar_en_archivo()
        return usuario
    
    def eliminar(self, id: int) -> bool:
        if id in self._usuarios:
            del self._usuarios[id]
            self._guardar_en_archivo()
            return True
        return False


# === PRUEBAS ===
if __name__ == "__main__":
    # No podemos instanciar Repository directamente
    # repo = Repository()  # ❌ Error
    
    # Pero sí las implementaciones concretas
    print("=== Repositorio en Memoria ===")
    repo_memoria = RepositorioEnMemoria()
    
    # Crear usuarios
    u1 = repo_memoria.crear(Usuario(None, "juan", "juan@email.com"))
    u2 = repo_memoria.crear(Usuario(None, "ana", "ana@email.com"))
    print(f"Creados: {u1}, {u2}")
    
    # Obtener todos
    todos = repo_memoria.obtener_todos()
    print(f"Total usuarios: {len(todos)}")
    
    # Obtener por ID
    encontrado = repo_memoria.obtener_por_id(1)
    print(f"Encontrado: {encontrado}")
    
    # Actualizar
    u1.email = "juan.nuevo@email.com"
    repo_memoria.actualizar(u1)
    print(f"Actualizado: {repo_memoria.obtener_por_id(1)}")
    
    # Eliminar
    repo_memoria.eliminar(2)
    print(f"Después de eliminar: {len(repo_memoria.obtener_todos())} usuarios")
    
    print("\n=== Polimorfismo con Repository ===")
    
    def probar_repositorio(repo: Repository):
        """Funciona con cualquier implementación de Repository."""
        u = repo.crear(Usuario(None, "test", "test@email.com"))
        print(f"Creado en {type(repo).__name__}: {u}")
        return u
    
    # Funciona con ambas implementaciones
    probar_repositorio(RepositorioEnMemoria())
    probar_repositorio(RepositorioArchivo("usuarios.txt"))
Conexión con TaskFlow: El patrón Repository separa la lógica de negocio del acceso a datos. Podemos cambiar de "en memoria" a "base de datos" sin modificar el código que usa el repositorio.

Resumen y Preparación E1

Unidad 1: POO en Python

  1. Clases y Objetos: __init__, self, atributos, métodos
  2. Encapsulamiento: _, __, @property, getters/setters
  3. Herencia: class Hija(Padre), super(), sobreescritura
  4. Polimorfismo: Mismo método, distinto comportamiento
  5. Clases Abstractas: ABC, @abstractmethod, interfaces

Tips para el Examen E1 (15%)

  • Practica crear clases completas con __init__, atributos y métodos
  • Domina @property para encapsulamiento
  • Sabe crear jerarquías con herencia y super()
  • Entiende cuándo usar clases abstractas
  • Lee atentamente los enunciados antes de escribir código
¡Éxito en el examen! Recuerda: la POO es el fundamento de todo el curso. Domina estos conceptos y el resto será más fácil.
← Anterior: Herencia y Polimorfismo
Clase 6 de 25
Siguiente: Introducción a TDD →