Come usare il pattern MVC in Java

Il pattern MVC (Model–View–Controller) è uno dei modi più diffusi per organizzare il codice nelle applicazioni Java. Separare i ruoli di modello, vista e controller rende il software più manutenibile, testabile ed estendibile.

1. Che cos'è il pattern MVC

MVC separa l'applicazione in tre componenti principali:

  • Model: rappresenta i dati e la logica di business (regole, validazioni, persistenza).
  • View: si occupa di mostrare i dati all'utente (UI grafica, HTML, console, ecc.).
  • Controller: riceve gli input dell'utente, aggiorna il Model e decide quale View mostrare.

L'idea chiave è che la View non conosce come i dati vengono gestiti a basso livello, e il Model non sa nulla di come verrà visualizzato: il Controller fa da ponte tra i due.

2. Vantaggi dell'uso di MVC in Java

  • Separazione delle responsabilità: ogni classe ha un compito chiaro.
  • Test facilitato: è più semplice testare Model e Controller separatamente.
  • Riutilizzo delle View: puoi cambiare interfaccia (es. da Swing a JavaFX o web) senza riscrivere il Model.
  • Manutenzione più semplice: il codice è meno accoppiato e più leggibile.

3. Strutturare un progetto Java in MVC

Una struttura tipica di un progetto Java desktop basato su MVC potrebbe essere:


src/
  model/
    ContoBancario.java
  view/
    ContoView.java
  controller/
    ContoController.java
  App.java
  

In questo esempio svilupperemo una piccola applicazione di conto bancario con saldo, prelievo e deposito.

4. Implementare il Model

Il Model contiene lo stato dell'applicazione e la logica di business. Non ha riferimenti alla View.


package model;

public class ContoBancario {

    private double saldo;

    public ContoBancario(double saldoIniziale) {
        this.saldo = saldoIniziale;
    }

    public double getSaldo() {
        return saldo;
    }

    public void deposita(double importo) {
        if (importo <= 0) {
            throw new IllegalArgumentException("L'importo deve essere positivo");
        }
        saldo += importo;
    }

    public void preleva(double importo) {
        if (importo <= 0) {
            throw new IllegalArgumentException("L'importo deve essere positivo");
        }
        if (importo > saldo) {
            throw new IllegalArgumentException("Saldo insufficiente");
        }
        saldo -= importo;
    }
}
  

Punti chiave:

  • Niente riferimenti a Swing, JavaFX o alla console.
  • Espone metodi per le operazioni di business (deposita, preleva).
  • Gestisce le validazioni (saldo sufficiente, importi positivi).

5. Implementare la View (esempio con Swing)

La View è responsabile solo dell'interfaccia utente. Espone metodi per:

  • mostrare/leggere dati dalla UI;
  • registrare listener di eventi che il Controller potrà usare.

package view;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
import javax.swing.JPanel;
import javax.swing.JOptionPane;
import java.awt.GridLayout;
import java.awt.event.ActionListener;

public class ContoView extends JFrame {

    private final JLabel saldoLabel = new JLabel("Saldo: 0.0");
    private final JTextField importoField = new JTextField();
    private final JButton depositaButton = new JButton("Deposita");
    private final JButton prelevaButton = new JButton("Preleva");

    public ContoView() {
        super("Conto Bancario - MVC");

        JPanel panel = new JPanel(new GridLayout(4, 2, 5, 5));

        panel.add(new JLabel("Importo:"));
        panel.add(importoField);
        panel.add(depositaButton);
        panel.add(prelevaButton);
        panel.add(saldoLabel);

        setContentPane(panel);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        pack();
        setLocationRelativeTo(null);
    }

    public String getImporto() {
        return importoField.getText();
    }

    public void setSaldo(double saldo) {
        saldoLabel.setText("Saldo: " + saldo);
    }

    public void mostraErrore(String messaggio) {
        JOptionPane.showMessageDialog(this, messaggio, "Errore", JOptionPane.ERROR_MESSAGE);
    }

    public void pulisciCampoImporto() {
        importoField.setText("");
    }

    public void aggiungiListenerDeposita(ActionListener listener) {
        depositaButton.addActionListener(listener);
    }

    public void aggiungiListenerPreleva(ActionListener listener) {
        prelevaButton.addActionListener(listener);
    }
}
  

Punti chiave:

  • La View non conosce il Model.
  • Offre metodi per leggere l'input e aggiornare la UI.
  • Espone metodi per registrare listener, ma non implementa la logica dei pulsanti.

6. Implementare il Controller

Il Controller collega Model e View: gestisce gli eventi dell'interfaccia, chiama i metodi del Model e aggiorna la View.


package controller;

import model.ContoBancario;
import view.ContoView;

public class ContoController {

    private final ContoBancario model;
    private final ContoView view;

    public ContoController(ContoBancario model, ContoView view) {
        this.model = model;
        this.view = view;

        this.view.setSaldo(model.getSaldo());

        this.view.aggiungiListenerDeposita(e -> deposita());
        this.view.aggiungiListenerPreleva(e -> preleva());
    }

    private void deposita() {
        try {
            double importo = Double.parseDouble(view.getImporto());
            model.deposita(importo);
            view.setSaldo(model.getSaldo());
            view.pulisciCampoImporto();
        } catch (NumberFormatException ex) {
            view.mostraErrore("Inserisci un numero valido");
        } catch (IllegalArgumentException ex) {
            view.mostraErrore(ex.getMessage());
        }
    }

    private void preleva() {
        try {
            double importo = Double.parseDouble(view.getImporto());
            model.preleva(importo);
            view.setSaldo(model.getSaldo());
            view.pulisciCampoImporto();
        } catch (NumberFormatException ex) {
            view.mostraErrore("Inserisci un numero valido");
        } catch (IllegalArgumentException ex) {
            view.mostraErrore(ex.getMessage());
        }
    }
}
  

Punti chiave:

  • Il Controller riceve riferimenti a Model e View nel costruttore.
  • Registra i listener sulla View e implementa la logica nei metodi deposita e preleva.
  • Traduce gli input utente in chiamate verso il Model e aggiorna la View in base al nuovo stato.

7. Classe di bootstrap dell'applicazione

Infine, creiamo la classe con il metodo main che inizializza Model, View e Controller.


import javax.swing.SwingUtilities;

import model.ContoBancario;
import view.ContoView;
import controller.ContoController;

public class App {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            ContoBancario model = new ContoBancario(1000.0);
            ContoView view = new ContoView();
            new ContoController(model, view);
            view.setVisible(true);
        });
    }
}
  

In questo modo, l'applicazione parte creando le tre componenti principali e mostrando la finestra definita dalla View.

8. Suggerimenti pratici per usare MVC in Java

8.1 Non mettere logica di business nella View

Evita di scrivere codice come calcoli, validazioni complesse o accessi al database direttamente nei listener della UI. Quella logica appartiene al Model o al Controller.

8.2 Mantieni il Controller sottile

Se il Controller diventa troppo grande, valuta se spostare parte della logica in servizi separati o nel Model stesso, mantenendo il Controller come coordinatore.

8.3 Usa interfacce per disaccoppiare

Puoi definire interfacce per la View (es. ContoViewInterface) e far sì che la classe concreta Swing le implementi. In questo modo puoi sostituire facilmente la View con una diversa implementazione (per esempio una console o una web app).


public interface ContoViewInterface {
    String getImporto();
    void setSaldo(double saldo);
    void mostraErrore(String messaggio);
    void pulisciCampoImporto();
    void aggiungiListenerDeposita(java.awt.event.ActionListener listener);
    void aggiungiListenerPreleva(java.awt.event.ActionListener listener);
}
  

Il Controller dipenderà dall'interfaccia, non dalla classe concreta della View, riducendo l'accoppiamento.

9. MVC nel mondo Java web

Lo stesso concetto di MVC si applica anche alle applicazioni web Java:

  • Model: classi di dominio, servizi, DAO, entità JPA.
  • View: JSP, HTML/Thymeleaf, pagine JSF, template.
  • Controller: Servlet, controller Spring MVC, backing beans.

Un controller Spring MVC tipico potrebbe essere:


@Controller
@RequestMapping("/conti")
public class ContoWebController {

    private final ContoService contoService;

    public ContoWebController(ContoService contoService) {
        this.contoService = contoService;
    }

    @GetMapping("/{id}")
    public String mostraConto(@PathVariable Long id, Model model) {
        Conto conto = contoService.trovaPerId(id);
        model.addAttribute("conto", conto);
        return "dettaglioConto";
    }

    @PostMapping("/{id}/deposita")
    public String deposita(@PathVariable Long id,
                           @RequestParam double importo) {
        contoService.deposita(id, importo);
        return "redirect:/conti/" + id;
    }
}
  

Anche qui ritroviamo i tre ruoli: il Model è la logica di business, la View è la pagina HTML/TEMPLATE, e il Controller riceve le richieste HTTP, chiama il servizio e decide quale pagina restituire.

10. Conclusione

Usare il pattern MVC in Java ti permette di scrivere applicazioni più pulite, modulari e testabili. Separando Model, View e Controller:

  • rendi più chiaro il flusso dei dati;
  • semplifichi l'evoluzione dell'interfaccia utente;
  • agevoli il riuso e il testing delle componenti di business.

Partendo dall'esempio di base presentato qui, puoi estendere l'architettura aggiungendo servizi, repository, gestione degli eventi più avanzata e integrazione con i principali framework Java (come Spring, JavaFX, Jakarta EE), mantenendo sempre al centro la separazione delle responsabilità.

Torna su