Principio de sustitución de Liskov en Python

Clase 8 de 27Curso de Patrones de Diseño y SOLID en Python

Resumen

Aprende a aplicar el principio de sustitución de Liskov (LSP) en Python usando protocolos, detectando y corrigiendo un bug real en un notificador por SMS. Verás cómo usar docstrings en formato NumPy, aprovechar un default factory y garantizar que las clases sean intercambiables sin romper el servicio de pagos.

¿Qué problema rompe el principio de sustitución de Liskov?

En el proyecto se reemplazaron las clases abstractas por protocolos (interfaces en Python) para Notifier y Payment Processor. Se añadieron docstrings que documentan comportamiento, parámetros y tipos, usando el formato NumPy en send_confirmation y describiendo también process_transaction.

Se introdujo a propósito un bug para ilustrar LSP: EmailNotifier respeta la firma del método send_confirmation(customer_data), tal como exige el protocolo, pero SMSNotifier exige un parámetro adicional sms_gateway. Al inyectar SMSNotifier en el servicio de pagos —que por defecto notifica por email mediante un default factory—, el debugger mostró: validación de cliente y pago correctas, cargo en Stripe correcto, pero una excepción al ejecutar SMSNotifier por incompatibilidad de firma. Esto impide sustituir email por SMS sin cambios en el servicio, violando LSP.

  • Protocolos: forma de declarar interfaces en Python con comportamiento similar a clases abstractas.
  • Docstrings: documentación del comportamiento y tipos; se usó formato NumPy.
  • Firma del método: el protocolo define send_confirmation(customer_data).
  • Bug: SMSNotifier pedía sms_gateway como parámetro y rompía la sustitución.

¿Cómo se veía la firma incompatible?

from typing import Protocol

class Notifier(Protocol):
    def send_confirmation(self, customer_data):
        ...

class EmailNotifier:
    def send_confirmation(self, customer_data):
        """
        Parameters
        ----------
        customer_data : dict
            Datos del cliente para confirmar la transacción.
        """
        pass

class SMSNotifier:
    # Incumple el protocolo: agrega un parámetro extra.
    def send_confirmation(self, customer_data, sms_gateway):  # rompe LSP
        pass

¿Cómo se corrige con dataclass y atributos?

La solución fue convertir SMSNotifier en data class y mover sms_gateway a un atributo de instancia. Así, el método respeta la firma del protocolo y usa el atributo interno.

from dataclasses import dataclass

@dataclass
class SMSNotifier:
    sms_gateway: str  # Atributo en lugar de parámetro del método.

    def send_confirmation(self, customer_data):
        # Aquí se usaría self.sms_gateway para enviar el SMS.
        pass

# Instanciación correcta del notificador SMS.
sms_notifier = SMSNotifier(sms_gateway="this is a SMS mock get")

Con este ajuste, EmailNotifier y SMSNotifier comparten la misma firma y son sustituibles.

¿Cómo se prueba la sustitución en el servicio de pagos?

En la implementación del servicio se creó un notificador por SMS y otro por email. El servicio de pagos, por defecto, usa el notificador de email gracias al default factory, pero se puede pasar el notificador SMS por parámetro. Con un breakpoint en la sección clave y usando el debugger se verificó: validación de cliente y pago correctas, cargo en Stripe, y tras la corrección, no se generó ninguna excepción. Luego se probó cambiando al notificador por email y todo funcionó de forma intercambiable, sin modificar el servicio.

¿Qué habilidades y conceptos se refuerzan?

  • Principio de sustitución de Liskov: clases hijas o implementaciones deben ser intercambiables sin romper el cliente.
  • Protocolos en Python: alternativa ligera a clases abstractas para definir interfaces.
  • Docstrings en formato NumPy: claridad en comportamiento, parámetros y tipos.
  • Firma consistente: evitar parámetros extra que rompen la interfaz.
  • Data class: mover dependencias a atributos de instancia.
  • Instanciación correcta: pasar el sms_gateway al crear SMSNotifier.
  • Pruebas con debugger y breakpoint: validar flujo y detectar excepciones.
  • Configuración por defecto con default factory: email como estrategia de notificación base.

¿Qué errores detectarías y cómo mejorarlos?

  • Verifica que todas las implementaciones de Notifier respeten la misma firma.
  • Asegura que los servicios acepten cualquier notificador sin cambios internos.
  • Documenta con docstrings los tipos esperados y valores de retorno.
  • Repite la prueba con ambos notificadores para confirmar la intercambiabilidad.

¿De qué otra forma aplicarías Liskov aquí? ¿Qué otros detalles mejorarías en el código? Comparte tu enfoque en los comentarios.