Assegnazione: Loop su una Lista di stringhe e stampare ogni elemento seguendo le regole seguenti:
- il primo valore deve essere stampato così com’è
- gli altri elementi devono essere stampati in maiuscolo
- alla fine del ciclo stampare “Exit”
Una prima versione
public class Service {
public void loop(List strings) {
int index = 0;
for (String string : strings) {
if (index == 0) {
System.out.println(string);
} else {
System.out.println(string.toUpperCase());
}
index++;
}
System.out.println("Exit");
}
}
Ok, funziona, è concisa e chiara; proviamo a “complicare” un po’ le cose.
State Pattern
I task eseguiti a ogni iterazione nell’esempio sono banali, ma in uno scenario reale potrebbero essere complessi: perché non isolare questi task in un metodo separato o addirittura in una classe separata?
Introduciamo quindi un’interfaccia Iteration che rappresenta il task eseguito in una determinata iterazione.
interface Iteration {
void execute(String value);
}
Tre task, tre implementazioni iniziali:
Iteration firstIteration = new Iteration() {
public void execute(String value) {
System.out.println(value);
}
}
Iteration middleIteration = new Iteration() {
public void execute(String value) {
System.out.println(value.toUppercase());
}
}
private final Iteration noIteration = new Iteration() {
public void execute() {
System.out.println("Exit");
}
}
Ora, ogni task dovrebbe essere responsabile dell’esecuzione della propria logica e, inoltre, dovrebbe fornire un meccanismo per passare all’iterazione successiva.
Un modo possibile per farlo è introdurre una piccola modifica all’interfaccia Iteration e alla classe esterna che gestisce i task:
interface Iteration {
void execute();
}
public class Loop {
private final Iterator iterator;
...
public Loop(final List strings) {
iterator = strings.iterator();
}
Iteration firstIteration = new Iteration() {
public void execute() {
System.out.println(iterator.next());
}
}
Iteration middleIteration = new Iteration() {
public void execute() {
System.out.println(iterator.next().toUppercase());
}
}
Iteration lastIteration = new Iteration() {
public void execute() {
System.out.println("Exit");
}
}
}
A questo punto dobbiamo mantenere un riferimento all’iterazione corrente. Introduciamo quindi un altro membro di istanza: una currentIteration inizialmente impostata su firstIteration.
private Iteration currentIteration = firstIteration;
Due cose mancano ancora. Primo, la classe Loop deve avere un metodo per avviare l’esecuzione del ciclo:
public void start() {
currentIteration.execute();
}
E poi, ogni task deve decidere come proseguire con l’iterazione successiva. Questa logica può essere isolata in un composed method:
private void nextIteration() {
currentIteration = iterator.hasNext()
? middleIteration
: noIteration;
currentIteration.execute();
}
Lo so: si tratta ancora di una logica condizionale; tuttavia, non è esattamente la stessa della prima versione di Loop: non ci sono indici né variabili temporanee e ogni classe Iteration contiene esattamente ciò che deve essere eseguito in un determinato ciclo di iterazione.
Il codice in questo esempio è solo un esercizio, e sono d’accordo con te che i benefici non sono così evidenti; questo perché il problema di partenza è molto semplice.
Questa è la versione finale della classe Loop:
interface Iteration {
void execute();
}
public class Loop {
Iteration firstIteration = new Iteration() {
public void execute() {
System.out.println(iterator.next());
nextIteration();
}
}
Iteration middleIteration = new Iteration() {
public void execute() {
System.out.println(iterator.next().toUppercase());
nextIteration();
}
}
Iteration lastIteration = new Iteration() {
public void execute() {
System.out.println("Exit");
}
}
private final Iterator iterator;
private Iteration currentIteration = firstIteration;
public Loop(final List strings) {
iterator = strings.iterator();
}
public void start() {
currentIteration.execute();
}
private void nextIteration() {
currentIteration = iterator.hasNext()
? middleIteration
: noIteration;
currentIteration.execute();
}
}