You are on page 1of 9

solid

October 3, 2022

0.0.1 SOLID
The rst ve principle of Object Orianted Design.
In software engineering, SOLID is a acronym for ve design principles intended to make object-
oriented designs more understandable, exible, and maintainable. The principles are a subset of
many principles promoted by American software engineer and instructor Robert C. Martin, rst
introduced in his 2000 paper Design Principles and Design Patterns.
Note While these principles can applied to any [Object Orianted] programming languages but in
this notebook we use Python. - But the supplimental material is in C# and PHP.

0.0.2 Single Responsibility Principle


It states that every module or class should have responsibility over a single part of the functionality
provided by the software, and that responsibility should be entirely encapsulated by the class. All
its services should be narrowly aligned with that responsibility. A class should have only one reason
to change. What the heck does that mean? It mean that one class should have only one task or
job. If a class is doing more than one things then it is violating the Single Responsibility Principle,
which causes the class to be harder to maintain and test.

Example Consider an application that takes a list[collections] of shapes, it can be Circle or Square
and calculate the sum of the all areas. - First Create Square Class, you need to know the length
of the square.
[38]: class Square:

def __init__(self, length: int = 0) -> None:


self.length = length

ˆ Then Create Circle Class, you need to know the radius of the circle.
[39]: class Circle:
def __init__(self, radius: int = 0) -> None:
self.radius = radius

ˆ Next Create AreaCalculator Class, it takes a list of shapes and calculate the sum of the all
areas.
ˆ The area pf Circle is πr2 and the area of Square is l2 .

1
[40]: class AreaCalculator:
def __init__(self, shapes: list) -> None:
self.shapes = shapes

def sum(self) -> int:


total = 0
for shape in self.shapes:
# isinstance() is a built-in function that returns True
# if the given object is an instance of the specified type,
# otherwise False.
if isinstance(shape, Circle):
total += shape.radius ** 2 * 3.14
elif isinstance(shape, Square):
total += shape.length ** 2
return total
def output(self) -> str:
import json as j
return j.dumps({
'area': self.sum()
})

To use the AreaCalculator class, you will need to instantiate the class and pass in an array of
shapes and print the output.
[41]: shapes = [Square(2), Square(3), Circle(4), Square(5), Circle(6.5)]
area = AreaCalculator(shapes)
print(area.output())

{"area": 220.905}
ˆ Here the problem arise with output method, for now it does not have problem but consider
the case when you have to convert output to other formats like XML, HTML or any other.
ˆ All of the logic would be handled by the AreaCalculator class. This would violate the single-
responsibility principle. The AreaCalculator class should only be concerned with the sum
of the areas of provided shapes. It should not care about how the output is formatted or to
output the result at all.
ˆ To resolve the problem, we can create a new class called SumCalculatorOutputter and move
the output logic to this class. The AreaCalculator class will now only be concerned with
calculating the sum of the areas of provided shapes. The SumCalculatorOutputter class will
be responsible for formatting the output.
[42]: class AreaCalculator:
def __init__(self, shapes: list) -> None:
self.shapes = shapes

def sum(self) -> int:


total = 0
for shape in self.shapes:

2
# isinstance() is a built-in function that returns True
# if the given object is an instance of the specified type,
# otherwise False.
if isinstance(shape, Circle):
total += shape.radius ** 2 * 3.14
elif isinstance(shape, Square):
total += shape.length ** 2
return total
# Removed output method.

[43]: class AreaCalculatorOutputter:


def __init__(self, AreaCalculator: AreaCalculator) -> None:
self.calculator = AreaCalculator

def json(self) -> str:


import json as j
return j.dumps({
'area': self.calculator.sum()
})

def html(self) -> str:


return f"""<html><body><h1>{self.calculator.sum()}</h1></body></html>
"""

def xml(self) -> str:


return f"""<xml><area>{self.calculator.sum()}</area></xml>
"""

[44]: output = AreaCalculatorOutputter(area)


print(output.json())
print(output.xml())

{"area": 220.905}
<xml><area>220.905</area></xml>

That satises the single-responsibility principle.

0.0.3 Open-Closed Principle


It states that software entities (classes, modules, functions, etc.) should be open for extension, but
closed for modication. This means that a class should be extendable without modifying the class
itself. This is achieved by using inheritance and composition.
Let's revsit the previous example AreaCalculator
ˆ Consider a case where you need to add a new shape Triangle to the AreaCalculator class.
You will need to modify the AreaCalculator class to add the new shape. This violates the
open-closed principle.

3
ˆ In order to solve this issue you have to remove sum from AreaCalculator class and then attach
to each shape class. This way you can add new shape without modifying the AreaCalculator
class.
[45]: from abc import abstractmethod

class ShapeInterface:
@abstractmethod
def area(self) -> int:
pass

class Square(ShapeInterface):
def __init__(self, length: int) -> None:
self.length = length

def area(self) -> int:


return self.length ** 2

class Circle(ShapeInterface):
def __init__(self, radius: int) -> None:
self.radius = radius

def area(self) -> int:


return self.radius ** 2 * 3.14

class Triangle(ShapeInterface):
def __init__(self, base: int, height: int) -> None:
self.base = base
self.height = height

def area(self) -> int:


return self.base * self.height / 2

class AreaCalculator:
def __init__(self, shapes: list) -> None:
self.shapes = shapes

def sum(self) -> int:


total = 0
for shape in self.shapes:
total += shape.area()
return total

ˆ Above we use interace to enable that every shape class must have area method.
 Python does not have interface, but we can use abc module to create abstract class.
 You can nd more about abc module here
 You can nd more about OOP in Notebook.

4
That satises the open-closed principle.

0.0.4 Liskov Substitution Principle


ˆ It states 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.
ˆ This means that every subclass or derived class should be substitutable for their base or parent
class.
ˆ Consider the following example:
[46]: from abc import ABC, abstractmethod

class Notification(ABC):
@abstractmethod
def notify(self, message, email):
pass

class Email(Notification):
def notify(self, message, email):
print(f'Send {message} to {email}')

class SMS(Notification):
def notify(self, message, phone):
print(f'Send {message} to {phone}')

ˆ In this example, we have three classes: Notification, Email, and SMS. The Email and SMS
classes inherit from the Notification class.
ˆ The Notication abstract class has notify() method that sends a message to an email address.
ˆ The Email class overrides the notify() method and sends an email to an email address.
ˆ The SMS class overrides the notify() method and sends an SMS to a phone number.
ˆ This violates the Liskov Substitution Principle. The SMS class should be substitutable for
the Notification class. The SMS class should be able to use the notify() method of the
Notification class without any issues. The SMS class should not have to change the signature
of the notify() method.
ˆ Conform with the Liskov substitution principle
ˆ First redene the notify() method in the Notification class so that it does not take email
parameter.
[47]: class Notification(ABC):
@abstractmethod
def notify(self, message):

5
pass

ˆ Now add email parameter to the Email class.


ˆ In same way add phone parameter to the SMS class.

[48]: class Email(Notification):


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

def notify(self, message):


print(f'Send "{message}" to {self.email}')

[49]: class SMS(Notification):


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

def notify(self, message):


print(f'Send "{message}" to {self.phone}')

ˆ Now add NotificationManager class that takes a object of Notification and send notica-
tion to the user.
[50]: class NotificationManager:
def __init__(self, notification):
self.notification = notification

def send(self, message):


self.notification.notify(message)

ˆ Finally create Email and SMS object and pass it to the NotificationManager class.

[51]: sms_notification = SMS('(123)-456-7890')


email_notification = Email('alice@example.com')

notification_manager = NotificationManager(sms_notification)
notification_manager.send('Hello Alice')

notification_manager.notification = email_notification
notification_manager.send('Hi Bob')

Send "Hello Alice" to (123)-456-7890


Send "Hi Bob" to alice@example.com
ˆ This satises the Liskov Substitution Principle.

0.0.5 Interface Segregation Principle


It states that no client should be forced to depend on methods it does not use. This principle is
about making ne grained interfaces that are client specic. By doing so, we prevent the bloating

6
of a single interface and promote decoupling of software modules. - Decoupling means that the
software modules are independent of each other and have looser coupling. - Python does not have
interface, but we can use abc module to create abstract class. - This mimic the interface. - Consider
the Vehicle class. It has drive() and fly() methods. The Car class inherits from the Vehicle
class and implements the drive() method. The Airplane class inherits from the Vehicle class
and implements the fly() method. - But the Car class does not need the fly() method and the
Airplane class does not need the drive() method. This violates the interface segregation principle.
[52]: class Vehicle(ABC):
@abstractmethod
def drive(self):
pass
@abstractmethod
def fly(self):
pass

class Car(Vehicle):
def drive(self):
print('Car is driving')

def fly(self):
raise Exception('Car cannot fly')

class Airplane(Vehicle):
def drive(self):
raise Exception('Airplane cannot drive')

def fly(self):
print('Airplane is flying')

ˆ In order to solve this issue, we can create two separate interfaces: Drivable and Flyable.
The Car class and Airplane class will implement these interfaces.
[53]: class Flyable(ABC):
@abstractmethod
def fly(self):
pass

class Driveable(ABC):
@abstractmethod
def drive(self):
pass

class Car(Driveable):
def drive(self):
print('Car is driving')

7
class Airplane(Flyable):
def fly(self):
print('Airplane is flying')

car = Car()
airplane = Airplane()
car.drive()
airplane.fly()

Car is driving
Airplane is flying
ˆ This satises the interface segregation principle.

0.0.6 Dependency Inversion Principle


ˆ It states that high-level modules should not depend on low-level modules. Both should de-
pend on abstractions. Abstractions should not depend on details. Details should depend on
abstractions.
ˆ This principle is about minimizing the dependencies between software modules. It is about
creating loosely coupled software modules.
 Loosely coupled means that the modules are not dependent on each other. They can
be developed, tested, and deployed independently.
ˆ Consider the following example PasswordReminder that connects to a database and retrieves
a user's password. The PasswordReminder class depends on MySQLConnection class. This
violates the dependency inversion principle.
 The PasswordReminder class should not depend on the MySQLConnection class. It should
depend on an abstraction.
 The MySQLConnection class is a low-level module. The PasswordReminder class is a
high-level module. So according to the denation of the dependency inversion principle,
the PasswordReminder class should not depend on the MySQLConnection class. It should
depend on abstraction not on details.
[54]: class MySQLConnection:
def connect(self):
return "Database Connection"

class PasswordReminder:
def __init__(self, connection: MySQLConnection) -> None:
self.connection = connection
print("Password Reset")

ˆ To solve this issue, we can create an interface called DBConnection and move the connect()
method to this interface. The MySQLConnection class will implement this interface. The
PasswordReminder class will depend on the Connection interface.
[55]: class DBConnectionInterface(ABC):
@abstractmethod

8
def connect(self):
pass

class MySQLConnection(DBConnectionInterface):
def connect(self):
# Handle the database connection.
return "Database Connection"

class PasswordReminder:
def __init__(self, connection: DBConnectionInterface) -> None:
self.connection = connection
print("Password reset")

mysql = MySQLConnection()

reset = PasswordReminder(mysql)
mysql.connect()

Password reset

[55]: 'Database Connection'

ˆ This satised Dependency inversion principle.

You might also like