Architecture logicielle

MVC
Model · View · Controller

Le pattern architectural le plus répandu du web. Séparer les données, l'affichage et la logique — pour mieux organiser, tester et faire évoluer une application.

C'est quoi MVC ?

MVC est un pattern architectural — une façon d'organiser le code d'une application en trois couches séparées qui communiquent entre elles de manière définie.

🍽️
Analogie — un restaurant :
Le Model = la cuisine (les données, les recettes, le stockage).
La View = la salle (ce que le client voit : assiette, présentation).
Le Controller = le serveur (reçoit la commande, parle à la cuisine, apporte le plat).
📁
Pourquoi séparer ?
Sans MVC, tout est mélangé dans un seul fichier : requête SQL, calcul métier, HTML. Modifier l'affichage risque de casser la logique. Avec MVC, chaque partie peut évoluer indépendamment.
📅

MVC a été inventé en 1979 par Trygve Reenskaug pour Smalltalk. Il est aujourd'hui utilisé dans Rails, Django, Laravel, Spring, Express, Angular… c'est le pattern de référence du web.

🗄️
Model
Données & logique métier
notifie / interroge
🎮
Controller
Orchestration & décisions
choisit la vue
🖥️
View
Présentation à l'utilisateur
← Utilisateur envoie une requête HTTP → Controller reçoit →
CoucheResponsabilitéNe doit PAS
ModelDonnées, BDD, règles métierGénérer du HTML
ControllerRecevoir, décider, coordonnerContenir du SQL
ViewAfficher, formaterContenir de la logique

Rôles détaillés — M, V, C

🗄️ Model

Le modèle représente les données et la logique métier de l'application.

Il contient :

  • Les requêtes vers la base de données
  • Les règles de validation (email valide, âge > 0…)
  • Les calculs et traitements sur les données
  • Les relations entre entités (User a des Articles)

Il ne sait pas comment les données seront affichées.

🖥️ View

La vue est responsable de la présentation — uniquement ce que l'utilisateur voit.

Elle contient :

  • Les templates HTML (Jinja2, EJS, Blade…)
  • La mise en forme des données reçues
  • Les boucles d'affichage (for article in articles)
  • Peu ou pas de logique

Elle ne fait pas de requêtes BDD directement.

🎮 Controller

Le contrôleur est le chef d'orchestre — il reçoit les requêtes et coordonne.

Il :

  • Reçoit la requête HTTP (GET, POST…)
  • Valide les données entrantes
  • Appelle le bon Model
  • Choisit la View et lui passe les données

Il doit rester mince — pas de logique métier complexe.

💡

"Fat Model, Skinny Controller" — principe clé en MVC : toute la logique métier va dans le Model. Le Controller ne fait que router et déléguer. Un Controller de plus de 20 lignes est souvent un signe que de la logique a mal été placée.

Flux d'une requête — pas à pas

1
Requête HTTP — l'utilisateur clique sur "Voir les articles". Le navigateur envoie GET /articles.
2
Router → Controller — le routeur identifie la route et appelle ArticleController.index().
3
Controller → Model — le contrôleur demande Article.findAll() au modèle.
4
Model → BDD — le modèle exécute SELECT * FROM articles et retourne la liste.
5
Controller → View — le contrôleur passe la liste à la vue articles/liste.html.
6
View → Réponse HTML — la vue génère le HTML avec les données, envoyé au navigateur.
Tracer le flux en code (Express)
// 1. Routeur — routes/articles.js
router.get('/', ArticleController.index);
//                 ↑ le routeur délègue au Controller

// 2. Controller — controllers/ArticleController.js
exports.index = async (req, res) => {
  const articles = await Article.findAll(); // → Model
  res.render('articles/liste', { articles });   // → View
};

// 3. Model — models/Article.js
Article.findAll = async () => {
  const [rows] = await db.query('SELECT * FROM articles');
  return rows;                // données brutes → Controller
};

// 4. View — views/articles/liste.ejs
// <ul>
// <% articles.forEach(a => { %>
//   <li><%= a.titre %></li>
// <% }) %>
// </ul>
⚠️

Erreur classique débutant : mettre du SQL directement dans le Controller, ou faire des calculs métier dans la View. La règle : si ça touche aux données → Model. Si ça formate pour l'écran → View. Si ça orchestre → Controller.

MVC en Python — Flask

models/article.py — le Model
from database import db

class Article(db.Model):
    __tablename__ = 'articles'

    id      = db.Column(db.Integer, primary_key=True)
    titre   = db.Column(db.String(200), nullable=False)
    contenu = db.Column(db.Text)
    publie  = db.Column(db.Boolean, default=False)

    # Méthode de classe = logique métier dans le Model
    @classmethod
    def tous_publies(cls):
        return cls.query.filter_by(publie=True).all()

    @classmethod
    def creer(cls, titre, contenu):
        if not titre.strip():
            raise ValueError("Le titre ne peut pas être vide")
        article = cls(titre=titre, contenu=contenu)
        db.session.add(article)
        db.session.commit()
        return article
controllers/articles_ctrl.py — le Controller
from flask import Blueprint, render_template, request, redirect, url_for
from models.article import Article

articles_bp = Blueprint('articles', __name__)

# GET /articles
@articles_bp.route('/articles')
def index():
    articles = Article.tous_publies()     # ← demande au Model
    return render_template(               # ← choisit la View
        'articles/liste.html',
        articles=articles
    )

# POST /articles/nouveau
@articles_bp.route('/articles/nouveau', methods=['POST'])
def creer():
    titre   = request.form.get('titre')
    contenu = request.form.get('contenu')
    try:
        Article.creer(titre, contenu)     # ← délègue au Model
        return redirect(url_for('articles.index'))
    except ValueError as e:
        return render_template('articles/nouveau.html', erreur=str(e))
templates/articles/liste.html — la View (Jinja2)
{# La View ne fait qu'afficher — pas de SQL ici ! #}
{% extends "base.html" %}

{% block content %}
<h1>Articles publiés</h1>

{% if articles %}
  <ul>
  {% for article in articles %}
    <li>
      <a href="/articles/{{ article.id }}">
        {{ article.titre }}
      </a>
    </li>
  {% endfor %}
  </ul>
{% else %}
  <p>Aucun article pour l'instant.</p>
{% endif %}
{% endblock %}
app.py — point d'entrée Flask
from flask import Flask
from database import db
from controllers.articles_ctrl import articles_bp

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'

db.init_app(app)
app.register_blueprint(articles_bp)

if __name__ == '__main__':
    app.run(debug=True)
🐍

Flask est micro-framework — il ne force pas MVC, mais on choisit de l'organiser ainsi. Django, lui, impose une structure MVC (qu'il appelle MVT — Model, View, Template — même concept).

MVC en Node.js — Express

models/Article.js — le Model
const db = require('../config/db');

const Article = {
  async findAllPublies() {
    const [rows] = await db.query(
      'SELECT * FROM articles WHERE publie = 1'
    );
    return rows;
  },

  async findById(id) {
    const [rows] = await db.query(
      'SELECT * FROM articles WHERE id = ?', [id]
    );
    return rows[0] || null;
  },

  async creer({ titre, contenu }) {
    if (!titre?.trim())
      throw new Error('Titre requis');       // validation dans le Model
    const [r] = await db.query(
      'INSERT INTO articles (titre, contenu) VALUES (?, ?)',
      [titre, contenu]
    );
    return { id: r.insertId, titre, contenu };
  },
};

module.exports = Article;
controllers/articleCtrl.js — le Controller
const Article = require('../models/Article');

exports.index = async (req, res, next) => {
  try {
    const articles = await Article.findAllPublies(); // Model
    res.render('articles/liste', { articles });         // View
  } catch (err) { next(err); }
};

exports.creer = async (req, res, next) => {
  try {
    const article = await Article.creer(req.body);
    res.redirect('/articles');
  } catch (err) {
    res.render('articles/nouveau', { erreur: err.message });
  }
};
views/articles/liste.ejs — la View
<!-- Vue EJS — affichage uniquement -->
<h1>Articles</h1>
<% if (articles.length) { %>
  <ul>
    <% articles.forEach(a => { %>
      <li><a href="/articles/<%= a.id %>"><%= a.titre %></a></li>
    <% }) %>
  </ul>
<% } else { %>
  <p>Aucun article.</p>
<% } %>
routes/articles.js + index.js
// routes/articles.js
const router = require('express').Router();
const ctrl = require('../controllers/articleCtrl');
router.get('/',      ctrl.index);
router.post(/nouveau', ctrl.creer);
module.exports = router;

// index.js — brancher le moteur de templates
app.set('view engine', 'ejs');
app.set('views', './views');
app.use('/articles', require('./routes/articles'));

Structure de projet MVC

Python / Flask

projet/ ├── controllers/ │ └── articles_ctrl.py ├── models/ │ └── article.py ├── templates/ ← Views │ ├── base.html │ └── articles/ │ ├── liste.html │ └── detail.html ├── static/ ← CSS, JS, images ├── config.py ├── app.py ← point d'entrée └── requirements.txt

Node.js / Express

projet/ └── src/ ├── controllers/ │ └── articleCtrl.js ├── models/ │ └── Article.js ├── routes/ │ └── articles.js ├── views/ ← EJS/Pug │ └── articles/ │ └── liste.ejs └── config/ └── db.js index.js package.json
💡

Avec une API REST (retournant du JSON plutôt que du HTML), la View disparaît — le Controller retourne directement res.json(). C'est ce qu'on appelle parfois MC (Model-Controller) ou simplement une "architecture en couches".

Avantages & limites

Avantages

Séparation des responsabilitésModifier le HTML n'affecte pas la BDD
TestabilitéOn peut tester le Model sans démarrer le serveur
Travail en équipeBack-end (Model) et front-end (View) en parallèle
RéutilisabilitéUn même Model peut servir plusieurs Views
MaintenabilitéChaque bug est localisé dans une couche
⚠️

Limites

VerbositéBeaucoup de fichiers pour une petite fonctionnalité
Couplage Controller↔ViewLe Controller choisit encore la View — difficile à tester
Inadapté aux UI richesAvec React/Vue, la View a sa propre logique → MVVM plus adapté
ScalabilitéPour de très grandes apps, MVC devient insuffisant → architecture hexagonale

Variations — MVP, MVVM, MVI…

PatternDifférence clé vs MVCUtilisé dans
MVCController choisit la View et injecte les donnéesRails, Django, Express (templates), Spring MVC
MVPLe Presenter ne connaît pas la View concrète (interface) → plus testableAndroid (ancien), WinForms
MVVMViewModel expose des données observables — la View se bind automatiquementVue.js, React, Angular, Kotlin, WPF
MVIFlux unidirectionnel strict — Intents → Model → ViewReact + Redux, Kotlin Android
🗺️

Ces patterns se succèdent chronologiquement et répondent aux mêmes problèmes fondamentaux — séparer les données de l'affichage. MVVM est couvert dans la page dédiée. La page comparative met tout ça en perspective.

Cheat sheet MVC

🗄️ Model — contient

Requêtes SQL / ORM
Validation des données
Règles métier
Relations entre entités
Génération de HTML
Code HTTP / routes

🖥️ View — contient

HTML / templates
Boucles d'affichage
Formatage (dates, €…)
Requêtes SQL
Logique métier
Appels à la BDD

🎮 Controller — contient

Réception des requêtes HTTP
Appel du bon Model
Choix de la View
Validation légère (champs requis)
SQL complexe
Logique métier lourde

🏗️ Frameworks MVC

DjangoPython — MVT (≈ MVC)
FlaskPython — micro, MVC manuel
Ruby on RailsRuby — MVC strict
LaravelPHP — MVC + Eloquent ORM
Spring MVCJava — MVC enterprise
Express + EJSNode.js — MVC manuel