Il pattern Model-View-Controller (MVC) è un modo di organizzare il codice che separa in modo netto la logica di business, la presentazione e la gestione degli input. Non è legato a un linguaggio specifico, quindi può essere applicato anche in Python, sia per applicazioni a riga di comando, sia per interfacce grafiche o applicazioni web.
1. Cos è il pattern MVC
MVC suddivide l applicazione in tre componenti principali:
- Model: gestisce i dati e la logica di business. Non conosce nulla di come i dati verranno mostrati all utente.
- View: si occupa solo di mostrare informazioni all utente (console, HTML, GUI, API). Non contiene logica di business, al massimo un po di logica di presentazione.
- Controller: riceve gli input dell utente, coordina Model e View, decide cosa fare e quali viste mostrare.
Separando questi ruoli si ottengono codice più pulito, più facile da testare e più semplice da estendere.
2. Strutturare un progetto MVC in Python
Una possibile struttura di cartelle per un piccolo progetto MVC in Python potrebbe essere:
mvc_todo/
models/
__init__.py
todo.py
views/
__init__.py
console_view.py
controllers/
__init__.py
todo_controller.py
main.py
Non esiste un unica struttura corretta, ma separare fisicamente i file in base al loro ruolo aiuta a mantenere l ordine man mano che l applicazione cresce.
3. Un esempio completo: gestore di TODO a riga di comando
Vediamo un esempio completo di una piccola applicazione a riga di comando che gestisce una lista di attività (TODO). L obiettivo è mostrare concretamente come suddividere Model, View e Controller.
3.1 Il Model: gestione dei dati
Il Model rappresenta le attività e la logica per aggiungerle, rimuoverle, segnarle come completate. Per semplicità useremo una lista in memoria, ma lo stesso concetto si applica se i dati vengono salvati in un file o in un database.
# file: models/todo.py
from dataclasses import dataclass, field
from typing import List
@dataclass
class TodoItem:
id: int
descrizione: str
completato: bool = False
class TodoModel:
def __init__(self) -> None:
self._items: List[TodoItem] = []
self._next_id: int = 1
def aggiungi(self, descrizione: str) -> TodoItem:
item = TodoItem(id=self._next_id, descrizione=descrizione)
self._items.append(item)
self._next_id += 1
return item
def lista(self) -> List[TodoItem]:
return list(self._items)
def completa(self, item_id: int) -> bool:
for item in self._items:
if item.id == item_id:
item.completato = True
return True
return False
def rimuovi(self, item_id: int) -> bool:
for i, item in enumerate(self._items):
if item.id == item_id:
del self._items[i]
return True
return False
Il Model non stampa nulla e non legge input: espone solo metodi per gestire i dati.
3.2 La View: interfaccia a riga di comando
La View si occupa di mostrare informazioni all utente e di raccogliere input, ma non decide cosa fare con quei dati. In questo esempio realizziamo una semplice view che lavora su console.
# file: views/console_view.py
from typing import Iterable
from models.todo import TodoItem
class ConsoleView:
def mostra_menu(self) -> None:
print("\n--- Gestore TODO (MVC) ---")
print("1) Elenca attività")
print("2) Aggiungi attività")
print("3) Completa attività")
print("4) Rimuovi attività")
print("0) Esci")
def chiedi_scelta(self) -> str:
return input("Scegli un opzione: ").strip()
def chiedi_descrizione(self) -> str:
return input("Descrizione attività: ").strip()
def chiedi_id(self) -> int:
raw = input("ID attività: ").strip()
try:
return int(raw)
except ValueError:
print("ID non valido, uso -1.")
return -1
def mostra_lista(self, items: Iterable[TodoItem]) -> None:
print("\n--- Elenco attività ---")
if not list(items):
print("Nessuna attività.")
return
for item in items:
stato = "[x]" if item.completato else "[ ]"
print(f"{item.id:3d} {stato} {item.descrizione}")
def mostra_messaggio(self, messaggio: str) -> None:
print(messaggio)
Anche la View non conosce la logica di business: non sa come i dati vengono memorizzati, si limita a stampare e leggere.
3.3 Il Controller: coordina Model e View
Il Controller riceve input (dalla View), chiama il Model per manipolare i dati e chiede alla View di mostrare i risultati.
# file: controllers/todo_controller.py
from models.todo import TodoModel
from views.console_view import ConsoleView
class TodoController:
def __init__(self, model: TodoModel, view: ConsoleView) -> None:
self.model = model
self.view = view
self._running = True
def esegui(self) -> None:
while self._running:
self.view.mostra_menu()
scelta = self.view.chiedi_scelta()
self._gestisci_scelta(scelta)
def _gestisci_scelta(self, scelta: str) -> None:
if scelta == "1":
self._elenca()
elif scelta == "2":
self._aggiungi()
elif scelta == "3":
self._completa()
elif scelta == "4":
self._rimuovi()
elif scelta == "0":
self._running = False
self.view.mostra_messaggio("Arrivederci")
else:
self.view.mostra_messaggio("Scelta non valida.")
def _elenca(self) -> None:
items = self.model.lista()
self.view.mostra_lista(items)
def _aggiungi(self) -> None:
descrizione = self.view.chiedi_descrizione()
if not descrizione:
self.view.mostra_messaggio("Descrizione vuota, attività non creata.")
return
item = self.model.aggiungi(descrizione)
self.view.mostra_messaggio(f"Attività creata con ID {item.id}.")
def _completa(self) -> None:
item_id = self.view.chiedi_id()
if item_id < 0:
return
if self.model.completa(item_id):
self.view.mostra_messaggio("Attività completata.")
else:
self.view.mostra_messaggio("Attività non trovata.")
def _rimuovi(self) -> None:
item_id = self.view.chiedi_id()
if item_id < 0:
return
if self.model.rimuovi(item_id):
self.view.mostra_messaggio("Attività rimossa.")
else:
self.view.mostra_messaggio("Attività non trovata.")
Notare come il Controller sia l unico punto in cui vengono prese decisioni sull ordine delle operazioni e sul flusso dell applicazione.
3.4 Il punto di ingresso dell applicazione
Infine, il file main.py si limita a creare le istanze e avviare il Controller.
# file: main.py
from models.todo import TodoModel
from views.console_view import ConsoleView
from controllers.todo_controller import TodoController
def main() -> None:
model = TodoModel()
view = ConsoleView()
controller = TodoController(model, view)
controller.esegui()
if __name__ == "__main__":
main()
4. Adattare MVC a diversi tipi di applicazione
Lo stesso schema si applica a molti contesti:
- Applicazioni web: il Model può essere uno strato di accesso al database; le View sono template HTML; il Controller sono le funzioni che gestiscono le richieste HTTP.
- GUI desktop: il Model gestisce lo stato dell applicazione; le View sono le finestre e i widget; il Controller sono i gestori degli eventi (click, input).
- Script complessi: anche negli script a riga di comando è utile separare logica di business e presentazione per evitare file unici difficili da mantenere.
Molti framework Python incorporano varianti di MVC (ad esempio nei web framework spesso si parla di Model Template View o Model View Template), ma l idea di base resta la stessa: separare responsabilità per ottenere un codice più chiaro.
5. Buone pratiche nell uso di MVC in Python
- Mantieni il Model completamente indipendente dal modo in cui presenti i dati. In questo modo potrai riutilizzarlo con viste diverse (console, web, GUI).
- Mantieni la View il più possibile priva di logica di business: deve solo mostrare e raccogliere dati.
- Lascia al Controller il compito di orchestrare le operazioni e di validare la maggior parte degli input.
- Usa test automatici soprattutto per il Model (più facile da testare perché non dipende dall interfaccia).
- Se il progetto cresce, valuta di introdurre altri pattern (Repository, Service, ecc.) sopra o accanto al Model.
6. Conclusioni
Il pattern MVC in Python non richiede librerie particolari: è una disciplina di progettazione. Applicarlo anche in piccoli progetti aiuta a scrivere codice più leggibile, testabile e pronto a crescere nel tempo. L esempio del gestore TODO a riga di comando è solo un punto di partenza: puoi estenderlo aggiungendo il salvataggio su file, una interfaccia web o una GUI, mantenendo sempre separate le responsabilità tra Model, View e Controller.