En ingeniería de software existe el principio S.O.L.I.D. Los principios SOLID son guías que pueden ser aplicadas en el desarrollo de software para eliminar malos diseños provocando que el programador tenga que refactorizar hasta que sea legible y extensible.
Sus principios son:
- Single responsability principle - Principio de responsabilidad única.
- Open/closed principle - Principio abierto/cerrado.
- Liskov substitution principle - Principio de sustitución Liskov.
- Interface segregation principle - Principio de segregación de la interfaz.
- Dependency inversion principle - Principio de inversión de la dependencia.
A continuación dejo un vídeo de ArjanCodes que explica con código python los principios S.O.L.I.D:
Los artículos anteriores:
- Principio de responsabilidad única (enlace roto).
- Principio abierto/cerrado (enlace roto).
- Principio de sustitución Liskov (enlace roto).
El Principio de segregación de la interfaz establece que los clientes de un programa dado solo deberían conocer de este aquellos métodos que realmente usan y no aquellos que no necesitan usar. El ISP se aplica a una interfaz amplia y compleja para escindirla en otras más pequeñas y específicas, de tal forma que cada cliente use solo aquella que necesite, pudiendo así ignorar al resto. A este tipo de interfaz reducida se le llama interfaces de rol.
El Principio de Segregación de Interfaz establece que los clientes no deberían ser forzados a depender de métodos que no utilizan y, por tanto, sugiere la creación de interfaces o clases específicas para dichos clientes.
- Del principio de sustitución de Livkov se tiene el códgio de procesamiento de pagos. A continuación el código.
from abc import ABC, abstractmethod # La clase Order se mantiene igual así que en el siguiente código no se mostrará. class Order: def __init__(self): self.items = [] self.quantities = [] self.prices = [] self.status = "open" def add_item(self, name, quantity, price): self.items.append(name) self.quantities.append(quantity) self.prices.append(price) def total_price(self): total = 0 for i in range(len(self.prices)): total += self.quantities[i] * self.prices[i] return total # La clase abstracta con el método de pago que ya no maneja el código de seguridad. class PaymentProcessor(ABC): @abstractmethod def pay(self,order): pass # Se define las clases de pago a debito y crédito y se le define el código de seguridad. class DebitPaymentProcessor(PaymentProcessor): def __init__(self,security_code): self.security_code = security_code def pay(self,order): print("Processing debit payment type") print(f"Verifying security code: {self.security_code}") order.status = "paid" class CreditPaymentProcessor(PaymentProcessor): def __init__(self,security_code): self.security_code = security_code def pay(self,order): print("Processing credit payment type") print(f"Verifying security code: {self.security_code}") order.status = "paid" # Para el caso de paypal se define el correo electrónico. class PaypalPaymentProcessor(PaymentProcessor): def __init__(self,email_address): self.email_address = email_address def pay(self,order): print("Processing paypal payment type") print(f"Verifying email address: {self.email_address}") order.status = "paid" # Ahora se instancia la clase Order, y se agrega los items a comprar en la orden. order = Order() order.add_item("Teclado", 1, 50) order.add_item("Memoria", 1, 150) order.add_item("Cable USB", 2, 5) # Imprime el precio total de la orden print(order.total_price()) # Se define el método de pago debito processor = DebitPaymentProcessor("0372846") processor.pay(order) # Se define el método de pago TC processor = CreditPaymentProcessor("0372846") processor.pay(order) # Se define el método de pago paypal processor = PaypalPaymentProcessor("h@h.com") processor.pay(order)
La salida genera lo siguiente:
210 Processing debit payment type Verifying security code: 0372846 Processing debit payment type Verifying security code: 0372846 Processing credit payment type Verifying security code: 0372846 Processing paypal payment type Verifying email address: h@h.com
Primera mejora
Para cumplir con el método de segregación la mejora que se hará es en la clase abstracta, en vez de tener un sólo método (pago), ahora tendrá uno de autenticación vía SMS.
A continuación el código:
from abc import ABC, abstractmethod # La clase abstracta de procesamiento de pago # tiene los métodos pay y auth_sms class PaymentProcessor(ABC): @abstractmethod def pay(self,order): pass @abstractmethod def auth_sms(self,code): pass # La clase DebitPaymentProcessor hereda de la clase abstracta. class DebitPaymentProcessor(PaymentProcessor): def __init__(self,security_code): self.security_code = security_code self.verified = false def pay(self,order): if not self.verified: raise Exception("Not authorized") print("Processing debit payment type") print(f"Verifying security code: {self.security_code}") order.status = "paid" def auth_sms(self,code): print(f"Verifying SMS code {code}") self.verified = True # La clase CreditPaymentProcessor hereda de la clase abstracta # El problema acá es que pago con TC no necesita enviar un SMS de autenticación # pero toca usarlo por la clase abstracta, al enviar un mensaje de error, pero # al hacer esto se está violando el principio de sustitución de Liskov. class CreditPaymentProcessor(PaymentProcessor): def __init__(self,security_code): self.security_code = security_code def pay(self,order): print("Processing credit payment type") print(f"Verifying security code: {self.security_code}") order.status = "paid" # Esta es una violacion del principio de sustitución de Liskov def auth_sms(self,code): raise Exception("Credit card payments don't support SMS code authorizations") # La clase de procesamiento de pago por paypal hereda de la clase abstracta. class PaypalPaymentProcessor(PaymentProcessor): def __init__(self,email_address): self.email_address = email_address self.verified = False def pay(self,order): if not self.verified: raise Exception("Not authorized") print("Processing paypal payment type") print(f"Verifying email address: {self.email_address}") order.status = "paid" def auth_sms(self,code): print(f"Verifying SMS code {code}") self.verified = True
Segunda mejora
Para resolver el incumplimiento del principio de sustitución de Liskov se va a crear 2 clases abstractas, una para autenticación vía SMS y otra para validar lo que no usan SMS.
# Clase abstracta procesador de pago que ahora sólo tiene el método pay class PaymentProcessor(ABC): @abstractmethod def pay(self,order): pass # Ahora se tiene una clase abstracta de procesamiento de pago SMS que hereda de # la clase anterior. # Con sólo el método de auth_sms por que ya el de pago lo hereda de la clase abstracta anterior. class PaymentProcessor_SMS(PaymentProcessor): @abstractmethod def auth_sms(self,code): pass # Clase procesamiento de tarjeta de debito que hereda de la # clase abstracta de pago SMS. class DebitPaymentProcessor(PaymentProcessor_SMS): def __init__(self,security_code): self.security_code = security_code self.verified = false def pay(self,order): if not self.verified: raise Exception("Not authorized") print("Processing debit payment type") print(f"Verifying security code: {self.security_code}") order.status = "paid" def auth_sms(self,code): print(f"Verifying SMS code {code}") self.verified = True # Clase de procesamiento de TC que hereda de la clase raíz. class CreditPaymentProcessor(PaymentProcessor): def __init__(self,security_code): self.security_code = security_code def pay(self,order): print("Processing credit payment type") print(f"Verifying security code: {self.security_code}") order.status = "paid" # Clase de procesamiento vía paypal que hereda de la Clase abstracta # que soporta SMS class PaypalPaymentProcessor(PaymentProcessor_SMS): def __init__(self,email_address): self.email_address = email_address self.verified = False def pay(self,order): if not self.verified: raise Exception("Not authorized") print("Processing paypal payment type") print(f"Verifying email address: {self.email_address}") order.status = "paid" def auth_sms(self,code): print(f"Verifying SMS code {code}") self.verified = True
Ya se tiene las diferentes clases con sólo los métodos que necesitan bien separados cumpliendo con el principio de segregación.
Existe otra solución y es usando commposición, se creará la clase SMSAuth que tendrá el método de verificación de código SMS y si está autorizado. La clase abstracta de procesamiento de pago sólo tendrá el método pay,
class SMSAuth: authorized = False def verify_code(self,code): print(f"Verifying code {code}") self.authorized = True def is_authorized(self) -> bool: return self.authorized # Clase abstracta procesamiento de pago con método pay. class PaymentProcessor(ABC): @abstractmethod def pay(self,order): pass # Clase procesamiento pago por tarjeta de debito que hereda de la clase abstracta. # el método init recibe como argumentos el código de seguridad y la clase SMSAuth para autorizar el pago. class DebitPaymentProcessor(PaymentProcessor): def __init__(self,security_code, authorizer: SMSAuth): self.authorizer = authorizer self.security_code = security_code def pay(self,order): # Se consulta si no está autorizado el pago # al no tener autorización devuelve una excepción. if not self.authorizer.is_authorized(): raise Exception("Not authorized") print("Processing debit payment type") print(f"Verifying security code: {self.security_code}") order.status = "paid" # Clase de procesamiento de pago con TC, que hereda de la clase abstracta. # En este caso no se necesita la clase SMSAuth, sólo el código de seguridad. class CreditPaymentProcessor(PaymentProcessor): def __init__(self,security_code): self.security_code = security_code def pay(self,order): print("Processing credit payment type") print(f"Verifying security code: {self.security_code}") order.status = "paid" # Clase de procesamiento de pago vía paypal, hereda de la misma clase abstracta. # El init recibe de argumentos dirección de correo y el objecto SMSAuth. class PaypalPaymentProcessor(PaymentProcessor): def __init__(self,email_address,authorizer:SMSAuth): self.authorizer = authorizer self.email_address = email_address self.verified = False def pay(self,order): # Se valida si se tiene autorización para realizar el pago. if not self.authorizer.is_authorized(): raise Exception("Not authorized") print("Processing paypal payment type") print(f"Verifying email address: {self.email_address}") order.status = "paid" # Ahora se instancia la clase Order, y se agrega los items a comprar en la orden. order = Order() order.add_item("Teclado", 1, 50) order.add_item("Memoria", 1, 150) order.add_item("Cable USB", 2, 5) # Imprime el precio total de la orden print(order.total_price()) # Se define el autorizador. authorizer = SMSAuth() # Se define el método de pago debito # ahora se le pasa el código y el autorizador. processor = DebitPaymentProcessor("0372846",authorizer) # Se verifica el pago authorizer.verify_code(454545) # Realiza el pago de la orden. processor.pay(order) # Se define el método de pago TC # Para este caso se mantiene igual. processor = CreditPaymentProcessor("0372846") processor.pay(order) # Se define el método de pago paypal # Se le pasa el correo y el autorizador. processor = PaypalPaymentProcessor("h@h.com",authorizer) # Se realiza el pago de la orden. processor.pay(order)
La salida de está ejecución es la siguiente:
210 Verifying code 454545 Processing debit payment type Verifying security code: 0372846 Processing debit payment type Verifying security code: 0372846 Processing credit payment type Verifying security code: 0372846 Processing paypal payment type Verifying email address: h@h.com
Lo bueno de la composición es que se elimina la creación de una clase abstracta adicional.
Referencias:
-
Los principios SOLID ilustrados en ejemplos sencillos de Python
-
The 5 (SOLID) Principles of Pyhon Readable Code (enlace roto)
-
How to Write Clean Code (in Python) With SOLID Principles | Principle #4 (enlace roto)
-
The Interface Segregation Principle (ISP) Explained in Python (enlace roto)
¡Haz tu donativo! Si te gustó el artículo puedes realizar un donativo con Bitcoin (BTC) usando la billetera digital de tu preferencia a la siguiente dirección: 17MtNybhdkA9GV3UNS6BTwPcuhjXoPrSzV
O Escaneando el código QR desde la billetera:
