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.
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
Sistema de Pagos con Polimorfismo
Veamos cómo encapsular la lógica de procesamiento de pagos, donde el método de pago puede variar:
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()
PayPal payment: 200
Encapsulación con @property
Python ofrece el decorador @property para encapsular atributos con validación:
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}")
_radius (protegido), pero la interfaz pública es radius. Esto permite modificar la implementación interna sin afectar el código cliente.
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
Componer un Auto con Motor
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()
Car started
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.
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
Técnica 1: Abstract Base Classes
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:
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())
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
Servicio de Mensajería
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")
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.