Принципы проектирования систем
Как строить компоненты, чтобы ими было удобно управлять
Компонент должен быть заменяем без переписывания системы
class EmailSender:
def send(self, msg): ...
# Можно заменить на SMS, Slack, etc.
class SMSSender:
def send(self, msg): ...
Один компонент — одна зона ответственности
auth-service/
├── login.py
├── register.py
└── token.py
# Не смешиваем с payment или profile
🔹 Легко тестировать, развивать, развертывать
Бизнес-логика не должна зависеть от деталей реализации
class TrainingService:
def __init__(self):
self.db = PostgreSQL() # жёсткая зависимость
def train_model(self):
data = self.db.get_data()
# ... обучение ...
self.db.save_result(result)
Что если поменяется БД?
class TrainingService:
def __init__(self, data_source: DataSource, result_sink: ResultSink):
self.source = source
self.sink = sink
def train_model(self):
data = self.source.get_data()
# ... обучение ...
self.sink.save(result)
# Зависимости внедряются снаружи
service = TrainingService(PostgresSource(), RedisSink())
🔹 Политика (что делать) отделена от деталей (как)
Связность и сцепление — два ключевых качества архитектуры
Компоненты зависят друг от друга минимально
# ❌ Сильное сцепление
user_service.db.engine.connection.cursor.execute(...)
# ✅ Слабое сцепление
user_repo.save(user) # абстракция
🔹 Меняешь реализацию — не ломается всё
Всё, что относится к одной задаче — вместе
notification/
├── email_sender.py
├── sms_sender.py
├── templates/
└── service.py
# Вместо:
# services/email.py
# utils/sms.py
# templates/notification/
🔹 Легко найти, понять, изменить
Низкое сцепление + высокая связанность
Где заканчивается одна часть и начинается другая
UI → Use Cases → Domain → Infrastructure
↑ ↑
зависит от зависит от
🔹 Код в Domain не знает про FastAPI или PostgreSQL
# auth-service предоставляет API
POST /login → { "token": "..." }
# order-service использует его
response = requests.post("auth/login", json=creds)
🔹 Внутренняя реализация скрыта за границей
# user/domain.py
class User: ...
# user/service.py
class UserService: ...
# user/repository.py
class UserRepository: ...
🔹 Явное разделение — меньше ошибок при изменениях
| Принцип | Суть |
|---|---|
| Component Principles | Компоненты должны быть заменяемыми и чёткими |
| Policy vs Detail | Бизнес — наверху, детали — снизу |
| Coupling & Cohesion | Меньше связей между, больше внутри |
| Boundaries | Чёткие границы — основа масштабируемости |
"Хорошая архитектура — это не про технологии.
Это про управляемость, гибкость и долгую жизнь системы."
Как принимать архитектурные решения
Что должно быть вместе? Что — отдельно?
class UserService:
def login(self): ... # безопасность
def update_profile(self): ... # UI
def send_email(self): ... # интеграция
def export_data(self): ... # ETL
Пять причин для изменения → хрупкий код
auth-service/
├── POST /login
└── JWT
profile-service/
├── GET /profile
└── PUT /avatar
notification-service/
├── send_email()
└── send_sms()
🔹 Каждый меняется независимо
Что стабильно? Что будет меняться?
class TrainingJob:
def run(self, dataset: Dataset) -> Model:
# бизнес-логика обучения
pass
Редко меняется
class CSVDataset(Dataset): ...
class HDF5Dataset(Dataset): ...
class LocalTrainer(Trainer): ...
class CloudTrainer(Trainer): ...
Будут меняться: форматы, платформы, API
Детали зависят от ядра,
а не наоборот.
→ Это и есть настоящая инверсия зависимостей
Кто на кого зависит? Где ядро?
core-models/ ← высокий Ca
├── User
├── Order
└── TrainingJob
many services depend on core-models
→ Должен быть стабильным!
notification-service/ ← высокий Ce
├── depends on: email-service
├── sms-gateway
└── slack-api
→ Может меняться, но сам — менее переиспользуем
Высокий Ca + низкий Ce = ядро системы
Низкий Ca + высокий Ce = периферия
Циклические зависимости — тупик
auth-service → order-service → auth-service
↑ ↓
payment-service ←────┘
🔹 Нельзя развернуть отдельно
🔹 Нельзя протестировать изолированно
# Через событие
event_bus.publish("user_registered", user_id)
# или через общий сервис
identity_service.get_user(user_id)
→ Разрыв цикла
На границе — не код, а договор
POST /login
{
"email": "user@example.com",
"password": "secret"
}
← 200 OK
{
"token": "xyz",
"expires": "2025-12-31"
}
← 401 Unauthorized
{
"error": "invalid_credentials"
}
🔹 Это контракт. Его нарушили — сломали систему.
class UserModel(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
email = Column(String)
# ↔ между domain и database
# При изменении таблицы — меняется только этот класс
ORM — не просто удобство. Это **защита домена**
Чтобы понять, зачем дальше: