1. Conosciamo il pattern «Osservatore»
Il pattern «Osservatore» (Observer) è uno dei pattern di progettazione più noti e fondamentali. Descrive la situazione in cui un oggetto (l’osservato, ovvero il subject) comunica le proprie modifiche ad altri oggetti (gli osservatori, observers) che si sono iscritti a tali cambiamenti.
Detto più semplicemente: abbiamo un canale Telegram (l’oggetto osservato) e ci sono «iscritti» (gli osservatori). Ogni volta che esce un nuovo post, il canale notifica tutti gli iscritti, i quali decidono cosa farne — leggere, ignorare o disiscriversi.
In programmazione, questo pattern consente di notificare automaticamente gli oggetti interessati di eventi o cambiamenti di stato, senza collegarli direttamente tra loro. Questo è importante per costruire sistemi flessibili, estendibili e di facile manutenzione.
Dove si incontra il pattern «Osservatore»?
- Nelle interfacce grafiche (Swing, AWT, JavaFX) — listener di eventi.
- Nelle librerie reattive (RxJava, Project Reactor).
- Nella logica di business: reagire al cambiamento dello stato del modello.
- Nei motori di gioco (eventi di collisione, vittoria, sconfitta, ecc.).
- Ovunque serva separare «che cosa è successo» da «che cosa farne».
Relazione del pattern con eventi e listener in Java
Di fatto, l’intero modello a eventi di Java si basa su «Osservatore». Quando scrivete button.addActionListener(listener);, state applicando questo pattern:
- Osservato — il pulsante (o un altro componente).
- Osservatore — il vostro listener che implementa il metodo actionPerformed().
- Evento — l’utente ha cliccato, ha passato il mouse, ecc.
- Notifica — il componente invoca actionPerformed().
Tutto questo è una realizzazione classica di Observer!
2. Implementazione classica del pattern «Osservatore»
Vediamo come implementare il pattern nelle nostre classi — senza Swing e AWT — per verificare che non ci sia alcuna magia.
Elementi principali del pattern
- Observable (Subject) — l’oggetto osservato. Mantiene un elenco di osservatori e li notifica delle modifiche.
- Observer — l’interfaccia dell’osservatore, in genere con il metodo update().
Esempio: termometro e condizionatore
Interfaccia dell’osservatore
public interface TemperatureObserver {
void temperatureChanged(int newTemperature);
}
Classe «Termometro» (osservato)
import java.util.*;
public class Thermometer {
private int temperature;
private final List<TemperatureObserver> observers = new ArrayList<>();
public void addObserver(TemperatureObserver observer) {
observers.add(observer);
}
public void removeObserver(TemperatureObserver observer) {
observers.remove(observer);
}
public void setTemperature(int newTemperature) {
if (this.temperature != newTemperature) {
this.temperature = newTemperature;
notifyObservers();
}
}
private void notifyObservers() {
for (TemperatureObserver observer : observers) {
observer.temperatureChanged(temperature);
}
}
}
Esempio di osservatore — «Condizionatore»
public class AirConditioner implements TemperatureObserver {
@Override
public void temperatureChanged(int newTemperature) {
if (newTemperature > 25) {
System.out.println("Condizionatore acceso! Fa caldo: " + newTemperature + "°C");
} else {
System.out.println("Condizionatore spento. Temperatura: " + newTemperature + "°C");
}
}
}
Uso
public class Main {
public static void main(String[] args) {
Thermometer thermometer = new Thermometer();
AirConditioner conditioner = new AirConditioner();
thermometer.addObserver(conditioner);
thermometer.setTemperature(22); // Condizionatore spento. Temperatura: 22°C
thermometer.setTemperature(28); // Condizionatore acceso! Fa caldo: 28°C
}
}
Ecco tutta la magia! Si possono aggiungere anche cento osservatori — tutti riceveranno una notifica quando cambia la temperatura.
Schema grafico del pattern
flowchart LR
T["Termometro (Observable)"] -- notifica --> AC["Condizionatore (Observer)"]
T -- notifica --> L["Logger (Observer)"]
T -- notifica --> Alarm["Allarme (Observer)"]
Dettagli moderni: Observable deprecato e nuovi approcci
Nella libreria standard di Java esistevano java.util.Observable e java.util.Observer, ma da Java 9 sono contrassegnati come deprecati (deprecated). Il motivo è la flessibilità insufficiente (ad esempio, Observable è una classe e non un’interfaccia, per cui è più difficile estendere un’altra classe).
L’approccio moderno è progettare interfacce di listener proprie e la logica di iscrizione/disiscrizione (come nell’esempio sopra). È più flessibile, sicuro e meglio aderente alle esigenze reali.
3. Esempio: mini‑app con sottoscrittori
Creiamo un «contatore di clic» con la possibilità di iscriversi al cambiamento del valore.
Interfaccia del listener
public interface CounterListener {
void counterChanged(int newValue);
}
Classe‑contatore
import java.util.*;
public class Counter {
private int value = 0;
private final List<CounterListener> listeners = new ArrayList<>();
public void addCounterListener(CounterListener l) {
listeners.add(l);
}
public void removeCounterListener(CounterListener l) {
listeners.remove(l);
}
public void increment() {
value++;
notifyListeners();
}
private void notifyListeners() {
for (CounterListener l : listeners) {
l.counterChanged(value);
}
}
public int getValue() {
return value;
}
}
Listener: stampiamo un messaggio
public class ConsoleCounterListener implements CounterListener {
@Override
public void counterChanged(int newValue) {
System.out.println("Il contatore è cambiato: " + newValue);
}
}
Uso
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
counter.addCounterListener(new ConsoleCounterListener());
counter.increment(); // Il contatore è cambiato: 1
counter.increment(); // Il contatore è cambiato: 2
}
}
4. Dettagli utili
Alternative moderne ed estensioni
Nei progetti reali si usano spesso classi anonime o espressioni lambda per iscriversi: counter.addCounterListener(newValue -> System.out.println("Nuovo valore: " + newValue));
(Per farlo, l’interfaccia deve essere funzionale — avere un unico metodo astratto.)
Sono popolari anche le librerie reattive (RxJava, Project Reactor), dove «Osservatore» è implementato con supporto di flussi di eventi, filtraggio, asincronia, ecc. Per capirne l’essenza è sufficiente lo schema classico visto sopra.
Applicazioni del pattern «Osservatore» nella vita reale
- Modelli di dati. La modifica del modello (elenco di attività, prodotti, utenti) notifica le viste per l’aggiornamento.
- Logging. Un logger sottoscritto reagisce agli eventi in tutto il sistema.
- Notifiche. Al cambiamento di stato — invio di email, push notification, messaggi su Telegram.
- Giochi. Modifica della salute, comparsa di un nemico, completamento del livello.
- Multithreading. Un thread pubblica eventi, altri reagiscono.
5. Errori tipici nell’implementazione del pattern «Osservatore»
Errore n. 1: ci si è dimenticati di rimuovere il listener. Se un listener non serve più ma non è stato rimosso, continuerà a ricevere notifiche. Nelle applicazioni di lunga durata questo può portare a leak di memoria.
Errore n. 2: Operazioni lunghe o bloccanti nei gestori. Se il gestore esegue un lavoro pesante (I/O, DB), l’applicazione può «bloccarsi», soprattutto se le notifiche arrivano dal thread dell’UI. Spostate i lavori pesanti su thread in background.
Errore n. 3: Eccezioni nei listener. Un’eccezione in un listener può interrompere l’invio agli altri. Incapsulate le chiamate dei listener in try-catch e registrate gli errori nei log.
Errore n. 4: Registrazione multipla dello stesso listener. Se lo stesso listener viene aggiunto più volte, riceverà l’evento un numero duplicato di volte. Controllate la registrazione e prevenite gli inserimenti duplicati.
Errore n. 5: Accoppiamento rigido tra osservato e osservatore. Se l’osservato conosce implementazioni specifiche degli osservatori, si viola il loose coupling. Usate solo interfacce (ad esempio, TemperatureObserver, CounterListener).
GO TO FULL VERSION