Разделение ответственности в веб-приложениях
Паттерн архитектуры, 1979
Model-View-Controller — архитектурный паттерн, разделяющий приложение на три компонента: Model (данные), View (отображение), Controller (логика).
┌─────────────┐
│ View │ ← Отображает данные пользователю
│ (HTML/UI) │
└──────┬──────┘
│ обновляет
↓
┌─────────────┐
│ Controller │ ← Обрабатывает действия пользователя
│ (Logic) │
└──────┬──────┘
│ запрашивает/обновляет
↓
┌─────────────┐
│ Model │ ← Хранит данные и бизнес-логику
│ (Data) │
└─────────────┘
Представляет данные и бизнес-логику.
# models/user.py
class User:
def __init__(self, user_id: int, name: str, email: str):
self.id = user_id
self.name = name
self.email = email
def is_valid(self) -> bool:
return "@" in self.email and len(self.name) > 0
def save(self):
# Сохранение в БД
db.execute("INSERT INTO users (name, email) VALUES (?, ?)",
(self.name, self.email))
@staticmethod
def find_by_id(user_id: int) -> 'User':
row = db.fetch_one("SELECT * FROM users WHERE id = ?", (user_id,))
return User(row['id'], row['name'], row['email'])
💡 Model не знает о View и Controller
Отображает данные пользователю. Не содержит бизнес-логику.
# views/user_view.py
class UserView:
def render_user(self, user: User) -> str:
return f"""
User Profile
{user.name}
Email: {user.email}
Edit
"""
def render_user_list(self, users: List[User]) -> str:
user_list = "".join([
f"{u.name} - {u.email} "
for u in users
])
return f"{user_list}
"
💡 View — "тупая" (dumb). Только форматирование.
Обрабатывает действия пользователя, координирует Model и View.
# controllers/user_controller.py
class UserController:
def __init__(self):
self.view = UserView()
def show_user(self, user_id: int) -> str:
# 1. Получаем данные из Model
user = User.find_by_id(user_id)
if not user:
return self.view.render_error("User not found")
# 2. Передаем в View
return self.view.render_user(user)
def create_user(self, name: str, email: str) -> str:
# 1. Создаем Model
user = User(id=None, name=name, email=email)
# 2. Валидация
if not user.is_valid():
return self.view.render_error("Invalid user data")
# 3. Сохраняем
user.save()
# 4. Перенаправляем на страницу пользователя
return self.view.redirect(f"/users/{user.id}")
💡 Controller — "умный". Вся логика здесь.
# models.py (Model)
from django.db import models
class User(models.Model):
name = models.CharField(max_length=100)
email = models.EmailField()
def is_valid(self):
return "@" in self.email
# views.py (Controller)
from django.shortcuts import render, get_object_or_404
from .models import User
def user_detail(request, user_id):
user = get_object_or_404(User, id=user_id)
return render(request, 'users/detail.html', {'user': user})
# templates/users/detail.html (View)
{{ user.name }}
Email: {{ user.email }}
💡 В Django: Model = models, View = templates, Controller = views
# models.py (Model)
class User:
def __init__(self, id, name, email):
self.id = id
self.name = name
self.email = email
# app.py (Controller)
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/users/')
def user_detail(user_id):
user = User.find_by_id(user_id)
return render_template('user_detail.html', user=user)
# templates/user_detail.html (View)
{{ user.name }}
Email: {{ user.email }}
# models.py (Model)
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
email: str
# controllers/users.py (Controller)
from fastapi import APIRouter
router = APIRouter()
@router.get("/users/{user_id}")
def get_user(user_id: int) -> User:
user = db.find_user(user_id)
return User(id=user.id, name=user.name, email=user.email)
@router.post("/users")
def create_user(user_data: UserCreate) -> User:
user = User.create(user_data.name, user_data.email)
return User(id=user.id, name=user.name, email=user.email)
# View — JSON response (API)
💡 В API View = JSON, а не HTML
View пассивна, Presenter управляет всем.
┌─────────────┐
│ View │ ← Пассивна, только отображение
└──────┬──────┘
│ события
↓
┌─────────────┐
│ Presenter │ ← Вся логика здесь
└──────┬──────┘
│ данные
↓
┌─────────────┐
│ Model │
└─────────────┘
💡 Используется в Android, Desktop приложениях
View и ViewModel связаны через data binding.
┌─────────────┐
│ View │ ← Data binding с ViewModel
└──────┬──────┘
│ автоматически
↓
┌─────────────┐
│ ViewModel │ ← Представление состояния View
└──────┬──────┘
│ данные
↓
┌─────────────┐
│ Model │
└─────────────┘
💡 Используется в Angular, Vue.js, WPF
Django называет Controller "View", а View — "Template".
┌─────────────┐
│ Template │ ← HTML шаблоны (View)
└──────┬──────┘
│
↓
┌─────────────┐
│ View │ ← Контроллеры (Controller)
└──────┬──────┘
│
↓
┌─────────────┐
│ Model │ ← Модели данных
└─────────────┘
💡 Названия другие, но суть та же
# models/post.py
class Post:
def __init__(self, post_id: int, title: str, content: str, author_id: int):
self.id = post_id
self.title = title
self.content = content
self.author_id = author_id
self.created_at = datetime.now()
def save(self):
db.execute("""
INSERT INTO posts (title, content, author_id, created_at)
VALUES (?, ?, ?, ?)
""", (self.title, self.content, self.author_id, self.created_at))
@staticmethod
def find_all() -> List['Post']:
rows = db.fetch_all("SELECT * FROM posts ORDER BY created_at DESC")
return [Post(**row) for row in rows]
@staticmethod
def find_by_id(post_id: int) -> 'Post':
row = db.fetch_one("SELECT * FROM posts WHERE id = ?", (post_id,))
return Post(**row) if row else None
# controllers/post_controller.py
from fastapi import APIRouter, HTTPException
from models.post import Post
from views.post_view import PostView
router = APIRouter()
view = PostView()
@router.get("/posts")
def list_posts():
posts = Post.find_all()
return view.render_post_list(posts)
@router.get("/posts/{post_id}")
def show_post(post_id: int):
post = Post.find_by_id(post_id)
if not post:
raise HTTPException(status_code=404, detail="Post not found")
return view.render_post(post)
@router.post("/posts")
def create_post(title: str, content: str, author_id: int):
post = Post(id=None, title=title, content=content, author_id=author_id)
post.save()
return view.render_post(post)
# views/post_view.py
from pydantic import BaseModel
class PostDTO(BaseModel):
id: int
title: str
content: str
author_id: int
created_at: str
class PostView:
def render_post(self, post: Post) -> PostDTO:
return PostDTO(
id=post.id,
title=post.title,
content=post.content,
author_id=post.author_id,
created_at=post.created_at.isoformat()
)
def render_post_list(self, posts: List[Post]) -> List[PostDTO]:
return [self.render_post(post) for post in posts]
💡 В API View возвращает DTO, в вебе — HTML
| Сценарий | Рекомендация |
|---|---|
| Веб-приложения | ✅ Отлично подходит |
| REST API | ✅ Хорошо (упрощённый MVC) |
| Простой скрипт | ❌ Избыточно |
| Микросервисы | ⚠️ Можно, но лучше другие паттерны |
# ❌ Controller делает слишком много
class UserController:
def create_user(self, name, email):
# Валидация
if not email or "@" not in email:
return error("Invalid email")
# Проверка существования
if db.find_user_by_email(email):
return error("User exists")
# Создание
user = User(name, email)
db.save(user)
# Отправка email
send_welcome_email(email)
# Логирование
logger.info(f"User created: {email}")
# Обновление аналитики
analytics.track("user_created")
return success()
Controller должен быть тонким! Выносите логику в сервисы.
# ✅ Controller координирует
class UserController:
def __init__(self, user_service: UserService):
self.user_service = user_service
def create_user(self, name, email):
try:
user = self.user_service.create_user(name, email)
return success(user)
except ValidationError as e:
return error(str(e))
Вся бизнес-логика в сервисах
# ❌ View содержит логику
def render_user(user):
if user.age > 18:
status = "adult"
else:
status = "minor"
if user.balance > 1000:
badge = "premium"
else:
badge = "regular"
return f"{user.name} - {status} - {badge}"
# ✅ Логика в Model/Service
def render_user(user):
return f"{user.name} - {user.status} - {user.badge}"
"MVC — это не про код. Это про организацию мышления.
Это про то, как структурировать приложение, чтобы его было легко понимать и изменять."