python_introduction/introduction_to_solid.md

9.6 KiB

Object-Oriented class design

SOLID

SOLID coding is a principle created by Robert C.Martin, he is a famous computer scientist. SOLID is an acronym for his five conventions of coding. With their conventions, you can improve the structure of your code, reduce time to implement changes and technical debts, etc. It is a collection of best practices. And it was developed through this decade. Principles of SOLID acronym are:

  • The Single-Responsibility Principle (SRP)
  • The Open-Closed Principle (OCP)
  • The Liskov Substitution Principle (LSP)
  • The Interface Segregation Principle (ISP)
  • The Dependency inversion Principle (DIP)

The first convention is SRP, that means that all classes of your code must do one thing. That is an important principle. That is the best way to work with others people in the same project. Version control is easier, You will never have Merge conflicts, because other people work in other operations. So, he will never have two same things in the code.

Single-Responsibility

Let's start something ! We will make common mistake that violate SRP and correct them. Let's code a bookstore invoice.

class Book(object):
    def __init__(self, name, authorName, year, price, isbn):
        self.name = name
        self.authorName = authorName
        self.year = year
        self.price = price
        self.isbn = isbn

As you can see, there is a class named Book with some fields. This fields are public and characterize a book.

OK ! Now we can start the invoice class. This class will calculate the final price for a customer.

class Invoice(object):
    def __init__(self, book, quantity, discountRate, taxRate, total):
        self.book = book
        self.quantity = quantity
        self.discountRate = discountRate
        self.taxRate = taxRate
        self.total = total


    def calculateTotal(self):
        self.price = ((self.book.price - self.book.price * self.discountRate)*self.quantity)

        self.priceWithTaxes = self.price * (1 + self.taxRate)

        return self.priceWithTaxes

    def printInvoice(self):
        print(self.quantity, "x", self.book.name,"", self.book.price, "$");
        print("Discount Rate: ", self.discountRate)
        print("Tax Rate: ", self.taxRate)
        print("Total: ", self.total)


    def saveToFile(self, fileName):
        pass

Alright, now we have the Invoice class, he had 3 methods (calculateTotal, printInvoice, saveToFile) and some fields too. Why this code violate the first convention of SOLID ?

The printInvoice method violate this one because the SRP told us to make just one thing per classes. Here, our printing logic is in the same class than calculateTotal method. So, the printing logic is mixed with business logic in the same class. As you think, the saveToFile method violate this convention too.

Let's correct this example.

class InvoicePrinter(object):
    def __init__(self, invoice):
        self.invoice = invoice
    def printInvoice(self):
        print(self.invoice.quantity, "x", self.invoice.book.name,"", self.invoice.book.price, "$");
        print("Discount Rate: ", self.invoice.discountRate)
        print("Tax Rate: ", self.invoice.taxRate)
        print("Total: ", self.invoice.total)
class InvoicePersistence(object):
    def __init__(self, invoice):
        self.invoice = invoice

    def saveToFile(self):
        pass

We have now two others classes, InvoicePrinter class and InvoicePersistence class. The InvoicePrinter is used to print information. And the InvoicePersistence is used to save information.

With these three classes, we respect the first principle of SOLID.

Open-Closed Principle

This principle says that classes are open for extension and closed to modification. Extension mean news functionalities and modification mean modifying your code. If you want to add new functionalities, you are able to add it without manipulating the existing program. If you touch the existing code, you have a risk to have news bugs. So, if you want to add something else, you can use abstract classes and help of interface.

Ok so, let's add new functionality in the InvoicePersistence class.

class InvoicePersistence(object):
    def __init__(self, invoice):
        self.invoice = invoice

    def saveToFile(self):
        pass

    def saveToDataBase(self):
        pass

The saveToDataBase method is used to save information in a Data Base. We have modified the InvoicePersistence class. And this class will be more difficult to make easily extendable. So, we violate the OCP convention. If you want to respect this principle, you have to create a new class.

class InvoicePersistence(abc.ABC):
    @abstractmethod
    def save(self, invoice):
        pass

The new InvoicePersistence class has an abstract method. So, if a class inherits the InvoicePersistence class, you have to implement the save method.

And for example, we will create a DataBasePersistence class, and this class inherits the abstract InvoicePersistence class.

class DatabasePersistence(InvoicePersistence):
    def __init__(self, invoice):
        self.invoice = invoice
        self.save(self.invoice)

    def save(self, invoice):
        print("Save in database ...")

Let's do the same thing with FilePersistence class.

class FilePersistence(InvoicePersistence):
    def __init__(self, invoice):
        self.invoice = invoice
        self.save(self.invoice)

    def save(self, invoice):
        print("Save to file ...")

Ok, we do a lot of things, let's make a UML to represent our class structure.

That will be more simple to do extension.

Liskov Substitution Principle

This convention tells us to create a substitutable subclass for their parents. So, if you want to create an object in the subclass, you have to be able to pass it in the interface.

Let's make an example ! We will write a Walk class and his subclass, Jump class.

class Walk(abc.ABC):
    def __init__(self):
        abc.ABC.__init__(self)
    @abc.abstractmethod
    def Speed(self):
        pass
    @abc.abstractmethod
    def Ahead(self):
        pass
        '''the player walk ahead'''

    @abc.abstractmethod
    def Behind(self):
        pass
    '''the player walk behind'''

And the Jump subclass.

class Jump(Walk):
    def __init__(self):
        pass

    def Speed(self):
        pass

    def Ahead(self):
        pass

    def Behind(self):
        pass

As you can see, the Jump subclass has all abstract method of Walk class. But we have a big problem ! The Jump subclass need a new method, the height method. And he does not need the Ahead and Behind method. If you remove abstract method and add a new method in Jump subclass, you will be in a big trouble. This subclass will be different about mother class. The simple way to resolve this trouble is to create a new mother class for Walk and Jump class.

class Movement(abc.ABC):
    def __init__(self):
        abc.ABC.__init__(self)
        
    @abc.abstractmethod
    def Speed(self):
        pass
    
    @abc.abstractmethod
    def Animation(self):
        pass
    
    @abc.abstractmethod
    def Direction(self):
        pass

The Movement class will be the mother class of Walk and Jump classes. These subclasses will have three methods (Speed, Animation and Direction methods). The problems that we had is resolved with the Direction method. With this method, you have choices to go ahead, behind, height, etc.

Subclasses The _Walk_ subclass:
class Walk(Movement):
    def __init__(self):
        pass

    def Speed(self):
        pass

    def Animation(self):
        pass


    def Direction(self):
        pass

And the Jump subclass:

class Jump(Movement):
    def __init__(self):
        pass
    def Speed(self):
        pass

    def Animation(self):
        pass

    def Direction(self):
        pass

Interface Segregation Principle

The Interface Segregation Principle means that all interfaces have to be separated. That means that clients has not functions that they do not need.

OK ! So, let's make an example with client manager.

class GoodCustomer(abc.ABC):
    def __init__(self):
        abc.ABC.__init__(self)
        
    @abc.abstractmethod
    def FirstName(self):
        pass
    @abc.abstractmethod
    def LastName(self):
        pass
    @abc.abstractmethod
    def Address(self):
        pass
    
    @abc.abstractmethod
    def Work(self):
        pass

The GoodCustomer mother class is a class where good customer information are saved.

class BadCustomer(GoodCustomer):
    def __init__(self):
        pass

    def FirstName(self):
        pass

    def LastName(self):
        pass

    def Address(self):
        pass

    def Work(self):
        pass

The BadCustomer subclass is a class where bad customer information are saved. For this example, we don't want to know the addresses of bad guys. So what can we do ?

You have to create a new mother class for these two classes like the preceding example.

Dependency Inversion Principle

This convention says that all classes that we use depends on the interface or abstract classes. Like precedents example, all methods was abstracts.

The Dependency Inversion principle states that our classes should depend upon interfaces or abstract classes instead of concrete classes and functions. We want our classes to be open to extension, so we have reorganized our dependencies to depend on interfaces instead of concrete classes.