miércoles, 14 de enero de 2026

Principios Fundamentales de Diseño en Python - Parte 2 - SOLID

Principios SOLID en Python
Python // Design Patterns

Principios SOLID

Los 5 pilares del diseño orientado a objetos robusto y mantenible

En el mundo de la ingeniería de software, los principios y mejores prácticas son la columna vertebral de un código base robusto, mantenible y eficiente. SOLID es un acrónimo acuñado por Robert C. Martin, que representa un conjunto de cinco principios de diseño orientados a hacer el software más comprensible, flexible y mantenible.

S
O
L
I
D

Cada letra representa un principio fundamental: Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation y Dependency Inversion. Al final de este artículo, tendrás un entendimiento profundo de estos cinco principios y cómo aplicarlos en Python.

S // PRINCIPIO_01

Single Responsibility Principle

DEFINICIÓN
Una clase debe tener una única razón para cambiar. Cada clase debe ser responsable de un solo aspecto de la funcionalidad y esa responsabilidad debe estar completamente encapsulada dentro de esa clase.

Al adherirse al SRP, estás esforzándote por clases que son enfocadas, cohesivas y especializadas en su funcionalidad. Este enfoque juega un papel crucial en mejorar la mantenibilidad y comprensibilidad de tu código.

Beneficios

MANTENIBILIDAD
Clases pequeñas y enfocadas son más fáciles de mantener
TESTABILIDAD
Responsabilidades aisladas facilitan las pruebas unitarias
REUTILIZACIÓN
Componentes especializados pueden reutilizarse en otros contextos
Separación de Responsabilidades
Report
——
ReportSaver

Ejemplo: Sistema de Reportes

srp.py
class Report:
    def __init__(self, content: str):
        self.content: str = content
    
    def generate(self):
        print(f"Report content: {self.content}")

class ReportSaver:
    def __init__(self, report: Report):
        self.report: Report = report
    
    def save_to_file(self, filename: str):
        with open(filename, 'w') as file:
            file.write(self.report.content)

# Uso
if __name__ == "__main__":
    report_content = "This is the content."
    report = Report(report_content)
    report.generate()
    
    report_saver = ReportSaver(report)
    report_saver.save_to_file("report.txt")
python srp.py
Report content: This is the content.
El SRP no se trata de minimizar las líneas de código en una clase, sino de asegurar que una clase tenga una única razón para cambiar, reduciendo la probabilidad de efectos secundarios no deseados al hacer modificaciones.
O // PRINCIPIO_02

Open-Closed Principle

DEFINICIÓN
Las entidades de software (clases, módulos, funciones) deben estar abiertas para extensión pero cerradas para modificación. Una vez definida e implementada, una entidad no debería cambiarse para agregar nueva funcionalidad.

El OCP proporciona una base robusta para construir sistemas de software flexibles y mantenibles. Permite a los desarrolladores introducir nuevas características sin alterar el código existente, minimizando el riesgo de introducir bugs o efectos secundarios no deseados.

Beneficios

ESTABILIDAD
El código existente permanece intacto y probado
EXTENSIBILIDAD
Nuevas funcionalidades se agregan mediante extensión
SEGURIDAD
Reduce riesgo de romper funcionalidad existente
Extensión mediante Protocol
Shape (Protocol)
Rectangle
Circle

Ejemplo: Cálculo de Áreas

ocp.py
import math
from typing import Protocol

class Shape(Protocol):
    def area(self) -> float:
        ...

class Rectangle:
    def __init__(self, width: float, height: float):
        self.width: float = width
        self.height: float = height
    
    def area(self) -> float:
        return self.width * self.height

class Circle:
    def __init__(self, radius: float):
        self.radius: float = radius
    
    def area(self) -> float:
        return math.pi * (self.radius ** 2)

# Esta función NO necesita modificarse para nuevas formas
def calculate_area(shape: Shape) -> float:
    return shape.area()

# Uso
if __name__ == "__main__":
    shapes = [Rectangle(10, 5), Circle(7)]
    for shape in shapes:
        print(f"Area: {calculate_area(shape):.2f}")
python ocp.py
Area: 50.00
Area: 153.94
Para agregar una nueva forma (Triangle, Hexagon, etc.), simplemente crea una nueva clase que implemente el método area(). La función calculate_area() funcionará automáticamente sin modificaciones.
L // PRINCIPIO_03

Liskov Substitution Principle

DEFINICIÓN
Los objetos de una clase derivada deben poder sustituir a los objetos de la clase base sin alterar el comportamiento correcto del programa. Si S es un subtipo de T, entonces los objetos de tipo T pueden ser reemplazados por objetos de tipo S.

El LSP es crucial para mantener la integridad del polimorfismo en el diseño orientado a objetos. Garantiza que una subclase pueda sustituir a su clase padre sin causar errores o comportamientos inesperados.

Beneficios

POLIMORFISMO
Garantiza sustitución segura de tipos
CONFIABILIDAD
Comportamiento predecible en jerarquías de clases
DISEÑO ROBUSTO
Jerarquías de herencia correctas y lógicas
Jerarquía Correcta
Bird (Protocol)
Sparrow
Penguin

Ejemplo: Sistema de Aves

lsp.py
from typing import Protocol

class Bird(Protocol):
    def move(self) -> str:
        ...

class Sparrow:
    def move(self) -> str:
        return "Sparrow is flying"

class Penguin:
    def move(self) -> str:
        return "Penguin is swimming"

def make_bird_move(bird: Bird) -> None:
    print(bird.move())

# Ambas clases pueden sustituir al tipo Bird
if __name__ == "__main__":
    sparrow = Sparrow()
    penguin = Penguin()
    
    make_bird_move(sparrow)
    make_bird_move(penguin)
python lsp.py
Sparrow is flying
Penguin is swimming
Nota cómo usamos move() en lugar de fly(). Un pingüino no puede volar, pero sí puede moverse. Al diseñar la abstracción correctamente, ambas clases pueden sustituir al tipo Bird sin violar el contrato.
I // PRINCIPIO_04

Interface Segregation Principle

DEFINICIÓN
Los clientes no deberían verse forzados a depender de interfaces que no utilizan. Es mejor tener muchas interfaces específicas que una interfaz general y monolítica.

El ISP sugiere que al diseñar software, debemos evitar crear interfaces grandes y monolíticas. En su lugar, debemos enfocarnos en crear interfaces más pequeñas y específicas, permitiendo que las clases implementen solo lo que necesitan.

Beneficios

MODULARIDAD
Interfaces pequeñas y enfocadas
FLEXIBILIDAD
Clases implementan solo lo necesario
DESACOPLAMIENTO
Dependencias mínimas entre componentes
Interfaces Segregadas
Printer
Scanner
Fax

Ejemplo: Sistema de Impresoras

isp.py
from typing import Protocol

# Interfaces segregadas
class Printer(Protocol):
    def print_document(self):
        ...

class Scanner(Protocol):
    def scan_document(self):
        ...

class Fax(Protocol):
    def fax_document(self):
        ...

# Implementa TODAS las interfaces
class AllInOnePrinter:
    def print_document(self):
        print("Printing")
    
    def scan_document(self):
        print("Scanning")
    
    def fax_document(self):
        print("Faxing")

# Implementa SOLO Printer
class SimplePrinter:
    def print_document(self):
        print("Simply Printing")

def do_the_print(printer: Printer):
    printer.print_document()

# Uso
if __name__ == "__main__":
    all_in_one = AllInOnePrinter()
    all_in_one.scan_document()
    all_in_one.fax_document()
    do_the_print(all_in_one)
    
    simple = SimplePrinter()
    do_the_print(simple)
python isp.py
Scanning
Faxing
Printing
Simply Printing
SimplePrinter solo implementa print_document() porque es lo único que necesita. No está forzada a implementar métodos de scanner o fax que nunca usará.
D // PRINCIPIO_05

Dependency Inversion Principle

DEFINICIÓN
Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones. Las abstracciones no deben depender de los detalles; los detalles deben depender de las abstracciones.

El DIP está estrechamente vinculado con el principio de acoplamiento débil. Promueve un diseño donde los componentes interactúan a través de interfaces en lugar de implementaciones concretas, reduciendo las interdependencias entre módulos.

Beneficios

DESACOPLAMIENTO
Componentes independientes e intercambiables
TESTABILIDAD
Fácil inyección de mocks para pruebas
EXTENSIBILIDAD
Agregar nuevas implementaciones sin cambios
Inversión de Dependencias
MessageSender (Protocol)
Email
Notification

Ejemplo: Sistema de Notificaciones

dip.py
from typing import Protocol

# Abstracción
class MessageSender(Protocol):
    def send(self, message: str):
        ...

# Implementación de bajo nivel
class Email:
    def send(self, message: str):
        print(f"Sending email: {message}")

# Módulo de alto nivel depende de abstracción
class Notification:
    def __init__(self, sender: MessageSender):
        self.sender: MessageSender = sender
    
    def send(self, message: str):
        self.sender.send(message)

# Uso con inyección de dependencias
if __name__ == "__main__":
    email = Email()
    notif = Notification(sender=email)
    notif.send(message="This is the message.")
python dip.py
Sending email: This is the message.
Con este diseño actualizado, tanto Notification como Email están basados en la abstracción MessageSender. Puedes agregar SMS, Push Notifications, o cualquier otro sender sin modificar Notification.

Resumen de Principios SOLID

S // Single Responsibility

Una clase, una responsabilidad. Cada clase debe tener una única razón para cambiar.

O // Open-Closed

Abierto para extensión, cerrado para modificación. Extiende sin modificar código existente.

L // Liskov Substitution

Las subclases deben poder sustituir a sus clases base sin romper el programa.

I // Interface Segregation

Interfaces pequeñas y específicas. Ningún cliente debe depender de métodos que no usa.

D // Dependency Inversion

Depende de abstracciones, no de implementaciones concretas. Usa inyección de dependencias.

// Próximos Pasos

Con SOLID dominado, estás listo para explorar los Patrones de Diseño del Gang of Four.

Basado en "Mastering Python Design Patterns" // Kamon Ayeva & Sakis Kasampalis