miércoles, 14 de enero de 2026

Principios Fundamentales de Diseño en Python - Parte 1 - Conociendo estos principios

Principios Fundamentales de Diseño en Python
Python // Design Patterns

Principios Fundamentales de Diseño

Los 4 pilares para escribir código mantenible, flexible y robusto

Los principios de diseño forman la base de cualquier software bien arquitecturado. Sirven como guía para ayudar a los desarrolladores a crear aplicaciones mantenibles, escalables y robustas, evitando las trampas del mal diseño.

En este artículo exploraremos los cuatro principios fundamentales que todo desarrollador Python debe conocer: Encapsular lo que Varía, Favorecer Composición sobre Herencia, Programar hacia Interfaces y Acoplamiento Débil.

PRINCIPIO_01

Encapsular lo que Varía

Uno de los desafíos más comunes en el desarrollo de software es lidiar con el cambio. Los requisitos evolucionan, las tecnologías avanzan y las necesidades de los usuarios cambian. Este principio nos enseña a aislar las partes del código que son más propensas a cambiar y encapsularlas.

Beneficios

MANTENIMIENTO
Solo modificas las partes encapsuladas, reduciendo el riesgo de bugs
FLEXIBILIDAD
Los componentes pueden intercambiarse o extenderse fácilmente
LEGIBILIDAD
El código queda más organizado y fácil de entender

Sistema de Pagos con Polimorfismo

Veamos cómo encapsular la lógica de procesamiento de pagos, donde el método de pago puede variar:

Arquitectura del Sistema
PaymentBase
CreditCard
PayPal
encapsulate.py
class PaymentBase:
    def __init__(self, amount: int):
        self.amount: int = amount
    
    def process_payment(self):
        pass

class CreditCard(PaymentBase):
    def process_payment(self):
        msg = f"Credit card payment: {self.amount}"
        print(msg)

class PayPal(PaymentBase):
    def process_payment(self):
        msg = f"PayPal payment: {self.amount}"
        print(msg)

# Uso del polimorfismo
if __name__ == "__main__":
    payments = [CreditCard(100), PayPal(200)]
    for payment in payments:
        payment.process_payment()
python encapsulate.py
Credit card payment: 100
PayPal payment: 200

Encapsulación con @property

Python ofrece el decorador @property para encapsular atributos con validación:

circle.py
class Circle:
    def __init__(self, radius: int):
        self._radius: int = radius
    
    @property
    def radius(self):
        return self._radius
    
    @radius.setter
    def radius(self, value: int):
        if value < 0:
            raise ValueError("Radius cannot be negative!")
        self._radius = value

# Uso
circle = Circle(10)
print(f"Initial radius: {circle.radius}")
circle.radius = 15
print(f"New radius: {circle.radius}")
El atributo real es _radius (protegido), pero la interfaz pública es radius. Esto permite modificar la implementación interna sin afectar el código cliente.
PRINCIPIO_02

Favorecer Composición sobre Herencia

En POO, es tentador crear jerarquías complejas de clases mediante herencia. Sin embargo, esto puede llevar a código fuertemente acoplado que es difícil de mantener. Este principio aconseja componer objetos a partir de partes más simples en lugar de heredar funcionalidades.

Beneficios

FLEXIBILIDAD
Cambiar comportamiento en tiempo de ejecución
REUSABILIDAD
Objetos simples reutilizables en toda la aplicación
MANTENIMIENTO
Actualizar componentes sin efectos secundarios
Herencia vs Composición
Car
—— has-a ——
Engine

Componer un Auto con Motor

composition.py
class Engine:
    def start(self):
        print("Engine started")

class Car:
    def __init__(self):
        self.engine = Engine()  # Composición: "has-a"
    
    def start(self):
        self.engine.start()
        print("Car started")

# Uso
if __name__ == "__main__":
    my_car = Car()
    my_car.start()
python composition.py
Engine started
Car started
La clase Car tiene un Engine (composición), no es un Engine (herencia). Esto permite cambiar fácilmente el tipo de motor sin alterar la clase Car.
PRINCIPIO_03

Programar hacia Interfaces

Este principio te anima a codificar contra una interfaz en lugar de una clase concreta. Al hacerlo, desacoplas tu código de las clases específicas que proporcionan el comportamiento requerido.

Beneficios

FLEXIBILIDAD
Intercambiar implementaciones fácilmente
TESTABILIDAD
Facilita la creación de mocks para pruebas
MANTENIBILIDAD
Actualizar componentes sin afectar el sistema

Técnica 1: Abstract Base Classes

abstractclass.py
from abc import ABC, abstractmethod

class MyInterface(ABC):
    @abstractmethod
    def do_something(self, param: str):
        pass

class MyClass(MyInterface):
    def do_something(self, param: str):
        print(f"Doing something with: '{param}'")

# Uso
MyClass().do_something("some param")

Técnica 2: Protocols

Los Protocols ofrecen structural duck typing: un objeto es válido si tiene ciertos métodos, sin importar su herencia:

Duck Typing Estructural
Protocol: Flyer
——
def fly() -> None
protocols.py
from typing import Protocol

class Flyer(Protocol):
    def fly(self) -> None:
        ...

# Cualquier clase con fly() satisface el Protocol
class Bird:
    def fly(self) -> None:
        print("Bird is flying")

class Airplane:
    def fly(self) -> None:
        print("Airplane is flying")

def take_off(flyer: Flyer) -> None:
    flyer.fly()

# Ambos funcionan sin heredar de Flyer
take_off(Bird())
take_off(Airplane())
"Si camina como pato y grazna como pato, es un pato" — Los Protocols se enfocan en lo que un objeto puede hacer, no en lo que es.
PRINCIPIO_04

Acoplamiento Débil

El acoplamiento se refiere al grado de dependencia entre componentes. El acoplamiento débil significa que los cambios en un componente tienen un impacto mínimo en otros, haciendo el código más fácil de refactorizar y probar.

Beneficios

MODULARIDAD
Componentes independientes y reemplazables
TESTABILIDAD
Pruebas unitarias más simples
ESCALABILIDAD
Facilita agregar nuevas funcionalidades
Inyección de Dependencias
MessageService
← inject →
EmailSender
SMSSender

Servicio de Mensajería

loose_coupling.py
class MessageService:
    def __init__(self, sender):
        self.sender = sender  # Inyección de dependencia
    
    def send_message(self, message: str):
        self.sender.send(message)

class EmailSender:
    def send(self, message: str):
        print(f"Sending email: {message}")

class SMSSender:
    def send(self, message: str):
        print(f"Sending SMS: {message}")

# Uso con inyección de dependencias
if __name__ == "__main__":
    email_service = MessageService(EmailSender())
    email_service.send_message("Hello via Email")
    
    sms_service = MessageService(SMSSender())
    sms_service.send_message("Hello via SMS")
python loose_coupling.py
Sending email: Hello via Email
Sending SMS: Hello via SMS
MessageService está débilmente acoplado con los senders mediante inyección de dependencias. Puedes cambiar el mecanismo de envío sin modificar la clase MessageService.

Resumen de Principios

01 // Encapsular lo que Varía

Aísla las partes del código propensas a cambiar para proteger el resto del sistema.

02 // Composición sobre Herencia

Construye objetos complejos combinando objetos simples en lugar de crear jerarquías.

03 // Programar hacia Interfaces

Codifica contra abstracciones, no implementaciones concretas.

04 // Acoplamiento Débil

Reduce las dependencias entre componentes usando inyección de dependencias.

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