# Topic covered
* SOLID Principles
* Issue with BAD Code
* Motivation behind the usage of SOLID Principles
* SOLID
* Single Responsibility Principle (SRP)
* Open closed Principle (OSP)
* Liskov substitution Principle (LSP)
* Interface Segregation Principle (ISP)
* Dependency Inversion Principle (DIP)
4.1 SOLID Principles
SOLID principles is a popular set of design principles
that are used in object-oriented software development.
The SOLID principle was introduced by Robert C. Martin, also known as Uncle Bob
.
Design principles intended to make software designs more understandable
, flexible
and maintainable
so that many developers can collaboratively work
on.
4.2 Issue with BAD Code
It's Confusing
- -> readability problemRigidity problem
- -> code that has dependency cannot make isolated changeFragility
- -> change at some point, breaks code at some other place
OO helps to prevent rigidity, fragility, non re-usability
4.3 Motivation behind the usage of SOLID Principles
Important factors during the development cycle.
Maintainability: Maintainable systems are very important to the organisations.
Testability: Test driven development (TDD) is required when we design and develop large scale systems
Flexibility and Extensibility:
Flexibility and extensibility is a very much desirable factor of enterprise applications.
Hence, we should design the application to make it flexible so that it can be adapted to work
in different ways and extensible so that we can add new features easily
.
Parallel Development:
It is one of the key features in the application development as it is not practical to have the entire development team
working simultaneously
on the same feature or component.
Loose Coupling (Independent):
We can address many of the requirements listed above by ensuring that our design results in an application
that loosely couples
many parts that makes up the application.
4.4 SOLID Acronym
S : Single Responsibility Principle (SRP)
O : Open closed Principle (OSP)
L : Liskov substitution Principle (LSP)
I : Interface Segregation Principle (ISP)
D : Dependency Inversion Principle (DIP)
4.5 Single Responsibility Principle
A class should have one and only one reason to change
, meaning that a class should have only one job
.
Everything in the class should be related to that single purpose. There can be many members in the class as long as they related to the single responsibility.
And that responsibility should be entirely encapsulated by the class.
With SRP, classes become smaller, cleaner and less fragile
.
class Journal:
def __init__(self):
self.entries = []
self.count = 0
def add_entry(self, text):
self.entries.append(f"{self.count}: {text}")
self.count += 1
def remove_entry(self, pos):
del self.entries[pos]
def __str__(self):
return "\n".join(self.entries)
# break SRP
def save(self, filename):
file = open(filename, "w")
file.write(str(self))
file.close()
def load(self, filename):
pass
def load_from_web(self, uri):
pass
class PersistenceManager:
def save_to_file(journal, filename):
file = open(filename, "w")
file.write(str(journal))
file.close()
j = Journal()
j.add_entry("I cried today.")
j.add_entry("I ate a bug.")
print(f"Journal entries:\n{j}\n")
p = PersistenceManager()
file = r'c:\temp\journal.txt'
p.save_to_file(j, file)
# verify!
with open(file) as fh:
print(fh.read())
4.6 Open-Closed Principle
Software entities(classes, modules, functions, etc) should be open for extension, but closed for modification
.
Because changing the current behaviour of a Class will affect all the systems using that Class.
Implementation guidelines
- The simplest way to apply OCP is to implement the
new functionality on new derived
(sub) classes that inherit the original class implementation. - Another way is to allow client to access the original class with an abstract interface,
- So, at any given point of time when there is a requirement change instead of touching the existing functionality it’s always suggested to create new classes and leave the original implementation untouched.
What if I do not follow Open closed principle
- May end up
testing the entire functionality
along with the requirement. - Not following the Open Closed Principle breaks the SRP since the class or function might
end up doing multiple tasks
. - Maintenance of the class becomes difficult since the code of the class increases by thousands of unorganized lines.
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
class Size(Enum):
SMALL = 1
MEDIUM = 2
LARGE = 3
class Product:
def __init__(self, name, color, size):
self.name = name
self.color = color
self.size = size
class ProductFilter:
def filter_by_color(self, products, color):
for p in products:
if p.color == color: yield p
def filter_by_size(self, products, size):
for p in products:
if p.size == size: yield p
def filter_by_size_and_color(self, products, size, color):
for p in products:
if p.color == color and p.size == size: yield p
# state space explosion
# 3 criteria
# c s w cs sw cw csw = 7 methods
# OCP = open for extension, closed for modification
class Specification:
def is_satisfied(self, item):
pass
# and operator makes life easier
def __and__(self, other):
return AndSpecification(self, other)
class Filter:
def filter(self, items, spec):
pass
class ColorSpecification(Specification):
def __init__(self, color):
self.color = color
def is_satisfied(self, item):
return item.color == self.color
class SizeSpecification(Specification):
def __init__(self, size):
self.size = size
def is_satisfied(self, item):
return item.size == self.size
# class AndSpecification(Specification):
# def __init__(self, spec1, spec2):
# self.spec2 = spec2
# self.spec1 = spec1
#
# def is_satisfied(self, item):
# return self.spec1.is_satisfied(item) and \
# self.spec2.is_satisfied(item)
class AndSpecification(Specification):
def __init__(self, *args):
self.args = args
def is_satisfied(self, item):
return all(map(
lambda spec: spec.is_satisfied(item), self.args))
class BetterFilter(Filter):
def filter(self, items, spec):
for item in items:
if spec.is_satisfied(item): yield item
apple = Product('Apple', Color.GREEN, Size.SMALL)
tree = Product('Tree', Color.GREEN, Size.LARGE)
house = Product('House', Color.BLUE, Size.LARGE)
products = [apple, tree, house]
pf = ProductFilter()
print('Green products (old):')
for p in pf.filter_by_color(products, Color.GREEN):
print(f' - {p.name} is green')
# ^ BEFORE
# v AFTER
bf = BetterFilter()
print('Green products (new):')
green = ColorSpecification(Color.GREEN)
for p in bf.filter(products, green):
print(f' - {p.name} is green')
print('Large products:')
large = SizeSpecification(Size.LARGE)
for p in bf.filter(products, large):
print(f' - {p.name} is large')
print('Large blue items:')
# large_blue = AndSpecification(large, ColorSpecification(Color.BLUE))
large_blue = large & ColorSpecification(Color.BLUE)
for p in bf.filter(products, large_blue):
print(f' - {p.name} is large and blue')
4.7 Liskov Substitution Principle
Introduced by Barbara Liskov state that “objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program”
We can also state that derived types must be completely substitutable for their base types
.
The child Class should be able to do everything the parent Class can do
.
This principle is just an extension of the Open Close Principle.
Implementation guidelines
In the process of development we should ensure that
- No new exceptions can be thrown by the subtype unless they are part of the existing exception hierarchy.
- We should also ensure that Clients should not know which specific subtype they are calling, nor should they need to know that.
- The client should behave the same regardless of the subtype instance that it is given.
- New derived classes just
extend without replacing
the functionality of old classes
class Rectangle:
def __init__(self, width, height):
self._height = height
self._width = width
@property
def area(self):
return self._width * self._height
def __str__(self):
return f'Width: {self.width}, height: {self.height}'
@property
def width(self):
return self._width
@width.setter
def width(self, value):
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
self._height = value
class Square(Rectangle):
def __init__(self, size):
Rectangle.__init__(self, size, size)
@Rectangle.width.setter
def width(self, value):
_width = _height = value
@Rectangle.height.setter
def height(self, value):
_width = _height = value
def use_it(rc):
w = rc.width
rc.height = 10 # unpleasant side effect
expected = int(w * 10)
print(f'Expected an area of {expected}, got {rc.area}')
rc = Rectangle(2, 3)
use_it(rc)
sq = Square(5)
use_it(sq)
4.8 Interface Segregation Principle
Many
client-specific interfaces are better than one
general-purpose interface.
It means instead of creating one big interface
we can break down it to smaller
interfaces.
A client should never be forced to implement an interface that it doesn’t use
,
or clients should not be forced to depend on methods they do not use.
The ISP was first used and formulated by Robert C. Martin while consulting for Xerox.
from abc import abstractmethod
class Machine:
def print(self, document):
raise NotImplementedError()
def fax(self, document):
raise NotImplementedError()
def scan(self, document):
raise NotImplementedError()
# ok if you need a multifunction device
class MultiFunctionPrinter(Machine):
def print(self, document):
pass
def fax(self, document):
pass
def scan(self, document):
pass
class OldFashionedPrinter(Machine):
def print(self, document):
# ok - print stuff
pass
def fax(self, document):
pass # do-nothing
def scan(self, document):
"""Not supported!"""
raise NotImplementedError('Printer cannot scan!')
class Printer:
@abstractmethod
def print(self, document): pass
class Scanner:
@abstractmethod
def scan(self, document): pass
# same for Fax, etc.
class MyPrinter(Printer):
def print(self, document):
print(document)
class Photocopier(Printer, Scanner):
def print(self, document):
print(document)
def scan(self, document):
pass # something meaningful
class MultiFunctionDevice(Printer, Scanner): # , Fax, etc
@abstractmethod
def print(self, document):
pass
@abstractmethod
def scan(self, document):
pass
class MultiFunctionMachine(MultiFunctionDevice):
def __init__(self, printer, scanner):
self.printer = printer
self.scanner = scanner
def print(self, document):
self.printer.print(document)
def scan(self, document):
self.scanner.scan(document)
printer = OldFashionedPrinter()
printer.fax(123) # nothing happens
printer.scan(123) # oops!
4.9 Dependency Inversion Principle
Entities must depend on abstractions, not on concretions
.
It states that the high-level module must not depend on the low-level module
, but they should depend on abstractions.
Abstractions should not depend on the details whereas the details should depend on abstractions.
from abc import abstractmethod
from enum import Enum
class Relationship(Enum):
PARENT = 0
CHILD = 1
SIBLING = 2
class Person:
def __init__(self, name):
self.name = name
class RelationshipBrowser:
@abstractmethod
def find_all_children_of(self, name): pass
class Relationships(RelationshipBrowser): # low-level
relations = []
def add_parent_and_child(self, parent, child):
self.relations.append((parent, Relationship.PARENT, child))
self.relations.append((child, Relationship.PARENT, parent))
def find_all_children_of(self, name):
for r in self.relations:
if r[0].name == name and r[1] == Relationship.PARENT:
yield r[2].name
class Research:
# dependency on a low-level module directly
# bad because strongly dependent on e.g. storage type
# def __init__(self, relationships):
# # high-level: find all of john's children
# relations = relationships.relations
# for r in relations:
# if r[0].name == 'John' and r[1] == Relationship.PARENT:
# print(f'John has a child called {r[2].name}.')
def __init__(self, browser):
for p in browser.find_all_children_of("John"):
print(f'John has a child called {p}')
parent = Person('John')
child1 = Person('Chris')
child2 = Person('Matt')
# low-level module
relationships = Relationships()
relationships.add_parent_and_child(parent, child1)
relationships.add_parent_and_child(parent, child2)
Research(relationships)
4.10 If we don’t follow SOLID Principles we
- End up with
tight or strong coupling
of the code with many other modules/applications - Tight coupling causes
time to implement
any new requirement, features or any bug fixes and sometimes it creates unknown issues - End up with a code which is not testable
- End up with
duplication of code
- End up creating
new bugs
by fixing another bug - End up with
many unknown issues
in the application development cycle
4.11 Following SOLID Principles helps us to
- Achieve
reduction in complexity
of code - Increase
readability
, extensibility and maintenance - Reduce error and implement
re-usability
- Achieve Better
testability
- Reduce tight coupling
.
Practice Data Munging problem: http://codekata.com/kata/kata04-data-munging
Create proper classes. Try to apply OOPs concepts and do it using minimum amount of code.
Reference
- Bob Martin SOLID Principles of Object-Oriented and Agile Design
- Solid Design Principles
- https://medium.com/backticks-tildes/the-s-o-l-i-d-principles-in-pictures-b34ce2f1e898
- https://www.freecodecamp.org/news/solid-principles-explained-in-plain-english/
- SOLID: The First 5 Principles of Object-Oriented Design