ML Algorithms
Python
28 / 29

SOLID Principles

Five rules that keep object-oriented code easy to change.

The point of SOLID
Software changes constantly. SOLID is about minimizing the blast radius of every change — so adding a feature touches one class, not twenty.

S — Single Responsibility

A class should have one, and only one, reason to change.

# BAD: report generation + email sending in one class
class Report:
    def build(self): ...
    def send_email(self): ...

# GOOD: split responsibilities
class Report:        def build(self): ...
class ReportMailer:  def send(self, report): ...

O — Open/Closed

Open for extension, closed for modification. Add behavior without editing existing code.

# BAD: every new shape forces editing this function
def area(shape):
    if shape.kind == "circle": ...
    elif shape.kind == "square": ...

# GOOD: each shape owns its area
class Shape(Protocol):
    def area(self) -> float: ...

class Circle:  def area(self): return 3.14*self.r**2
class Square:  def area(self): return self.side**2

def total_area(shapes: list[Shape]) -> float:
    return sum(s.area() for s in shapes)

L — Liskov Substitution

Subtypes must be usable wherever their base type is — without surprises.

# BAD: violates LSP — callers of Bird.fly() break for Penguin
class Bird:    def fly(self): ...
class Penguin(Bird):
    def fly(self): raise NotImplementedError

# GOOD: model the actual hierarchy
class Bird:           ...
class FlyingBird(Bird):    def fly(self): ...
class Penguin(Bird):       ...

I — Interface Segregation

Don't force clients to depend on methods they don't use. Prefer many small Protocols.

# BAD: one fat interface
class Worker(Protocol):
    def work(self) -> None: ...
    def eat(self)  -> None: ...

class Robot:
    def work(self): ...
    def eat(self):  raise NotImplementedError  # robots don't eat

# GOOD: split
class Workable(Protocol):  def work(self) -> None: ...
class Eatable(Protocol):   def eat(self)  -> None: ...

D — Dependency Inversion

Depend on abstractions, not concretions. High-level code shouldn't know if storage is Postgres or a file.

# BAD: hard-wired to Postgres
class OrderService:
    def __init__(self):
        self.db = PostgresClient("prod-db:5432")

# GOOD: inject the abstraction
class Repository(Protocol):
    def save(self, order: Order) -> None: ...

class OrderService:
    def __init__(self, repo: Repository):
        self.repo = repo

# Swap implementations in tests / staging / prod
OrderService(PostgresRepo())
OrderService(InMemoryRepo())

Pythonic SOLID

  • Use Protocol instead of abstract base classes for interfaces.
  • Use plain functions when a class would have one method.
  • Inject dependencies in __init__, never call new inside business logic.
  • Test the principle, not the dogma — readability beats perfect adherence.