Chapitre 12
12 — Architectures MVC, MVVM, MVP, Flux & Clean Architecture
12 — Architectures MVC, MVVM, MVP, Flux & Clean Architecture
Chapitre 12 : Architectures MVC, MVVM, MVP, Flux & Clean Architecture
Durée estimée : 5 séances de 3h Objectifs : Comprendre l'évolution des architectures UI, savoir choisir entre MVC/MVVM/MVP/Flux, maîtriser Clean Architecture et Hexagonal Architecture.
Partie 1 : Histoire et Évolution
1.1 Les origines (1979)
Le pattern MVC a été introduit par Trygve Reenskaug chez Xerox PARC pour le langage Smalltalk-80. L'idée révolutionnaire était de séparer :
- Model : Les données et la logique métier
- View : La représentation visuelle
- Controller : La gestion des entrées utilisateur
Diagramme en cours de génération...
1.2 Le problème fondamental
Séparation des préoccupations (SoC) : Comment organiser le code d'une interface utilisateur pour que :
- La logique métier soit indépendante de l'UI
- Les changements UI n'affectent pas la logique métier
- Le code soit testable
- L'équipe puisse travailler en parallèle
Partie 2 : MVC — Model-View-Controller
2.1 Structure
Diagramme en cours de génération...
2.2 Implémentation classique
// Model
class TodoModel {
private todos: Array<{ id: number; text: string; completed: boolean }> = [];
private listeners: Array<() => void> = [];
add(text: string): void {
this.todos.push({ id: Date.now(), text, completed: false });
this.notify();
}
toggle(id: number): void {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
this.notify();
}
getAll() { return [...this.todos]; }
subscribe(listener: () => void): () => void {
this.listeners.push(listener);
return () => { this.listeners = this.listeners.filter(l => l !== listener); };
}
private notify(): void {
this.listeners.forEach(l => l());
}
}
// View
class TodoView {
private app: HTMLElement;
private input: HTMLInputElement;
private list: HTMLElement;
private addButton: HTMLElement;
constructor() {
this.app = document.getElementById('app')!;
this.app.innerHTML = `
<input id="todo-input" type="text" />
<button id="add-btn">Add</button>
<ul id="todo-list"></ul>
`;
this.input = document.getElementById('todo-input') as HTMLInputElement;
this.list = document.getElementById('todo-list')!;
this.addButton = document.getElementById('add-btn')!;
}
render(todos: Array<{ id: number; text: string; completed: boolean }>): void {
this.list.innerHTML = todos.map(todo => `
<li class="${todo.completed ? 'completed' : ''}" data-id="${todo.id}">
<span>${todo.text}</span>
<button class="toggle-btn">Toggle</button>
</li>
`).join('');
}
getInputValue(): string { return this.input.value; }
clearInput(): void { this.input.value = ''; }
bindAddTodo(handler: () => void): void {
this.addButton.addEventListener('click', handler);
this.input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') handler();
});
}
bindToggleTodo(handler: (id: number) => void): void {
this.list.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
if (target.classList.contains('toggle-btn')) {
const id = Number(target.closest('li')!.dataset.id);
handler(id);
}
});
}
}
// Controller
class TodoController {
constructor(
private model: TodoModel,
private view: TodoView
) {
this.view.bindAddTodo(this.handleAddTodo.bind(this));
this.view.bindToggleTodo(this.handleToggleTodo.bind(this));
this.model.subscribe(() => this.view.render(this.model.getAll()));
this.view.render(this.model.getAll());
}
private handleAddTodo(): void {
const text = this.view.getInputValue().trim();
if (text) {
this.model.add(text);
this.view.clearInput();
}
}
private handleToggleTodo(id: number): void {
this.model.toggle(id);
}
}
// Bootstrap
const model = new TodoModel();
const view = new TodoView();
const controller = new TodoController(model, view);
2.3 Variantes du MVC
| Variante | Flux | Utilisation |
|---|---|---|
| Classic MVC | Model notify View | Smalltalk, iOS |
| MVC Web | Request→Controller→Model→View | Spring MVC, Rails, Laravel |
| MVC JavaScript | Model→View, Controller gère events | Backbone.js |
Partie 3 : MVP — Model-View-Presenter
3.1 Structure
Diagramme en cours de génération...
3.2 Implémentation
interface UserView {
showUsers(users: User[]): void;
showError(message: string): void;
showLoading(): void;
hideLoading(): void;
getUserSearchTerm(): string;
}
class UserPresenter {
constructor(
private model: UserModel,
private view: UserView
) {}
async onSearch(): Promise<void> {
const term = this.view.getUserSearchTerm();
if (!term) {
this.view.showError('Please enter a search term');
return;
}
this.view.showLoading();
try {
const users = await this.model.search(term);
this.view.showUsers(users);
} catch (err) {
this.view.showError('Failed to search users');
} finally {
this.view.hideLoading();
}
}
}
Partie 4 : MVVM — Model-View-ViewModel
4.1 Structure
Le MVVM a été introduit par John Gossman (Microsoft) pour WPF/Silverlight. Il se base sur le data binding bidirectionnel.
Diagramme en cours de génération...
4.2 Implémentation avec Vue.js
// Model (TypeScript)
interface User {
id: number;
name: string;
email: string;
}
// ViewModel (Vue.js Composition API)
import { ref, computed, onMounted } from 'vue';
export function useUserViewModel() {
const users = ref<User[]>([]);
const searchTerm = ref('');
const loading = ref(false);
const error = ref<string | null>(null);
const filteredUsers = computed(() =>
users.value.filter(u =>
u.name.toLowerCase().includes(searchTerm.value.toLowerCase())
)
);
async function loadUsers() {
loading.value = true;
error.value = null;
try {
const response = await fetch('/api/users');
users.value = await response.json();
} catch (e) {
error.value = 'Failed to load users';
} finally {
loading.value = false;
}
}
onMounted(loadUsers);
return { users: filteredUsers, searchTerm, loading, error, loadUsers };
}
// View (Vue SFC)
<template>
<div>
<input v-model="searchTerm" placeholder="Search..." />
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error }}</div>
<ul v-else>
<li v-for="user in users" :key="user.id">
{{ user.name }} - {{ user.email }}
</li>
</ul>
</div>
</template>
Partie 5 : Flux / Redux — Unidirectional Data Flow
5.1 Problème résolu
Facebook a créé Flux suite aux problèmes de MVC pour les applications complexes (messenger, notifications). Le problème principal : les vues pouvaient modifier le modèle qui notifiait d'autres vues → cascade incontrôlable.
5.2 Flux
Diagramme en cours de génération...
5.3 Redux
Redux simplifie Flux avec un seul store et des reducers purs :
import { createStore } from 'redux';
// State
interface AppState {
todos: Todo[];
filter: FilterType;
}
// Action
type Action =
| { type: 'ADD_TODO'; payload: string }
| { type: 'TOGGLE_TODO'; payload: number }
| { type: 'SET_FILTER'; payload: FilterType };
// Reducer (pure function)
function todoReducer(state: AppState, action: Action): AppState {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, {
id: Date.now(),
text: action.payload,
completed: false
}]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
default:
return state;
}
}
// Store
const store = createStore(todoReducer, { todos: [], filter: 'ALL' });
// Dispatch
store.dispatch({ type: 'ADD_TODO', payload: 'Learn Redux' });
// Subscribe (Observer pattern)
store.subscribe(() => console.log(store.getState()));
5.4 Avantages du flux unidirectionnel
- Prévisible : Une seule direction de flux de données
- Débogable : Time-travel debugging, log d'actions
- Testable : Les reducers sont des fonctions pures
- Maintenable : Séparation claire action→reducer→state
Partie 6 : Hexagonal Architecture (Ports & Adapters)
6.1 Principe
L'Hexagonal Architecture (Alistair Cockburn) place la logique métier au centre, avec des ports (interfaces) et des adaptateurs (implémentations).
Diagramme en cours de génération...
6.2 Code
// Domain Entity
class Order {
constructor(
public id: string,
public items: OrderItem[],
public status: OrderStatus
) {}
getTotal(): number {
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
}
// Port — Repository Interface
interface OrderRepository {
findById(id: string): Promise<Order | null>;
save(order: Order): Promise<void>;
}
// Port — Presenter Interface
interface OrderPresenter {
showOrder(order: Order): void;
showError(message: string): void;
}
// Use Case
class GetOrderUseCase {
constructor(
private repository: OrderRepository,
private presenter: OrderPresenter
) {}
async execute(orderId: string): Promise<void> {
try {
const order = await this.repository.findById(orderId);
if (!order) {
this.presenter.showError('Order not found');
return;
}
this.presenter.showOrder(order);
} catch (err) {
this.presenter.showError('Failed to retrieve order');
}
}
}
// Adapter — Database
class PostgresOrderRepository implements OrderRepository {
async findById(id: string): Promise<Order | null> {
const result = await db.query('SELECT * FROM orders WHERE id = $1', [id]);
return result.rows[0] ? this.toDomain(result.rows[0]) : null;
}
async save(order: Order): Promise<void> {
await db.query('INSERT INTO orders ...', [/* ... */]);
}
private toDomain(row: any): Order {
return new Order(row.id, row.items, row.status);
}
}
Partie 7 : Clean Architecture (Robert C. Martin)
7.1 Principe
La Clean Architecture organise le code en cercles concentriques. La règle d'or : les dépendances vont de l'extérieur vers l'intérieur.
Diagramme en cours de génération...
7.2 Règles de dépendance
- Le cercle intérieur ne sait rien du cercle extérieur
- Les noms dans le cercle intérieur ne doivent pas être mentionnés à l'extérieur
- Les Use Cases orchestrent le flux de données
- Les Entities encapsulent les règles métier critiques
7.3 Implémentation
// === Circle 4: Domain ===
class User {
constructor(
public id: string,
public email: Email,
public name: string
) {}
changeEmail(newEmail: Email): void {
this.email = newEmail;
// Domain validation
}
}
// Value Object
class Email {
private constructor(public readonly value: string) {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
throw new Error('Invalid email');
}
}
static create(value: string): Email {
return new Email(value);
}
}
// === Circle 3: Use Cases ===
interface RegisterUserInput {
email: string;
name: string;
password: string;
}
interface RegisterUserOutput {
userId: string;
success: boolean;
}
class RegisterUserUseCase {
constructor(
private userRepository: UserRepository,
private emailService: EmailService
) {}
async execute(input: RegisterUserInput): Promise<RegisterUserOutput> {
const email = Email.create(input.email);
const existing = await this.userRepository.findByEmail(email);
if (existing) {
throw new Error('Email already registered');
}
const user = new User(
generateId(),
email,
input.name
);
await this.userRepository.save(user);
await this.emailService.sendWelcomeEmail(user);
return { userId: user.id, success: true };
}
}
// === Circle 2: Adapters ===
interface UserRepository {
findByEmail(email: Email): Promise<User | null>;
save(user: User): Promise<void>;
}
interface EmailService {
sendWelcomeEmail(user: User): Promise<void>;
}
// Controller (adapter)
class RegisterUserController {
constructor(private useCase: RegisterUserUseCase) {}
async handle(request: HttpRequest): Promise<HttpResponse> {
try {
const result = await this.useCase.execute({
email: request.body.email,
name: request.body.name,
password: request.body.password
});
return { statusCode: 201, body: result };
} catch (err) {
return { statusCode: 400, body: { error: err.message } };
}
}
}
Partie 8 : Comparaison et Choix
8.1 Tableau comparatif
| Critère | MVC | MVP | MVVM | Flux/Redux | Clean Arch |
|---|---|---|---|---|---|
| Data Binding | Manuel | Manuel | Automatique | Manuel | Manuel |
| Testabilité | Moyenne | Haute | Haute | Haute | Très haute |
| Complexité | Faible | Moyenne | Moyenne | Haute | Haute |
| Flow | Bidirectionnel | Unidirectionnel | Bidirectionnel | Unidirectionnel | Dépend du use case |
| Quand choisir | Petites apps | Apps testables | Apps riches (WPF, Vue) | Apps complexes | Apps enterprise |
8.2 Guide de sélection
Petite app simple → MVC
App testable → MVP
App riche en bindings → MVVM
App complexe / state global → Redux
App enterprise → Clean / Hexagonal
Résumé
- MVC : Le classique, simple mais limité pour les apps complexes
- MVP : Haute testabilité, présentateur sans référence à l'UI
- MVVM : Data binding automatique, idéal pour les frameworks modernes
- Flux/Redux : Flux unidirectionnel, state management prévisible
- Hexagonal/Clean : Indépendance framework/DB, testabilité maximale
Prochain chapitre : Architectures Microservices, Event-Driven, CQRS.