S — Single Responsibility Principle (SRP)
A class should have one job and only one reason to change. Below codes separate printer, saver to different classes
class Report:
def __init__(self, data):
self.data = data
class ReportPrinter:
def print(self, report: Report):
print(report.data)
class ReportSaver:
def save(self, report: Report, filename):
with open(filename, 'w') as f:
f.write(report.data)
O — Open/Closed Principle (OCP)
Code should be open for extension but closed for modification. For example if you tuck all into one class for different discount like
def get_discount(price, customer_type): if customer_type == "student": return price * 0.9 elif customer_type == "senior": return price * 0.8
You will have to modify if new customer type is added, instead the better practice with O principle is
class DiscountStrategy:
def apply(self, price):
raise NotImplementedError
class StudentDiscount(DiscountStrategy):
def apply(self, price):
return price * 0.9
class SeniorDiscount(DiscountStrategy):
def apply(self, price):
return price * 0.8
def get_discount(price, strategy: DiscountStrategy):
return strategy.apply(price)
L — Liskov Substitution Principle (LSP)
Subclasses should be usable as their base class without breaking things.
I — Interface Segregation Principle (ISP)
Don’t force classes to implement methods they don’t use. Python doesn’t have formal interfaces, but we can simulate. Below codes the OldPrinter is forced to implement methods it doesn’t care about, it violated ISP:
class Machine: def print(self): pass def fax(self): pass def scan(self): pass class OldPrinter(Machine): def fax(self): raise NotImplementedError def scan(self): raise NotImplementedErrorthe proper version is
class Printer: def print(self): pass class Scanner: def scan(self): pass class Fax: def fax(self): pass class AllInOnePrinter(Printer, Scanner, Fax): def print(self): print("Printing") def scan(self): print("Scanning") def fax(self): print("Faxing")
D — Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules; both depend on abstractions. For example below is bad
class MySQLDatabase: def connect(self): pass class App: def __init__(self): self.db = MySQLDatabase()Better one is
class Database: def connect(self): raise NotImplementedError class MySQLDatabase(Database): def connect(self): print("Connecting to MySQL") class App: def __init__(self, db: Database): self.db = db
Now App depends on the abstract interface (Database) instead of a concrete class.
“Abstract” = Blueprint
Just like you can have a blueprint for a car without building one yet, you can define a class or method without implementing the details.
It Encourages consistent interfaces. Supports polymorphism (same interface, different behavior). Makes your code more modular and testable.
Abstraction is central in many design patterns. Here are three common ones:
Strategy Pattern
Use: Choose behavior at runtime.
Example: You want different sorting algorithms. It can switch sorting algos without touching class DataProcessor
class SortStrategy(ABC):
@abstractmethod
def sort(self, data):
pass
class QuickSort(SortStrategy):
def sort(self, data):
return sorted(data) # simulate quick sort
class BubbleSort(SortStrategy):
def sort(self, data):
# bubble sort logic here
return data
class DataProcessor:
def __init__(self, strategy: SortStrategy):
self.strategy = strategy
def process(self, data):
return self.strategy.sort(data)
Template Method Pattern
Use: Define the skeleton of an algorithm in an abstract class, and allow subclasses to fill in the steps.
Factory Pattern
Use: Create objects without specifying the exact class.
