S.O.L.I.D. principles as fast as possible
🧱

S.O.L.I.D. principles as fast as possible

Tags
Software Development
Object Oriented Design
Published
March 15, 2023

TLDR

SOLID is an acronym for classic 5 object oriented design principles, that are geared towards making software more maintainable and scalable. Reocurring theme in most of the principles is programming to the interface, not implementation.
Principle
Super-Quick Explanation
Single Responsibility
1 class should do 1 thing
Open-Closed
don’t modify classes, implement interfaces instead
Liskov Substitution
passing both parent and derived class does not change correctness of the program
Interface Segregation
create minimal interfaces, so classes can implement only what makes sense for them
Dependency Inversion
don’t make your class dependent on concretions, make it dependent on abstraction

Rock-SOLID Code

ℹ️
SOLID principles were originally introduced by Robert C. Martin (Uncle Bob).
Software is in a constant state of flux (a.k.a. continuous movement, frequently changing). New use cases are added and old ones change faster than the release cycle. There is certainly no silver bullet to combat this endless struggle, but SOLID principles can get you far. The goal of this guide is to help you understand these principle as fast as possible. It is structured in a way that every principle first has it’s pseudo-official definition followed by my simplified take. Each principle is then concluded by an example that violates it and an example that employs given principle. Code with explanation is always collapsed under a corresponding section so don’t forget to click ▶️ to expand.
🐍 Side Note: In Python there are no interface and implements keywords. Instead interfaces are implemented using abstract classes. In the article, I refer to those occurrences as “interfaces” (quotation marks included).

Single-responsibility principle (SRP)

💡
Every class should have only 1 responsibility
Create classes that do 1 thing only
❌ violation of SRP example
class Customer: def __init__(self, name: str, email: str): self.name = name self.email = email def create_customer(self): """ This method is responsible for creating the customer and saving it to the database. """ # code to create customer and save to database def send_email(self, message: str): """ This method is responsible for sending an email to the customer. """ # code to send email to customer
The principle is violated because Customer class has 2 responsibilities, creating customers and sending emails.
✅ employment of SRP example
class Customer: def __init__(self, name: str, email: str): self.name = name self.email = email class CustomerCreator: def create_customer(self, name: str, email: str): """ This method is responsible for creating the customer and saving it to the database. """ # code to create customer and save to database class EmailSender: def send_email(self, customer: Customer, message: str): """ This method is responsible for sending an email to the customer. """ # code to send email to customer
We have 1 class for storing customer’s data Customer, 1 class to create customer CustomerCreator, 1 class to send emails EmailSende. Each class does only 1 thing only.

Open-closed principle (OCP)

💡
Entities should be open for extension, but closed for modification.
Use interfaces and abstract methods to create new classes (extend), don’t modify existing classes
❌ violation of OCP example
class Shape: def __init__(self, shape_type): self.shape_type = shape_type def area(self): if self.shape_type == 'circle': # code to calculate area of circle elif self.shape_type == 'square': # code to calculate area of square elif self.shape_type == 'rectangle': # code to calculate area of rectangle
if we want to add a new shape, say a triangle, we would have to modify the existing Shape class → class is not closed for modification and not designed to be extensible
✅ employment of OCP example
class Shape: def area(self): pass class Circle(Shape): def __init__(self, radius): self.radius = radius def area(self): return 3.14 * (self.radius ** 2) class Square(Shape): def __init__(self, side): self.side = side def area(self): return self.side ** 2 class Rectangle(Shape): def __init__(self, length, width): self.length = length self.width = width def area(self): return self.length * self.width
Shape is an abstract base class with method area() that is not implemented. Circle , Square, and Rectangle classes inherit from Shape and implement their own area() method
Shape class is closed for modification, new shapes can be created by inheriting from it

Liskov segregation principle (LSP)

💡
Instances of a superclass should be able to be replaced with objects of a subclass.
You can use both object constructed using superclass and subclass and the program will be still correct (the behavior of subclasses is consistent with the superclass)
❌ violation of LSP example
class Bird: def fly(self): print("Flying...") class Penguin(Bird): def fly(self): raise Exception("Penguins can't fly!")
We cannot interchange Penguin class with Bird class, the behavior is overridden in an inconsistent way (printing vs. raising Exception)
✅ employment of LSP example
class Vehicle: def start_engine(self): pass class Car(Vehicle): def start_engine(self): print("Starting car engine...") class Motorcycle(Vehicle): def start_engine(self): print("Starting motorcycle engine...") class Driver: def __init__(self, vehicle): self.vehicle = vehicle def drive(self): self.vehicle.start_engine() car = Car() motorcycle = Motorcycle() driver1 = Driver(car) driver1.drive() # Output: Starting car engine... driver2 = Driver(motorcycle) driver2.drive() # Output: Starting motorcycle engine...
both Car and Motorcycle classes implement start_engine method that is based on Vehicle superclass and the behaviour is consistent, Driver expects vehicle regardless of the actual type, both Vehicle class and two of it’s subclasses can be passed with no problem

Interface Substitution principle (ISP)

💡
Classes should not have to implement methods they don't need
strive to create as small interfaces as possible so that class that needs methods outlined in an interface does not need to implement extra methods that does not make sense for it
❌ violation of ISP example
class Worker: def work(self): pass def eat(self): pass class SuperWorker(Worker): def work(self): print("Working super hard!") def eat(self): print("Eating a super lunch!") class LazyWorker(Worker): def work(self): print("Working very little...") def eat(self): pass
Worker ”interface” defines eat and sleep methods, LazyWorker impementation however, does not use it.
✅ employment of ISP example
from abc import ABC, abstractmethod class Workable(ABC): @abstractmethod def work(self): pass class Eatable(ABC): @abstractmethod def eat(self): pass class SuperWorker(Workable, Eatable): def work(self): print("Working super hard!") def eat(self): print("Eating a super lunch!") class LazyWorker(Workable): def work(self): print("Working very little...")
We have 2 different “interfaces” Workable and Eatable , both SuperWorker and LazyWorker implement only those interfaces that makes sense for them and they need. The

Dependency Inversion principle (DIP)

💡
High-level modules should not depend on low-level modules, both should depend on abstractions
❌ violation of DIP example
class LowLevelModule: def __init__(self): self.data = [] def get_data(self): return self.data class HighLevelModule: def __init__(self): self.llm = LowLevelModule() def process_data(self): data = self.llm.get_data() # Process data hlm = HighLevelModule() hlm.process_data()
Class HighLevelModule uses (depends on) LowLevelModule instance to function properly.
✅ employment of DIP example
from abc import ABC, abstractmethod class DataStorage(ABC): @abstractmethod def get_data(self): pass class LowLevelModule(DataStorage): def __init__(self): self.data = [] def get_data(self): return self.data class HighLevelModule: def __init__(self, data_storage: DataStorage): self.ds = data_storage def process_data(self): data = self.ds.get_data() # Process data llm = LowLevelModule() hlm = HighLevelModule(llm) hlm.process_data()
Class HighLevelModule here accepts DataStorage ”interface”. This can be any class that implements it. Here it’s LowLevelModule class creating the instance llm.
 

Picture of Hynek Zemanec

Written by Hynek Zemanec who lives and works in Vienna, Austria, building beautiful things. Follow him on Twitter and check out his Photography adventures