In questo post descriviamo un esempio di implementazione del State Design Pattern.
Contesto
Come punto di partenza c’è un’interfaccia chiamata IService, che rappresenta un servizio generico. L’interfaccia offre tre metodi: start(), stop() e isRunning().
public interface IService {
void start();
void stop();
boolean isRunning();
}
Questa è un’implementazione:
public class ServiceImpl implements IService {
private volatile boolean started;
private Thread thread;
@Override
public synchronized void start() {
if (started) {
return;
}
started = true;
thread = ... // Create a daemon thread instance
thread.setDaemon(true);
thread.start();
}
@Override
public synchronized void stop() {
if (!started) {
return;
}
started = false;
thread.interrupt();
try {
thread.join();
}
catch (InterruptedException ignore) {
...
}
}
@Override
public synchronized boolean isRunning() {
return started;
}
}
Il codice sopra funziona… tuttavia, cosa non va bene?
Mantenere lo stato del servizio in una variabile booleana e fare affidamento su di essa per sapere se il servizio è in esecuzione oppure no.
La logica condizionale all’inizio dei metodi start() e stop(). Un oggetto dovrebbe essere consapevole del proprio stato attuale: se stai guidando un’auto, come fai a sapere che è in movimento? Perché si sta muovendo, perché la stai guidando, non perché hai messo un post-it sul sedile (“Attenzione! È in funzione!”).
State Pattern
Lo State pattern è un behavioral Software Design Pattern. Incapsula lo stato interno dell’oggetto, che può modificare il proprio comportamento quando avviene una transizione.
Oltre a rappresentare gli stati possibili di un dato oggetto, ciascun implementatore concreto di State definisce anche le regole per la transizione da uno stato all’altro. Per questo motivo lo State pattern è molto vicino al concetto di finite-state machine.
Il seguente codice dell’interfaccia IService è un’implementazione dello State Pattern:
public class IfLessService implements IService {
private Thread _watcher;
private final IService running = new IService() {
public boolean isRunning()
{
return true;
}
public void start() {
// Nothing to do here...it is already started.
}
public void stop() {
_watcher.interrupt();
try {
_watcher.join();
}
catch (InterruptedException ignore)
{
...
}
state = notRunning;
}
};
private final IService notRunning = new IService() {
public boolean isRunning() {
return false;
}
public synchronized void start() {
_watcher = ...
_watcher.setDaemon(true);
_watcher.start();
// ...make a state change
// from NOT-RUNNING to RUNNING.
state = running;
}
public void stop() {
// Nothing to do here...it is already stopped!
}
};
// Default initial state is stopped.
private IService state = notRunning;
public synchronized void start() {
state.start();
}
public synchronized void stop() {
state.stop();
}
public boolean isRunning() {
return state.isRunning();
}
}
L’implementazione del servizio delega l’esecuzione dei metodi dell’interfaccia IService allo stato interno (attuale).
Ogni possibile State è a sua volta un’implementazione dell’interfaccia IService. Questo è uno scenario “binario”, quindi esistono solo due classi di State: running e notRunning.
Nota che la responsabilità di ciascuno State è di gestire il comportamento dell’oggetto proprietario in un determinato momento, ma anche – quando necessario – di gestire una transizione di stato verso un altro State: ad esempio, quando il servizio non è in esecuzione (cioè currentState = notRunning), se chiami il metodo start() ci sarà una transizione di stato (da notRunning a running). Una volta che il servizio è avviato, richiamare il metodo start() non avrà più effetto.