Principios S.O.L.I.D. - 5. Principio de inversión de la dependencia. (DIP - Dependency inversion principle)

Posted on sáb 14 mayo 2022 in Tutorial Python • 6 min read

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. * Principio abierto/cerrado. * Principio de sustitución Liskov. * [Principio de segregación de la interfaz](https://www.seraph.to/python_solid4.html#python_solid4.

El Principio de inversión de la dependencia es una forma específica de desacoplar módulos de software. Al seguir el principio, las relaciones de dependencia convencionales establecidas desde los módulos de alto nivel de establecimiento de políticas a los módulos de depencia de bajo nivel se invierten, lo que hace que los módulos de alto nivel sean independientes de los detalles de implementación del módulo de bajo nivel, el principio establece:

  1. Los módulos de alto nivel no deberían depender de los módulos de bajo nivel. Ambos deberían depender de abstracciones (p.ej., interfaces).
  2. Las abstracciones no deberían depender de los detalles. Los detalles (implementaciones concretas) deben depender de abstracciones.

La dependencia debe estar en abstracciones, no en concreciones. Los módulos de alto nivel no deben depender de módulos de bajo nivel. Tanto las clases de nivel bajo como las de alto nivel deberían depender de las mismas abstracciones. Las abstracciones no deberían depender de los detalles. Los detalles deben depender de abstracciones.

  1. Se tiene el código del artículo anterior sobre el Principio de segregación de interfaz. Con solución usando composición.

En este caso, al pasar como argumento de un método un objeto, ya viola este principio.

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

Solución

Para cumplir el principio de la inversión de la dependencia se crea una clase abstracta que maneje si se verifica la autenticación junto con la clase AuthSMS, en las respectivas clases del procesamiento de pago que se necesite verificar por SMS, en vez de pasar como argumento el objeto, se pasa la clase abstracta.

A continuación el código:

from abc import ABC, abstractmethod

# La clase Order 
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

# Clase abstracta authorizer con método que devuelve si está autorizado o no. 
class Authorizer(ABC):
    @abstractmethod
    def is_authorized(self) ->bool:
        pass


# Clase abstracta de procesamiento de pago
class PaymentProcessor(ABC):
    @abstractmethod
    def pay(self,order):
        pass 

# Clase SMSAutho que hereda de la clase abstracta authorizer
class SMSAuth(Authorizer):
    authorized = False

    def verify_code(self,code):
        print(f"Verifying code {code}")
        self.authorized = True

    def is_authorized(self) -> bool:
        return self.authorized

# clase pago por tarjeta de debito que recibe en el init authorizer que es la clase abstracta.
class DebitPaymentProcessor(PaymentProcessor):

    def __init__(self,security_code, authorizer: Authorizer):

        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"

# La clase de procesamiento de TC se mantiene igual.
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 dde procesamiento de pago por paypal
# Se le pasa como argumento la clase abstracta.
class PaypalPaymentProcessor(PaymentProcessor):

    def __init__(self,email_address,authorizer:Authorizer):
        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 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 que se logro es ocultar la implementación de la composición que se pasa como argumento en los procesadores de pago que tiene que validar vía SMS.

En próximo artículo

Referencias:


¡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:

17MtNybhdkA9GV3UNS6BTwPcuhjXoPrSzV