In software engineering there is the S.O.L.I.D principle. The SOLID principles are guidelines that can be applied in software development to eliminate bad designs that force the programmer to refactor until the code is readable and extensible.

Its principles are:

  • Single responsability principle - Single responsibility principle.
  • Open/closed principle - Open/closed principle.
  • Liskov substitution principle - Liskov substitution principle.
  • Interface segregation principle - Interface segregation principle.
  • Dependency inversion principle - Dependency inversion principle.

Below I leave a video by ArjanCodes that explains the S.O.L.I.D principles with python code:

The previous articles:

The Liskov substitution principle is defined as: Every class that inherits from another can be used as its parent without needing to know the differences between them.

The main idea behind the Liskov substitution principle is that, for any class, a client should be able to use any of its subtypes indistinguishably, without even noticing, and therefore without compromising the expected behavior at runtime. This means that clients are completely isolated from, and unaware of, changes in the class hierarchy.

  1. From the open/closed principle, an abstract class was created and different classes that inherit from that abstract class were created for each payment process. The code is shown below.

from abc import ABC, abstractmethod

# La clase Order se mantiene igual:

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


# Se crea una clase abstracta del proceso de pago:
class PaymentProcessor(ABC):
    @abstractmethod
    def pay(self,order,security_code):
        pass 


# Se crea la clase de pago con debito que hereda de la clase abstracta
class DebitPaymentProcessor(PaymentProcessor):
    def pay(self,order,security_code):
        print("Processing debit payment type")
        print(f"Verifying security code: {security_code}")
        order.status = "paid"

# Se crea la clase de pago con TC que hereda de la clase abstracta
class CreditPaymentProcessor(PaymentProcessor):
    def pay(self,order,security_code):
        print("Processing credit payment type")
        print(f"Verifying security code: {security_code}")
        order.status = "paid"


# Se crea el método de pago paypal
class PaypalPaymentProcessor(PaymentProcessor):
    def pay(self,order,security_code):
        print("Processing paypal payment type")
        print(f"Verifying security code: {security_code}")
        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()
processor.pay(order, "0372846")

# Se define el método de pago TC

processor = CreditPaymentProcessor()
processor.pay(order, "0372846")



# Se define el método de pago paypal
processor = PaypalPaymentProcessor()
processor.pay(order, "0372846")

The output is the following:


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 security code: 0372846

There is an error in how the paypal payment process class was created. Paypal does not need a security code, it needs an email address, but modifying the payment method of the paypal class would violate the Liskov substitution principle.

To solve it, the security code argument is removed and it is set when initializing the respective payment method.

The code is shown below:


from abc import ABC, abstractmethod

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)

The output produces the following:


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
  1. QR code generation.

From the article on the open/closed principle we have the following code:


from abc import ABC, abstractmethod
import qrcode as qrc
from MyQR import myqr

# Clase abstracta
class GenerateQR(ABC):
    @abstractmethod
    def generate(self,my_qr,text,save_name,**kwargs):
        pass 


# Clase para la librería MyQR que hereda de la clase abstracta.
class GenerateMyQR(GenerateQR):

    def __init__(self, version):
        self.version = version

    def generate(self,my_qr,text,save_name,**kwargs):
        # colorize,save_dir,picture=None
        colorize = kwargs.get("colorize", True)
        save_dir = kwargs.get("save_dir","./")
        picture = kwargs.get("picture",None)
        if not picture: 
            return myqr.run(words=text, version=self.version, save_name=save_name,
                            save_dir=save_dir, colorized=colorize, contrast=1.0, brightness=1.0)

        return myqr.run(words=text, version=my_qr.version, save_name=save_name, picture=picture,
                        save_dir=save_dir, colorized=colorize, contrast=1.0, brightness=1.0)

# Clase de la librería qr_code que hereda de la clase abstracta.
class GenerateQRCode(GenerateQR):

    def __init__(self, version):
        self.version = version

    def generate(self,my_qr,text,save_name,**kwargs):
        # box_size,border,fit,fill,back_color
        box_size = kwargs.get("box_size",10)
        border = kwargs.get("border",5)
        fit = kwargs.get("fit",True)
        fill = kwargs.get("fill","black")
        back_color = kwargs.get("back_color","white")
        self.qr = qrc.QRCode(
            version=self.version,
            box_size=box_size,
            border=border
        )
        self.qr.add_data(text)
        self.qr.make(fit=fit)
        self.img = self.qr.make_image(
            fill=fill, back_color=back_color)
        self.img.save(save_name)

# Se crea la instancia de la clase pasandole la versión del código QR que se quiere usar 
# para la librería MyQR y luego para qr_code
gen_qr = GenerateMyQR(version=1)
gen_qr.generate(my_qr,"Hola mundo","hola1c.png")

gen_qrcode = GenerateQRCode()
gen_qrcode.generate(my_qr,"Hola mundo","hola2c.png")

The change that will allow compliance with this principle is to initialize the classes with the kwargs instead of passing them to the generate method.


from abc import ABC, abstractmethod
import qrcode as qrc
from MyQR import myqr

class GenerateQR(ABC):
    @abstractmethod
    def generate(self,text):
        pass 

class GenerateMyQR(GenerateQR):

    def __init__(self,version,**kwargs):
        self.version = version
        self.colorize = kwargs.get("colorize", True)
        self.save_dir = kwargs.get("save_dir","./")
        self.picture = kwargs.get("picture",None)
        self.save_name = kwargs.get("save_name","hola1e.png")

    def generate(self,text):

        if not self.picture: 
            return myqr.run(words=text, version=my_qr.version, save_name=self.save_name,
                            save_dir=self.save_dir, colorized=self.colorize, contrast=1.0, brightness=1.0)

        return myqr.run(words=text, version=self.version, save_name=self.save_name, picture=self.picture,
                        save_dir=self.save_dir, colorized=self.colorize, contrast=1.0, brightness=1.0)


class GenerateQRCode(GenerateQR):

    def __init__(self,version,**kwargs):
        self.version = version
        self.save_name = kwargs.get("save_name","hola2e.png")
        self.box_size = kwargs.get("box_size",10)
        self.border = kwargs.get("border",5)
        self.fit = kwargs.get("fit",True)
        self.fill = kwargs.get("fill","black")
        self.back_color = kwargs.get("back_color","white")

    def generate(self,text,):
        # box_size,border,fit,fill,back_color

        self.qr = qrc.QRCode(
            version=self.version,
            box_size=self.box_size,
            border=self.border
        )
        self.qr.add_data(text)
        self.qr.make(fit=self.fit)
        self.img = self.qr.make_image(
            fill=self.fill, back_color=self.back_color)
        self.img.save(self.save_name)


gen_qr = GenerateMyQR(version=1,{"save_name":"hola2.png"})
gen_qr.generate("Hola mundo")

gen_qr = GenerateQRCode(version=1,{"save_name":"hola2.png"})
gen_qr.generate("Hola mundo")

References:


Make a donation! If you liked the article you can make a donation with Bitcoin (BTC) using the digital wallet of your choice to the following address: 17MtNybhdkA9GV3UNS6BTwPcuhjXoPrSzV

Or by scanning the QR code from your wallet:

17MtNybhdkA9GV3UNS6BTwPcuhjXoPrSzV