Solid Principles (Python)

In all the below examples they are not complete but basically show you in principle how to break a specified principle and what you should have done.

Single Responsibility Principle

The Single Responsibility Principle (SRP) basically means that a class should have one and only one responsibility, it should be encapsulated within the class, the class should not be a GOD object.

Breaking the SRP principle
#Below is Given a class which has two responsibilities 
class  User:
    def __init__(self, name: str):
        self.name = name
    
    def get_name(self) -> str:
        pass

    def save(self, user: User):
        pass
Following the SRP rule
# Break the two responsibilities into two classes
class User:
    def __init__(self, name: str):
            self.name = name
    
    def get_name(self):
        pass


class UserDB:
    def get_user(self, id) -> User:
        pass

    def save(self, user: User):
        pass

Open-Closed Principle

The Open-Closed Principle (OCP) should be open for extension (extending) but closed for modification, the reason for this is that if a class has been tested and you know it works don't change it. This class could be at any clients location or in a library and thus making changes could cause issues. It uses interfaces instead of superclasses to allow different implementations which you can easily substitute without changing the code that uses them.

Breaking the OCP rule
class Discount:
  def __init__(self, customer, price):
      self.customer = customer
      self.price = price
  def give_discount(self): 				## This was added at a later date
      if self.customer == 'fav':
          return self.price * 0.2
      if self.customer == 'vip':
          return self.price * 0.4
Following the OCP rule
class Discount:
    def __init__(self, customer, price):
      self.customer = customer
      self.price = price
    def get_discount(self):
      return self.price * 0.2
	  
class VIPDiscount(Discount): 				## we extend the original Discount class and add the additional features
    def get_discount(self):
      return super().get_discount() * 2

Liskov Subsitution Principle

The Liskov Subsitution Principle (LSP) is that objects of a superclass shall be replaceable with objects of its subclasses without breaking the application. That requires the objects of your subclasses to behave in the same way as the objects of your superclass. So any functions that use references to base classes must be able to use objects of the derived class without knowing it, for example you inherit a class method that starts to break things or behaves oddly.

Breaking the LSP rule
class Rectangle:

    def __init__(self, width=0, height=0):
        self.width = width
        self.height = height

    def setwidth(self, width):
        self.width = width

    def setheight(self, height):
        self.height = height

    def calculate(self):
        print("Rectangle width: " + str(self.width) + " height: " + str(self.height))
        return self.width * self.height


class Square(Rectangle):

    def __init__(self, size):
        super().__init__(size, size)

    def setwidth(self, width):
        super().setwidth(width)

    def setheight(self, height):
        super().setheight(height)


rec = Rectangle(2, 3)
rec.setheight(10)
print(str(rec.calculate()))

sq = Square(5)          		# this works
print(str(sq.calculate()))

sq.setheight(10)        		# we only need to set either height/width, so should be 10 (width) * 10 (height)
print(str(sq.calculate()))
Following the LSP rule
class Rectangle:

    def __init__(self, width=0, height=0):
        self.width = width
        self.height = height

    def setwidth(self, width):
        self.width = width

    def setheight(self, height):
        self.height = height

    def calculate(self):
        print("Rectangle width: " + str(self.width) + " height: " + str(self.height))
        #print(type(self).__name__) 							# we could get the class name and if square set both height and width
        return self.width * self.height


class Square(Rectangle):

    def __init__(self, size):
        super().__init__(size, size)

    def setwidth(self, width):
        super().setwidth(width)
        super().setheight(width) 								# we could set both height and width here

    def setheight(self, height):
        super().setheight(height)
        super().setwidth(height) 								# we could set both height and width here


rec = Rectangle(2, 3)
rec.setheight(10)
print(str(rec.calculate()))

sq = Square(5)          		# this works
print(str(sq.calculate()))

sq.setheight(10)        		# this now works
print(str(sq.calculate()))

Interface Segregation Principle

The Interface Segregation Principle (ISP) states that no client should be forced to depend on methods it does not use, you should split interfaces that are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them. There is a design pattern that you can also use here called the decorator pattern which I will cover in another section.

Breaking the ISP rule
## This class methods that may or may not be used if extended
class Machine:
    def print(self):
	pass
    def fax(self):
	pass
    def scan(self):
	pass

class OldPrinter(Machine):
    def print(self):
	pass
    def fax(self):
	pass 			## We don't need this
    def scan(self):
	pass 			## We don't need this
Following the ISP rule
## Separate the methods into classes of there own then inherit only what you need
class Printer:
    def print(self):
	
class Fax:
    def fax(self):

class Scanner:
    def scan(self):

class OldPrinter(Printer):
    ....
	
class PrinterScanner(Printer, Scanner):
    ....

Dependency Inversion Principle

The Dependency Inversion Principle (DIP) requires that High-level modules, which provide complex logic, should be easily reusable and unaffected by changes in low-level modules, which provide utility features. To achieve that, you need to introduce an abstraction that decouples the high-level and low-level modules from each other, by decoupling components it allows for easier testing, most often this is solved by using dependency injection which is different than dependency inversion.

DIP example
## Low-level component
class AuthenticationForUser():
  def __init__(self, connector:Connector):
		self.connection = connector.connect()
	
	def authenticate(self, credentials):
		pass
	def is_authenticated(self):
		pass	
	def last_login(self):
		pass

## High-level components
class AnonymousAuth(AuthenticationForUser):
	pass

class GithubAuth(AuthenticationForUser):
	def last_login(self):
		pass

class FacebookAuth(AuthenticationForUser):
	pass

class Permissions()
	def __init__(self, auth: AuthenticationForUser)
		self.auth = auth
		
	def has_permissions():
		pass
		
class IsLoggedInPermissions (Permissions):
	def last_login():
		return auth.last_log

Summary

Below is a table that summaries the SOLID principles

Single Responsibility Principle
  • A class should have only one reason to change
  • Separation of concerns, different classes handling different independent tasks/problems
Open-Closed Principle
  • Classes should be open for extension but closed for modification
Liskov Substitution Principle
  • You should be able to substitute a base type for a subtype
Iterface Segregation Principle
  • Don't put too much into an Interface, split into separate specific Interfaces
  • YAGNI - You Ain't Going To Need It
Dependency Inversion Principle
  • High-Level modules should not depend upon Low-Level Modules, use Abstractions instead