Les trois patterns de la couche de présentation — principes, schémas, métaphores et exemples TypeScript complets selon les principes de Robert C. Martin.
📖
Uncle Bob — Clean Architecture
Les patterns MVC, MVP et MVVM ne sont que des détails de la couche de présentation. Dans une Clean Architecture, ils sont relégués au rang de “Plugin UI”. Le domaine métier — Use Cases et Entities — ne doit jamais dépendre du pattern qui orchestre son affichage. Ce qui importe, c’est que les dépendances pointent toujours vers l’intérieur.
01
La métaphore du restaurant
Avant de plonger dans les schémas, une métaphore qui unifie les trois patterns. Imagine un restaurant gastronomique. Le client (l’Utilisateur) ne va jamais en cuisine. Il interagit avec une interface (la salle). La cuisine prépare les données (le Model).
Ce qui change entre MVC, MVP et MVVM, c’est qui orchestre, qui parle à qui, et qui porte la responsabilité de la logique de présentation.
👨🍳
MVC
Le Maître d'Hôtel
Le Controller prend ta commande, la transmet en cuisine, et dit à la salle comment dresser la table. Mais la salle peut regarder dans la cuisine directement.
Orchestrateur centralCouplage possible
🎭
MVP
Le Puppet Master
La View est une marionnette — elle ne fait rien par elle-même. Le Presenter tire toutes les ficelles et ne connaît la View que via un contrat (interface).
View passiveInterface stricte
📡
MVVM
Le Tableau de Bord
Le ViewModel publie des données en temps réel. La View s'abonne et se met à jour automatiquement — sans qu'on lui demande. Zéro référence directe.
Data bindingRéactivité
02
MVC — Model View Controller
Métaphore Le Controller est le maître d’hôtel. Il reçoit ta commande (action utilisateur), la passe en cuisine (Model) et dit à la salle comment dresser la table (View). Problème : la salle peut regarder directement dans la cuisine — c’est le couplage View → Model, la faille principale du pattern.
Utilisateur
Action UI
→1. action
Controller
Orchestre les actions
→2. met à jour
Model
Données + logique métier
→3. notifie
View
Affichage UI
→4. affiche
Utilisateur
Voit le résultat
Le Controller est le pivot central. Il reçoit les actions (requêtes HTTP, clics), interroge ou modifie le Model, puis sélectionne la View à rendre. La View peut parfois observer le Model directement — ce qui crée un couplage indésirable.
1
Utilisateur
Déclenche une action — clic, soumission de formulaire, requête HTTP GET/POST
2
Controller
Reçoit l'action, interprète les paramètres, appelle la bonne méthode du Model
3
Model
Exécute la logique métier, interroge la base de données, retourne les données
4
Model → View
Notifie les Views abonnées (Observer) ou le Controller choisit la View à rendre
5
View
Reçoit les données, génère le rendu HTML/UI, retourne le résultat à l'utilisateur
model/ArticleModel.ts
// MODEL — logique métier + accès aux données// Ne connaît ni la View ni le ControllerinterfaceArticle {
id: number;
titre: string;
contenu: string;
auteur: string;
}
classArticleModel {
private articles: Article[] = [
{ id: 1, titre: "Clean Code", contenu: "Écrire du code lisible...", auteur: "Bob Martin" },
{ id: 2, titre: "Clean Architecture", contenu: "Séparer les responsabilités...", auteur: "Bob Martin" },
];
findById(id: number): Article | undefined {
returnthis.articles.find(a => a.id === id);
}
findAll(): Article[] { returnthis.articles; }
}
// CONTROLLER — orchestre Model et View// C'est lui qui reçoit les requêtes HTTPclassArticleController {
constructor(private model: ArticleModel) {}
afficher(req: Request, res: Response): void {
const id = Number(req.params.id);
const article = this.model.findById(id);
if (!article) {
res.status(404).send(ArticleView.renderNotFound());
return;
}
res.send(ArticleView.render(article));
}
liste(req: Request, res: Response): void {
const articles = this.model.findAll();
res.send(ArticleView.renderListe(articles));
}
}
// Wiring — Express.jsconst controller = newArticleController(newArticleModel());
app.get('/articles', (req, res) => controller.liste(req, res));
app.get('/articles/:id', (req, res) => controller.afficher(req, res));
✓ Points forts
Simple à comprendre — le pattern standard du web
Frameworks nombreux : Express, NestJS, Django, Rails
Séparation claire des responsabilités initiales
Idéal pour le rendu côté serveur (SSR)
✗ Points faibles
La View peut dépendre du Model directement (couplage)
Controller difficile à tester unitairement (lié à HTTP)
Fat Controller : la logique de présentation dérive vers lui
Moins adapté aux UIs riches et réactives
03
MVP — Model View Presenter
Métaphore La View dans MVP est une marionnette. Elle ne fait rien par elle-même. Le Presenter tire les ficelles : il décide de tout, dit exactement quoi afficher, et la View obéit. Le Presenter ne connaît la View que via une interface (un contrat) — il ne sait même pas si c’est une vraie UI ou un mock de test. C’est là toute la puissance du MVP.
Utilisateur
Action UI
→1. action
View
Passive — délègue tout
⇔2. via IView
Presenter
Toute la logique de présentation
⇔3. lit / écrit
Model
Données + métier
Règle fondamentale MVP : La View implémente une interface (IView). Le Presenter ne référence jamais la View concrète — seulement l’interface. Cette séparation est le fondement de la testabilité totale.
view/ILoginView.ts — Le contrat strict
// INTERFACE — le contrat que toute View DOIT respecter// C'est la frontière entre Presenter et ViewinterfaceILoginView {
afficherChargement(visible: boolean): void;
afficherErreur(message: string): void;
afficherSucces(utilisateur: Utilisateur): void;
getEmail(): string;
getMotDePasse(): string;
}
La puissance du test MVP — Voici pourquoi l’interface ILoginView est un superpouvoir : on peut tester toute la logique de présentation sans démarrer le DOM.
tests/LoginPresenter.test.ts — Zero DOM, zero infra
// MockLoginView implémente ILoginView — c'est un espionclassMockLoginViewimplementsILoginView {
erreurAffichee = '';
chargementVisible = false;
utilisateurConnecte: Utilisateur | null = null;
afficherChargement(v: boolean) { this.chargementVisible = v; }
afficherErreur(msg: string) { this.erreurAffichee = msg; }
afficherSucces(u: Utilisateur) { this.utilisateurConnecte = u; }
getEmail() { return'test@test.com'; }
getMotDePasse() { return'1234'; }
}
// Test unitaire — pas besoin de navigateur, de DOM, ni de serveurtest('affiche une erreur si email vide', async () => {
const mock = newMockLoginView();
jest.spyOn(mock, 'getEmail').mockReturnValue('');
const presenter = newLoginPresenter(mock, newAuthService());
await presenter.onLoginClique();
expect(mock.erreurAffichee).toBe('Email et mot de passe requis');
expect(mock.chargementVisible).toBe(false);
});
✓ Points forts
Presenter testable à 100% sans DOM ni framework
View totalement passive — zéro logique à tester côté UI
L'interface IView est un contrat strict et explicite
Idéal pour Android natif, iOS UIKit
✗ Points faibles
Beaucoup de boilerplate — une interface par View
Relation 1-to-1 Presenter/View — peu flexible
Presenter peut grossir (God Presenter)
Mise à jour UI manuelle — pas de réactivité automatique
04
MVVM — Model View ViewModel
Métaphore Imagine un tableau de bord dans une salle de marché. Le ViewModel est le serveur de données qui publie des informations en temps réel. Le tableau (View) s’abonne automatiquement : dès qu’une donnée change, l’affichage se met à jour sans qu’on lui demande. La View ne tire pas les données — elle observe et réagit. C’est le Data Binding.
Utilisateur
Interagit
⇔binding bidir.
View
Déclarative / S’abonne
⇔observe / commandes
ViewModel
État observable / Zéro ref. View
⇔lit / écrit
Model
Données + métier
Règle fondamentale MVVM : Le ViewModel expose un état observable. Il ne connaît jamais la View et ne la référence jamais. La View s’abonne à l’état — quand l’état change, l’UI se met à jour automatiquement. C’est le contrat du Data Binding.
Le ViewModel est l’abstraction de l’état de la View. Il n’a aucune dépendance vers l’UI — ni DOM, ni composant, ni framework graphique. Il expose des observables (LiveData, Signal, BehaviorSubject, useState…) que la View consomme déclarativement.
core/Observable.ts — Le moteur du MVVM
// Observable générique — comprendre le mécanisme sans frameworkclassObservable<T> {
private valeur: T;
private abonnes: Array<(val: T) => void> = [];
constructor(valeurInitiale: T) { this.valeur = valeurInitiale; }
get(): T { returnthis.valeur; }
set(nouvelleValeur: T): void {
this.valeur = nouvelleValeur;
this.abonnes.forEach(fn => fn(nouvelleValeur)); // notifie tous les abonnés
}
abonner(fn: (val: T) => void): void {
this.abonnes.push(fn);
fn(this.valeur); // émission immédiate de la valeur actuelle
}
}
viewmodel/CompteurViewModel.ts
// VIEWMODEL — expose des Observables// Ne connaît PAS le DOM — aucun import UIclassCompteurViewModel {
readonly compteur = newObservable<number>(0);
readonly message = newObservable<string>('Prêt');
incrementer(): void {
const n = this.compteur.get() + 1;
this.compteur.set(n);
this.message.set(n >= 10 ? 'Objectif atteint !' : `Plus que ${10 - n} clics`);
}
reinitialiser(): void {
this.compteur.set(0);
this.message.set('Réinitialisé');
}
}
view/CompteurView.ts — S’abonne, ne tire jamais
// VIEW — s'abonne aux Observables, aucune logique métierclassCompteurView {
private vm = newCompteurViewModel();
constructor() {
const affichageCompteur = document.getElementById('compteur')!;
const affichageMessage = document.getElementById('message')!;
// BINDING — l'UI se met à jour automatiquement à chaque changementthis.vm.compteur.abonner(val => affichageCompteur.textContent = String(val));
this.vm.message.abonner(msg => affichageMessage.textContent = msg);
// Events → délégués au ViewModel (commandes)
document.getElementById('btn-plus')!
.addEventListener('click', () => this.vm.incrementer());
document.getElementById('btn-reset')!
.addEventListener('click', () => this.vm.reinitialiser());
}
}
newCompteurView();
Version React / TypeScript — Dans React, le hook useState + useEffect joue le rôle d’Observable. Le custom hook est le ViewModel.
// VIEW — zéro logique métier, observe et réagit// Elle NE SAIT PAS comment les données arriventconstArticlesView: React.FC = () => {
const { articles, chargement, erreur, supprimer } = useArticlesViewModel();
if (chargement) return <Spinner />;
if (erreur) return <Erreur texte={erreur} />;
return (
<ul>
{articles.map(article => (
<li key={article.id}>
<strong>{article.titre}</strong> — {article.auteur}
<button onClick={() => supprimer(article.id)}>Supprimer</button>
</li>
))}
</ul>
);
};
✓ Points forts
View totalement passive — aucun appel explicite
Testabilité maximale du ViewModel sans UI
Parfait pour les UIs réactives et complexes
React, Angular, SwiftUI, Jetpack Compose
✗ Points faibles
Courbe d'apprentissage du data binding
Overhead pour les UIs simples (overkill)
Gestion de la mémoire (désabonnement)
Debugging parfois difficile avec le binding
05
La différence fondamentale en 3 lignes
// MVC — le Controller POUSSE les données à la View
view.render(model.getData());
// MVP — le Presenter DIT à la View quoi afficher (via interface)this.view.afficherArticles(articles); // ← appel explicite// MVVM — la View S'ABONNE et réagit toute seule
viewModel.articles.abonner(articles => this.render(articles));
// ^^^^^^^^^^^^^^^^^^ Binding automatique — zéro appel explicite
06
Comparatif synthétique
Critère
MVC
MVP
MVVM
Couplage View / Logique
Moyen
Faible
Très faible
Testabilité
Moyenne
Élevée
Très élevée
Complexité
Faible
Moyenne
Élevée
Relation Présentateur / View
1-N
1-1 stricte
Aucune
Réactivité UI
Manuelle
Manuelle
Automatique
View passive ?
Non
Oui
Totalement
Contexte idéal
Web SSR Express, Rails, Django
Mobile legacy Android, iOS UIKit
UI réactive React, Angular, Compose
Boilerplate
Faible
Élevé (interfaces)
Moyen
07
Quel pattern choisir ?
Selon Robert C. Martin, l’architecture doit servir le cas d’usage, pas l’ego du développeur. Ces patterns sont des détails — choisir le bon dépend du contexte, du framework imposé, et des contraintes de testabilité.
🌐
Choisir MVC quand…
Application web SSR
Application web serveur-side, équipe junior, besoin de rapidité. Le framework impose souvent le pattern. Rendu HTML côté serveur sans état UI complexe.
ExpressNestJSRailsDjangoLaravel
« Utilise MVC quand la Vue n’a pas besoin de mémoire. »
📱
Choisir MVP quand…
Mobile & testabilité critique
Application mobile, legacy code à tester, contexte où le binding automatique n'existe pas. La testabilité du Presenter sans framework est l'avantage décisif.
Android legacyiOS UIKitTests unitaires
« Utilise MVP quand tu dois tester la logique sans l’UI. »
⚡
Choisir MVVM quand…
UI réactive et complexe
UI avec état partagé entre composants, framework moderne, besoins de réactivité. Le Data Binding est un avantage structurel, pas un luxe.
ReactAngularJetpack ComposeSwiftUI
« Utilise MVVM quand l’UI doit réagir à l’état, pas l’inverse. »
Rappel Uncle Bob — Clean Architecture : Ces trois patterns sont des détails d’implémentation de la couche de présentation. Dans une Clean Architecture, ils sont relégués au rang de “Plugin UI”. Le domaine métier — Use Cases et Entities — ne doit jamais en dépendre. Le vrai découplage, c’est que le Model ignore totalement quel pattern orchestre son affichage.