Model-View-Controller

Разделение ответственности в веб-приложениях

Паттерн архитектуры, 1979

Что такое MVC?

Model-View-Controller — архитектурный паттерн, разделяющий приложение на три компонента: Model (данные), View (отображение), Controller (логика).

🔹 Архитектура MVC

┌─────────────┐
│    View     │ ← Отображает данные пользователю
│  (HTML/UI)  │
└──────┬──────┘
       │ обновляет
       ↓
┌─────────────┐
│ Controller  │ ← Обрабатывает действия пользователя
│  (Logic)    │
└──────┬──────┘
       │ запрашивает/обновляет
       ↓
┌─────────────┐
│   Model     │ ← Хранит данные и бизнес-логику
│   (Data)    │
└─────────────┘

🔹 Поток данных в MVC

  1. Пользователь взаимодействует с View (клик, форма)
  2. View отправляет запрос в Controller
  3. Controller обрабатывает запрос, работает с Model
  4. Model обновляет данные, возвращает результат
  5. Controller обновляет View
  6. View отображает новое состояние

Компоненты MVC

Model (Модель)

Представляет данные и бизнес-логику.

# 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

View (Представление)

Отображает данные пользователю. Не содержит бизнес-логику.

# 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). Только форматирование.

    Controller (Контроллер)

    Обрабатывает действия пользователя, координирует 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 — "умный". Вся логика здесь.

    MVC в веб-фреймворках

    🔹 Django (Python)

    # 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

    🔹 Flask (Python)

    # 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 }}

    🔹 FastAPI (Python) — API MVC

    # 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

    Варианты MVC

    MVP (Model-View-Presenter)

    View пассивна, Presenter управляет всем.

    ┌─────────────┐
    │    View     │ ← Пассивна, только отображение
    └──────┬──────┘
           │ события
           ↓
    ┌─────────────┐
    │ Presenter   │ ← Вся логика здесь
    └──────┬──────┘
           │ данные
           ↓
    ┌─────────────┐
    │   Model     │
    └─────────────┘

    💡 Используется в Android, Desktop приложениях

    MVVM (Model-View-ViewModel)

    View и ViewModel связаны через data binding.

    ┌─────────────┐
    │    View     │ ← Data binding с ViewModel
    └──────┬──────┘
           │ автоматически
           ↓
    ┌─────────────┐
    │ ViewModel   │ ← Представление состояния View
    └──────┬──────┘
           │ данные
           ↓
    ┌─────────────┐
    │   Model     │
    └─────────────┘

    💡 Используется в Angular, Vue.js, WPF

    MVT (Model-View-Template) — Django

    Django называет Controller "View", а View — "Template".

    ┌─────────────┐
    │  Template   │ ← HTML шаблоны (View)
    └──────┬──────┘
           │
           ↓
    ┌─────────────┐
    │    View     │ ← Контроллеры (Controller)
    └──────┬──────┘
           │
           ↓
    ┌─────────────┐
    │   Model     │ ← Модели данных
    └─────────────┘

    💡 Названия другие, но суть та же

    Практический пример: Блог

    🔹 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

    🔹 Controller

    # 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)

    🔹 View

    # 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

    Преимущества MVC

    • Разделение ответственности — каждый компонент делает своё
    • Переиспользование — одну Model можно использовать в разных View
    • Тестируемость — компоненты можно тестировать отдельно
    • Поддержка — легко найти, где что лежит
    • Масштабируемость — можно добавлять новые View/Controller

    Недостатки MVC

    • Сложность — для простых приложений избыточно
    • Оверхеад — больше кода, больше файлов
    • Связность — View и Controller часто тесно связаны
    • Обновления — изменения в Model могут затронуть всё

    💡 Когда использовать MVC?

    Сценарий Рекомендация
    Веб-приложения ✅ Отлично подходит
    REST API ✅ Хорошо (упрощённый MVC)
    Простой скрипт ❌ Избыточно
    Микросервисы ⚠️ Можно, но лучше другие паттерны

    Антипаттерны MVC

    ❌ Fat Controller

    # ❌ 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

    # ✅ 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

    # ❌ 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

    1. Разделение ответственности — Model, View, Controller делают своё
    2. Слабая связность — компоненты независимы
    3. Одна ответственность — каждый класс делает одну вещь
    4. Тонкий Controller — логика в сервисах, Controller координирует
    5. Пассивная View — только отображение, без логики
    "MVC — это не про код. Это про организацию мышления.
    Это про то, как структурировать приложение, чтобы его было легко понимать и изменять."