Architecture logicielle

MVVM
Model · View · ViewModel

L'architecture des interfaces réactives modernes — Vue, React, Angular. Le ViewModel est le pont intelligent entre les données et l'interface, éliminant le besoin de manipuler le DOM manuellement.

C'est quoi MVVM ?

MVVM remplace le Controller de MVC par un ViewModel — une couche intermédiaire qui expose des données réactives. Quand les données changent, l'interface se met à jour automatiquement, sans code manuel.

📊
Analogie — un tableau de bord de voiture :
Le Model = le moteur (données réelles : vitesse, carburant).
Le ViewModel = le boîtier électronique (transforme les signaux bruts en valeurs lisibles).
La View = le tableau de bord (affiche les jauges — se met à jour seul quand les valeurs changent).
🔑
Le concept clé : réactivité
En MVC, le Controller dit manuellement à la View quoi afficher. En MVVM, la View observe le ViewModel — dès qu'une valeur change, l'UI se met à jour. Plus besoin d'écrire document.getElementById(...).textContent = ....
🗄️
Model
Données brutes, API, BDD
fetch / update
🧠
ViewModel
Données réactives, logique de présentation
data binding
🖥️
View
Template HTML, composants

Data Binding — synchronisation automatique

vm.titre = "Bonjour" → auto → <h1>Bonjour</h1>
vm.articles = [...] → auto → <li> × 3</li>
vm.compteur++ → auto → <span>1</span>
← event ← click ← <button>+1</button>

MVVM vs MVC — la différence concrète

❌ MVC — mise à jour manuelle du DOM

JavaScript vanilla — MVC-like
// Controller — doit mettre à jour le DOM manuellement
let compteur = 0;

function incrementer() {
  compteur++;
  // Mettre à jour MANUELLEMENT le DOM
  document.getElementById('affichage').textContent = compteur;
  document.getElementById('double').textContent = compteur * 2;
  document.getElementById('btn').disabled = compteur >= 10;
  // ... oublier une mise à jour = bug difficile à trouver
}

<!-- HTML -->
<span id="affichage">0</span>
<span id="double">0</span>
<button id="btn" onclick="incrementer()">+1</button>

✅ MVVM — mise à jour automatique (Vue.js)

Vue 3 — réactivité automatique
// ViewModel — exposer des données réactives
// <script setup>
import { ref, computed } from 'vue';

const compteur = ref(0);               // donnée réactive
const double   = computed(() => compteur.value * 2); // dérivée auto
const maxAtteint = computed(() => compteur.value >= 10);

function incrementer() {
  compteur.value++;
  // C'est tout ! La View se met à jour seule.
}

<!-- View — template Vue -->
<span>{{ compteur }}</span>
<span>{{ double }}</span>
<button @click="incrementer" :disabled="maxAtteint">+1</button>
💡

En Vue, ref() crée une valeur réactive. computed() crée une valeur dérivée qui se recalcule automatiquement. Le template se lie à ces valeurs — quand elles changent, le DOM se met à jour.

Le data binding — types et syntaxe

Vue.js

Types de binding Vue 3
<!-- 1. Interpolation — ViewModel → View (one-way) -->
<p>{{ message }}</p>
<p>{{ prix.toFixed(2) }} €</p>

<!-- 2. Binding d'attribut — : est un raccourci de v-bind -->
<img :src="urlImage" :alt="titre">
<button :disabled="chargement">Envoyer</button>
<div :class="{ actif: estActif, erreur: aErreur }"></div>

<!-- 3. Événements — @ est un raccourci de v-on -->
<button @click="sauvegarder">Sauver</button>
<input @input="rechercherEnTempsReel">
<form @submit.prevent="soumettre">

<!-- 4. Two-way binding — v-model (View ↔ ViewModel) -->
<input v-model="nom">        <!-- saisie → ViewModel auto -->
<textarea v-model="bio"></textarea>
<select v-model="categorie">...</select>
<input type="checkbox" v-model="accepte">

<!-- 5. Directives de structure -->
<p v-if="estConnecte">Bienvenue !</p>
<p v-else>Connectez-vous</p>
<li v-for="article in articles" :key="article.id">
  {{ article.titre }}
</li>

React (équivalent)

Binding React — JSX + useState
import { useState, useMemo } from 'react';

function Formulaire() {
  // ViewModel = state + handlers du composant
  const [nom, setNom]       = useState('');
  const [articles, setArticles] = useState([]);
  const [chargement, setChargement] = useState(false);

  // computed → useMemo en React
  const nomMaj = useMemo(() => nom.toUpperCase(), [nom]);

  return (
    {/* View = JSX */}
    <div>
      {/* two-way binding manuel en React */}
      <input value={nom} onChange={e => setNom(e.target.value)} />
      <p>{nomMaj}</p>
      <button disabled={chargement}>Envoyer</button>
      <ul>
        {articles.map(a => (
          <li key={a.id}>{a.titre}</li>
        ))}
      </ul>
    </div>
  );
}
⚛️

React n'est pas MVVM au sens strict — il n'y a pas de two-way binding automatique (v-model). Le binding est explicite : value={state} + onChange={handler}. Mais le principe est identique : le composant est le ViewModel.

MVVM complet avec Vue.js

ViewModel — composant Vue 3 (Composition API)
<!-- ArticleList.vue -->
<script setup>
import { ref, computed, onMounted } from 'vue';
import { api } from '../services/api';  // ← Model (API)

// --- ViewModel : données réactives ---
const articles    = ref([]);
const recherche   = ref('');
const chargement  = ref(true);
const erreur      = ref(null);

// Données dérivées — recalculées automatiquement
const articlesFiltres = computed(() =>
  articles.value.filter(a =>
    a.titre.toLowerCase().includes(recherche.value.toLowerCase())
  )
);
const nbResultats = computed(() => articlesFiltres.value.length);

// --- Actions ---
async function chargerArticles() {
  try {
    articles.value = await api.get('/articles'); // ← Model
  } catch (e) { erreur.value = e.message; }
  finally    { chargement.value = false; }
}

async function supprimer(id) {
  await api.delete(`/articles/${id}`);
  articles.value = articles.value.filter(a => a.id !== id);
}

onMounted(chargerArticles); // charger au montage
</script>
View — template Vue lié au ViewModel
<template>
  <!-- La View observe le ViewModel -- aucun code JS ici -->

  <div v-if="chargement">Chargement...</div>
  <div v-else-if="erreur">Erreur : {{ erreur }}</div>

  <div v-else>
    <!-- Two-way binding : recherche ↔ input -->
    <input v-model="recherche" placeholder="Filtrer...">
    <p>{{ nbResultats }} résultats</p>

    <!-- Liste réactive -->
    <article
      v-for="article in articlesFiltres"
      :key="article.id"
    >
      <h3>{{ article.titre }}</h3>
      <button @click="supprimer(article.id)">
        Supprimer
      </button>
    </article>
  </div>
</template>
💡

La structure d'un .vue reflète exactement MVVM : <script setup> = ViewModel, <template> = View, les appels API = Model. Chaque fichier est un composant autonome et testable.

MVVM avec React — hooks comme ViewModel

Custom hook = ViewModel réutilisable
// hooks/useArticles.js — ViewModel isolé
import { useState, useEffect, useMemo } from 'react';

export function useArticles() {
  // État réactif (comme ref() de Vue)
  const [articles, setArticles]   = useState([]);
  const [recherche, setRecherche] = useState('');
  const [chargement, setChargement] = useState(true);

  // computed → useMemo
  const articlesFiltres = useMemo(() =>
    articles.filter(a =>
      a.titre.toLowerCase().includes(recherche.toLowerCase())
    ),
    [articles, recherche]
  );

  // onMounted → useEffect
  useEffect(() => {
    fetch('/api/articles')
      .then(r => r.json())
      .then(setArticles)
      .finally(() => setChargement(false));
  }, []);

  const supprimer = (id) => {
    fetch(`/api/articles/${id}`, { method: 'DELETE' });
    setArticles(prev => prev.filter(a => a.id !== id));
  };

  // Exposer au composant View
  return { articles: articlesFiltres, recherche, setRecherche,
             chargement, supprimer };
}
Composant View — consomme le ViewModel
// components/ArticleList.jsx — View pure
import { useArticles } from '../hooks/useArticles';

export function ArticleList() {
  // Le composant est la View — il consomme le ViewModel
  const { articles, recherche, setRecherche,
          chargement, supprimer } = useArticles();

  if (chargement) return <p>Chargement...</p>;

  return (
    <div>
      <input
        value={recherche}
        onChange={e => setRecherche(e.target.value)}
        placeholder="Filtrer..."
      />
      {articles.map(a => (
        <article key={a.id}>
          <h3>{a.titre}</h3>
          <button onClick={() => supprimer(a.id)}>
            Supprimer
          </button>
        </article>
      ))}
    </div>
  );
}
💡

Extraire la logique dans un custom hook est la bonne pratique React pour MVVM : le hook est le ViewModel testable indépendamment, le composant est la View qui ne fait qu'afficher.

MVVM côté Python — contexte

MVVM est un pattern front-end par nature. Côté Python, on le rencontre dans les applications desktop (PyQt, Tkinter) ou en combinaison avec un front Vue/React qui consomme une API Flask/FastAPI.

MVVM desktop — Python + PyQt6
from PyQt6.QtCore import QObject, pyqtSignal, pyqtProperty

# Model — données
class CompteurModel:
    def __init__(self): self.valeur = 0
    def incrementer(self): self.valeur += 1

# ViewModel — expose les données + notifie la View
class CompteurViewModel(QObject):
    valeurChangee = pyqtSignal(int)  # signal réactif

    def __init__(self):
        super().__init__()
        self._model = CompteurModel()

    @pyqtProperty(int, notify=valeurChangee)
    def valeur(self): return self._model.valeur

    def incrementer(self):
        self._model.incrementer()
        self.valeurChangee.emit(self._model.valeur)  # notif View
Pattern full-stack le plus courant avec Python
# FastAPI (Python) = Model + API
# Vue.js / React    = ViewModel + View

# api/main.py — FastAPI expose les données
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Article(BaseModel):
    id: int
    titre: str
    contenu: str

articles_db = [
    Article(id=1, titre="Premier", contenu="...")
]

@app.get("/articles")
async def lister():
    return articles_db         # ← Model → JSON

------------------------------------------
# Vue.js consomme l'API (ViewModel côté front)
# const articles = ref([])
# onMounted(() => fetch('/articles')
#   .then(r => r.json())
#   .then(d => articles.value = d))
💡

La combinaison FastAPI/Flask (back) + Vue/React (front) est un MVVM moderne : Python gère le Model (BDD, logique métier), JavaScript gère le ViewModel et la View. C'est la stack la plus fréquente en 2024.

État global — Pinia (Vue) & Redux (React)

Pinia — store Vue 3 (ViewModel global)
// stores/articlesStore.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

export const useArticlesStore = defineStore('articles', () => {
  // State (équivaut à ref() dans un composant)
  const articles   = ref([]);
  const chargement = ref(false);

  // Getters (computed)
  const publies = computed(() => articles.value.filter(a => a.publie));

  // Actions
  async function charger() {
    chargement.value = true;
    articles.value = await fetch('/api/articles').then(r => r.json());
    chargement.value = false;
  }

  return { articles, chargement, publies, charger };
});

// Dans n'importe quel composant :
const store = useArticlesStore();
store.charger();  // tous les composants voient la même donnée

Quand un ViewModel par composant ne suffit plus — par exemple, partager l'utilisateur connecté entre 10 composants — on utilise un store global (Pinia pour Vue, Redux/Zustand pour React). C'est un ViewModel partagé.

Zustand — store React minimal
import { create } from 'zustand';

const useAuthStore = create(set => ({
  user:      null,
  connecte:  false,

  connecter: (userData) => set({
    user: userData, connecte: true
  }),
  deconnecter: () => set({ user: null, connecte: false }),
}));

// Dans n'importe quel composant :
const { user, connecte, deconnecter } = useAuthStore();
🏪

Store = ViewModel global. Les composants s'abonnent au store — quand les données changent, seuls les composants qui utilisent ces données se re-rendent.

Avantages & limites

Avantages

Réactivité automatiquePlus de manipulations manuelles du DOM
UI déclarativeOn décrit ce qu'on veut afficher, pas comment le mettre à jour
ViewModel testableTester la logique sans navigateur ni DOM
Two-way bindingFormulaires synchronisés sans event listeners manuels
Composants réutilisablesChaque composant = ViewModel + View autonomes
⚠️

Limites

Courbe d'apprentissageref/reactive/computed, effets de bord
OverheadRéactivité = système de tracking coûteux pour de petites apps
Debugging complexeMise à jour automatique = difficile à tracer sans devtools
SEOSPA = contenu généré côté client → invisible pour les bots (sauf SSR)

Cheat sheet MVVM

Vue 3 ↔ Concepts MVVM

ref()Donnée réactive (ViewModel)
computed()Donnée dérivée automatique
reactive()Objet réactif entier
watch()Réagir aux changements
v-modelTwo-way binding
v-bind / :One-way VM → View
v-on / @Event View → VM
onMounted()Charger les données

React ↔ Concepts MVVM

useState()Donnée réactive
useMemo()Donnée dérivée
useEffect()Effets de bord / chargement
useReducer()État complexe
value + onChangeTwo-way binding explicite
custom hookViewModel réutilisable
Zustand / ReduxViewModel global

MVC vs MVVM — résumé

MVC ControllerChoisit la View, injecte données
MVVM ViewModelExpose données réactives
MVC ViewTemplate passif (Jinja, EJS)
MVVM ViewTemplate actif, se bind au VM
MVC fluxController → View (one-way)
MVVM fluxVM ↔ View (two-way possible)

Quand utiliser MVVM ?

SPA (Single Page App)✓ idéal
Interface très interactive✓ idéal
Formulaires complexes✓ idéal
Site vitrine simple✗ overkill
API REST back-end✗ MVC suffit
App desktop Python✓ PyQt/Kivy