About us Guides Projects Contacts
Админка
please wait

SOLID principles are five design guidelines that help developers write maintainable, flexible, and scalable code. Understanding and applying these principles separates professional-grade code from quick hacks. This guide covers practical SOLID application from a senior developer's perspective.

Why SOLID Matters

Following SOLID principles leads to:

  1. Maintainability: Code is easier to modify without breaking
  2. Testability: Components can be tested in isolation
  3. Flexibility: New features require less refactoring
  4. Readability: Clear responsibilities and interfaces
  5. Collaboration: Teams can work on separate components

S — Single Responsibility Principle (SRP)

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

Bad Example

class User:
def __init__(self, name, email):
self.name = name
self.email = email
def save_to_database(self):
# Database logic
db.execute("INSERT INTO users...")
def send_welcome_email(self):
# Email logic
smtp.send(self.email, "Welcome!")
def generate_report(self):
# Reporting logic
return f"User Report: {self.name}"

This class has four reasons to change: user data, database operations, email sending, and reporting.

Good Example

class User:
"""Only handles user data."""
def __init__(self, name, email):
self.name = name
self.email = email
class UserRepository:
"""Handles user persistence."""
def save(self, user: User):
db.execute("INSERT INTO users...", user.name, user.email)
def find_by_email(self, email: str) -> User:
row = db.query("SELECT * FROM users WHERE email = ?", email)
return User(row['name'], row['email'])
class EmailService:
"""Handles email sending."""
def send_welcome(self, user: User):
smtp.send(user.email, "Welcome!", self._welcome_template(user))
def _welcome_template(self, user: User) -> str:
return f"Hello {user.name}, welcome to our platform!"
class UserReportGenerator:
"""Handles user reports."""
def generate(self, user: User) -> str:
return f"User Report: {user.name}, Email: {user.email}"

Each class now has a single responsibility and can be modified independently.

O — Open/Closed Principle (OCP)

Definition: Classes should be open for extension but closed for modification.

Bad Example

class PaymentProcessor:
def process(self, payment_type: str, amount: float):
if payment_type == "credit_card":
# Credit card logic
return self._process_credit_card(amount)
elif payment_type == "paypal":
# PayPal logic
return self._process_paypal(amount)
elif payment_type == "bitcoin":
# Bitcoin logic
return self._process_bitcoin(amount)
# Adding a new payment method requires modifying this class

Every new payment method requires modifying the processor.

Good Example

from abc import ABC, abstractmethod
class PaymentMethod(ABC):
"""Abstract base class for payment methods."""
@abstractmethod
def process(self, amount: float) -> bool:
pass
class CreditCardPayment(PaymentMethod):
def __init__(self, card_number: str, cvv: str):
self.card_number = card_number
self.cvv = cvv
def process(self, amount: float) -> bool:
# Credit card processing logic
return gateway.charge(self.card_number, amount)
class PayPalPayment(PaymentMethod):
def __init__(self, email: str):
self.email = email
def process(self, amount: float) -> bool:
# PayPal processing logic
return paypal.charge(self.email, amount)
class BitcoinPayment(PaymentMethod):
def __init__(self, wallet_address: str):
self.wallet_address = wallet_address
def process(self, amount: float) -> bool:
# Bitcoin processing logic
return bitcoin.transfer(self.wallet_address, amount)
class PaymentProcessor:
"""Processor is closed for modification, open for extension."""
def process(self, method: PaymentMethod, amount: float) -> bool:
return method.process(amount)
# Usage: Adding a new payment method doesn't require changing PaymentProcessor
class ApplePayPayment(PaymentMethod):
def process(self, amount: float) -> bool:
return apple_pay.charge(amount)

New payment methods can be added without touching existing code.

L — Liskov Substitution Principle (LSP)

Definition: Subclasses should be substitutable for their base classes without altering program correctness.

Bad Example

class Rectangle:
def __init__(self, width: int, height: int):
self._width = width
self._height = height
def set_width(self, width: int):
self._width = width
def set_height(self, height: int):
self._height = height
def area(self) -> int:
return self._width * self._height
class Square(Rectangle):
def set_width(self, width: int):
self._width = width
self._height = width # Breaks LSP!
def set_height(self, height: int):
self._width = height # Breaks LSP!
self._height = height
def calculate_area(rect: Rectangle):
rect.set_width(5)
rect.set_height(4)
assert rect.area() == 20 # Fails for Square!

Square changes behavior inherited from Rectangle, breaking substitutability.

Good Example

from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
class Square(Shape):
def __init__(self, side: float):
self.side = side
def area(self) -> float:
return self.side ** 2
def print_area(shape: Shape):
print(f"Area: {shape.area()}") # Works for any shape
# Both work correctly
print_area(Rectangle(5, 4)) # Area: 20
print_area(Square(5)) # Area: 25

I — Interface Segregation Principle (ISP)

Definition: Clients should not be forced to depend on interfaces they don't use.

Bad Example

from abc import ABC, abstractmethod
class Worker(ABC):
@abstractmethod
def work(self):
pass
@abstractmethod
def eat(self):
pass
@abstractmethod
def sleep(self):
pass
class Human(Worker):
def work(self):
print("Working")
def eat(self):
print("Eating")
def sleep(self):
print("Sleeping")
class Robot(Worker):
def work(self):
print("Working")
def eat(self):
pass # Robots don't eat—forced to implement
def sleep(self):
pass # Robots don't sleep—forced to implement

Good Example

from abc import ABC, abstractmethod
class Workable(ABC):
@abstractmethod
def work(self):
pass
class Eatable(ABC):
@abstractmethod
def eat(self):
pass
class Sleepable(ABC):
@abstractmethod
def sleep(self):
pass
class Human(Workable, Eatable, Sleepable):
def work(self):
print("Human working")
def eat(self):
print("Human eating")
def sleep(self):
print("Human sleeping")
class Robot(Workable):
def work(self):
print("Robot working")
# Factory that only needs workers
def assign_work(workers: list[Workable]):
for worker in workers:
worker.work()
# Works with both
assign_work([Human(), Robot()])

Clients depend only on the interfaces they actually need.

D — Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.

Bad Example

class MySQLDatabase:
def connect(self):
return mysql.connect("localhost", "user", "pass")
def query(self, sql: str):
return self.connect().execute(sql)
class UserService:
def __init__(self):
self.db = MySQLDatabase() # Direct dependency on MySQL
def get_user(self, user_id: int):
return self.db.query(f"SELECT * FROM users WHERE id = {user_id}")

UserService is tightly coupled to MySQL—can't easily switch databases.

Good Example

from abc import ABC, abstractmethod
class Database(ABC):
"""Abstraction that both high and low-level modules depend on."""
@abstractmethod
def query(self, sql: str):
pass
@abstractmethod
def execute(self, sql: str, params: tuple):
pass
class MySQLDatabase(Database):
def __init__(self, host: str, user: str, password: str):
self.connection = mysql.connect(host, user, password)
def query(self, sql: str):
return self.connection.execute(sql)
def execute(self, sql: str, params: tuple):
return self.connection.execute(sql, params)
class PostgreSQLDatabase(Database):
def __init__(self, host: str, user: str, password: str):
self.connection = psycopg2.connect(host=host, user=user, password=password)
def query(self, sql: str):
return self.connection.cursor().execute(sql)
def execute(self, sql: str, params: tuple):
return self.connection.cursor().execute(sql, params)
class UserService:
def __init__(self, database: Database): # Depends on abstraction
self.db = database
def get_user(self, user_id: int):
return self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
# Easy to switch implementations
mysql_db = MySQLDatabase("localhost", "user", "pass")
user_service = UserService(mysql_db)
# Or use PostgreSQL without changing UserService
postgres_db = PostgreSQLDatabase("localhost", "user", "pass")
user_service = UserService(postgres_db)
# Or mock for testing
class MockDatabase(Database):
def query(self, sql: str):
return [{"id": 1, "name": "Test User"}]
def execute(self, sql: str, params: tuple):
pass
test_service = UserService(MockDatabase())

Complete Example: Order Processing System

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List
# Domain models (SRP)
@dataclass
class Product:
id: str
name: str
price: float
@dataclass
class Order:
id: str
products: List[Product]
@property
def total(self) -> float:
return sum(p.price for p in self.products)
# Abstractions (DIP)
class OrderRepository(ABC):
@abstractmethod
def save(self, order: Order) -> None:
pass
@abstractmethod
def find(self, order_id: str) -> Order:
pass
class NotificationService(ABC):
@abstractmethod
def notify(self, message: str, recipient: str) -> None:
pass
class PaymentGateway(ABC):
@abstractmethod
def charge(self, amount: float, payment_details: dict) -> bool:
pass
# Implementations (OCP - can add new implementations)
class DatabaseOrderRepository(OrderRepository):
def save(self, order: Order) -> None:
db.execute("INSERT INTO orders...")
def find(self, order_id: str) -> Order:
return db.query("SELECT * FROM orders WHERE id = ?", order_id)
class EmailNotificationService(NotificationService):
def notify(self, message: str, recipient: str) -> None:
smtp.send(recipient, "Order Update", message)
class SMSNotificationService(NotificationService):
def notify(self, message: str, recipient: str) -> None:
twilio.send(recipient, message)
class StripePaymentGateway(PaymentGateway):
def charge(self, amount: float, payment_details: dict) -> bool:
return stripe.charge(amount, payment_details['card_token'])
# High-level service (DIP - depends on abstractions)
class OrderService:
def __init__(
self,
repository: OrderRepository,
notification: NotificationService,
payment: PaymentGateway
):
self.repository = repository
self.notification = notification
self.payment = payment
def place_order(self, order: Order, payment_details: dict, customer_email: str) -> bool:
if self.payment.charge(order.total, payment_details):
self.repository.save(order)
self.notification.notify(
f"Order {order.id} confirmed! Total: ${order.total}",
customer_email
)
return True
return False
# Dependency injection (composition root)
def create_order_service() -> OrderService:
return OrderService(
repository=DatabaseOrderRepository(),
notification=EmailNotificationService(),
payment=StripePaymentGateway()
)
# Usage
service = create_order_service()
order = Order("123", [Product("1", "Widget", 29.99)])
service.place_order(order, {"card_token": "tok_xxx"}, "[email protected]")

Key Takeaways

  1. SRP: One class, one responsibility
  2. OCP: Extend through abstraction, not modification
  3. LSP: Subclasses must honor parent contracts
  4. ISP: Small, focused interfaces over large ones
  5. DIP: Depend on abstractions, inject dependencies

SOLID principles aren't rigid rules—they're guidelines that improve code quality when applied thoughtfully. Start by identifying code smells, then refactor toward these principles incrementally.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive