Design Pattern in Java: Guida Completa con Esempi Pratici

Design Pattern Java: Guida Completa con Esempi Pratici

I design pattern Java sono soluzioni collaudate a problemi ricorrenti di progettazione software che puoi applicare direttamente al tuo codice. Non sono librerie o framework, ma schemi di progettazione riutilizzabili che rendono le tue app più leggibili, manutenibili ed estensibili. In sviluppo Android, conoscere i principali design pattern Java (inclusi pattern creazionali, pattern strutturali e pattern comportamentali) ti permette di evitare architetture fragili, ridurre il codice duplicato e collaborare meglio con altri sviluppatori, specialmente quando lavori su design pattern Android complessi.

design pattern java: Singleton, Builder, Observer e Adapter illustrati con diagrammi UML

In questa guida vedremo i principali pattern creazionali, strutturali e comportamentali, con spiegazioni chiare, diagrammi UML testuali, esempi di codice Java funzionanti e indicazioni su quando usarli in un progetto Android.

Design Pattern Java: Pattern Creazionali

Singleton

Descrizione: garantisce che esista una sola istanza di una classe e fornisce un punto di accesso globale a quella istanza.

Problema che risolve: quando hai bisogno di un’unica istanza condivisa (ad esempio un gestore di configurazioni, un logger, un client di rete) e vuoi evitare di creare più oggetti incoerenti o costosi da istanziare.

UML (semplificato)

+----------------+
|   Singleton    |
+----------------+
| - instance     |<---------+ (static)
+----------------+
| + getInstance()|----------+
+----------------+

Esempio Java (Singleton thread-safe con inizializzazione lazy): un esempio tipico legato a thread e concorrenza, approfonditi nelle Guide Java del sito.

public class ConfigManager {
    // Volatile per visibilità tra thread
    private static volatile ConfigManager instance;

    // Costruttore privato per impedire l'istanziazione esterna
    private ConfigManager() {
        // Carica configurazioni, ad esempio da SharedPreferences o file
    }

    public static ConfigManager getInstance() {
        if (instance == null) {
            synchronized (ConfigManager.class) {
                if (instance == null) {
                    instance = new ConfigManager();
                }
            }
        }
        return instance;
    }

    public String getApiBaseUrl() {
        return "https://api.example.com";
    }
}

// Utilizzo
ConfigManager config = ConfigManager.getInstance();
String baseUrl = config.getApiBaseUrl();

Quando usarlo in Android: per gestori globali come client di rete (es. Retrofit wrapper), repository, accesso a database (RoomDatabase spesso è usato come singleton e approfondito nelle nostre Guide Java), logger, service locator. Va usato con moderazione per non trasformare tutto in stato globale difficile da testare.

Factory Method

Descrizione: definisce un’interfaccia per creare un oggetto, ma lascia alle sottoclassi la decisione su quale classe concreta istanziare.

Problema che risolve: quando il codice client non deve dipendere da classi concrete, oppure quando la logica di creazione degli oggetti è complessa e deve essere centralizzata e variabile (es. in base a parametri o contesto).

UML (semplificato)

Creator            Product
+-------------+    +----------------+
| + factory()|---->| + operation()  |
+-------------+    +----------------+
      ^                     ^
      |                     |
ConcreteCreator    ConcreteProduct

Esempio Java (notifiche diverse in base al tipo):

interface Notifier {
    void send(String message);
}

class EmailNotifier implements Notifier {
    @Override
    public void send(String message) {
        System.out.println("Invio email: " + message);
    }
}

class PushNotifier implements Notifier {
    @Override
    public void send(String message) {
        System.out.println("Invio push: " + message);
    }
}

abstract class NotifierFactory {
    public abstract Notifier createNotifier();
}

class EmailNotifierFactory extends NotifierFactory {
    @Override
    public Notifier createNotifier() {
        return new EmailNotifier();
    }
}

class PushNotifierFactory extends NotifierFactory {
    @Override
    public Notifier createNotifier() {
        return new PushNotifier();
    }
}

public class MainFactoryExample {
    public static void main(String[] args) {
        NotifierFactory factory = new PushNotifierFactory();
        Notifier notifier = factory.createNotifier();
        notifier.send("Benvenuto utente!");
    }
}

Quando usarlo in Android: quando vuoi creare componenti diversi in base al contesto (es. creazione di ViewModel, repository o sorgenti dati diverse in fase di test/produzione), o per incapsulare la logica di istanziazione di fragment/custom view.

Abstract Factory

Descrizione: fornisce un’interfaccia per creare famiglie di oggetti correlati o dipendenti tra loro, senza specificarne le classi concrete.

Problema che risolve: quando hai gruppi di oggetti che devono collaborare e variano insieme (es. diversi temi UI, diversi backend), e vuoi poter cambiare famiglia di oggetti senza modificare il codice client.

UML (semplificato)

AbstractFactory
+-----------------------------+
| + createButton()           |
| + createCheckbox()         |
+-----------------------------+
      ^                  ^
      |                  |
ConcreteFactoryA   ConcreteFactoryB

AbstractProductA         AbstractProductB
(Button)                 (Checkbox)
      ^                        ^
      |                        |
ConcreteProductA1      ConcreteProductB1
ConcreteProductA2      ConcreteProductB2

Esempio Java (famiglia di componenti UI per tema chiaro/scuro):

interface Button {
    void render();
}

interface Checkbox {
    void render();
}

class LightButton implements Button {
    @Override
    public void render() {
        System.out.println("Render bottone tema chiaro");
    }
}

class DarkButton implements Button {
    @Override
    public void render() {
        System.out.println("Render bottone tema scuro");
    }
}

class LightCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("Render checkbox tema chiaro");
    }
}

class DarkCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("Render checkbox tema scuro");
    }
}

interface UIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

class LightUIFactory implements UIFactory {
    @Override
    public Button createButton() {
        return new LightButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new LightCheckbox();
    }
}

class DarkUIFactory implements UIFactory {
    @Override
    public Button createButton() {
        return new DarkButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new DarkCheckbox();
    }
}

public class MainAbstractFactoryExample {
    public static void main(String[] args) {
        UIFactory factory = new DarkUIFactory();
        Button button = factory.createButton();
        Checkbox checkbox = factory.createCheckbox();
        button.render();
        checkbox.render();
    }
}

Quando usarlo in Android: quando devi supportare diverse varianti coerenti tra loro, ad esempio diversi backend (REST vs GraphQL), diversi provider (Google/Firebase vs provider proprietario), o famiglie di componenti UI personalizzati per brand/tema.

Builder

Descrizione: separa la costruzione di un oggetto complesso dalla sua rappresentazione, permettendo di creare oggetti passo-passo e rendendo il codice più leggibile.

Problema che risolve: quando un costruttore ha troppi parametri (soprattutto opzionali) e il codice client diventa poco leggibile e soggetto a errori. Il Builder permette una costruzione fluente e chiara.

UML (semplificato)

Director (opzionale)

Builder -----------> Product
  ^
  |
ConcreteBuilder

Esempio Java (configurazione di una richiesta di rete):

public class HttpRequestConfig {
    private final String url;
    private final String method;
    private final int timeout;
    private final boolean cacheEnabled;

    private HttpRequestConfig(Builder builder) {
        this.url = builder.url;
        this.method = builder.method;
        this.timeout = builder.timeout;
        this.cacheEnabled = builder.cacheEnabled;
    }

    public static class Builder {
        private final String url;
        private String method = "GET";
        private int timeout = 5000;
        private boolean cacheEnabled = false;

        public Builder(String url) {
            this.url = url;
        }

        public Builder method(String method) {
            this.method = method;
            return this;
        }

        public Builder timeout(int timeout) {
            this.timeout = timeout;
            return this;
        }

        public Builder cacheEnabled(boolean cacheEnabled) {
            this.cacheEnabled = cacheEnabled;
            return this;
        }

        public HttpRequestConfig build() {
            return new HttpRequestConfig(this);
        }
    }

    @Override
    public String toString() {
        return "HttpRequestConfig{" +
                "url='" + url + '\'' +
                ", method='" + method + '\'' +
                ", timeout=" + timeout +
                ", cacheEnabled=" + cacheEnabled +
                '}';
    }

    public static void main(String[] args) {
        HttpRequestConfig config = new HttpRequestConfig.Builder("https://api.example.com")
                .method("POST")
                .timeout(10000)
                .cacheEnabled(true)
                .build();

        System.out.println(config);
    }
}

Quando usarlo in Android: è molto comune (es. AlertDialog.Builder, NotificationCompat.Builder, Retrofit.Builder). Usalo quando hai oggetti complessi con molti parametri opzionali (configurazioni di rete, oggetti modello per richieste API, impostazioni di schermate/feature).

Prototype

Descrizione: permette di creare nuovi oggetti clonando istanze esistenti (prototipi), invece di istanziarli da zero.

Problema che risolve: quando la creazione di un oggetto è costosa (es. per calcoli complessi o accesso a risorse) e vuoi riutilizzare uno stato base clonandolo. Utile anche quando devi duplicare oggetti con piccole variazioni.

UML (semplificato)

Prototype
+------------------+
| + clone()        |
+------------------+
      ^
      |
ConcretePrototype

Client --> usa clone()

Esempio Java (clonazione di un oggetto configurazione):

class MapConfig implements Cloneable {
    private String mapType;
    private boolean trafficEnabled;
    private boolean buildingsEnabled;

    public MapConfig(String mapType, boolean trafficEnabled, boolean buildingsEnabled) {
        this.mapType = mapType;
        this.trafficEnabled = trafficEnabled;
        this.buildingsEnabled = buildingsEnabled;
    }

    @Override
    public MapConfig clone() {
        try {
            return (MapConfig) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }

    public void setTrafficEnabled(boolean trafficEnabled) {
        this.trafficEnabled = trafficEnabled;
    }

    @Override
    public String toString() {
        return "MapConfig{" +
                "mapType='" + mapType + '\'' +
                ", trafficEnabled=" + trafficEnabled +
                ", buildingsEnabled=" + buildingsEnabled +
                '}';
    }

    public static void main(String[] args) {
        MapConfig defaultConfig = new MapConfig("NORMAL", false, true);
        MapConfig trafficConfig = defaultConfig.clone();
        trafficConfig.setTrafficEnabled(true);

        System.out.println(defaultConfig);
        System.out.println(trafficConfig);
    }
}

Quando usarlo in Android: quando devi creare molte varianti simili di oggetti pesanti (configurazioni di mappe, opzioni di animazione, modelli di dati complessi) partendo da un default, evitando di riconfigurare ogni volta da zero.

Design Pattern Java: Pattern Strutturali

Adapter

Descrizione: converte l’interfaccia di una classe in un’altra interfaccia attesa dal client, permettendo la collaborazione tra classi incompatibili.

Problema che risolve: quando vuoi riutilizzare una classe esistente ma la sua interfaccia non coincide con quella attesa dal codice client (ad esempio quando integri una libreria di terze parti).

UML (semplificato)

Client ---> Target
              ^
              |
          Adapter ---> Adaptee

Esempio Java (adattare una libreria di log a una nostra interfaccia):

// Interfaccia attesa dal client
interface Logger {
    void logInfo(String message);
}

// Classe esistente (adaptee) con interfaccia diversa
class ThirdPartyLogger {
    public void log(String level, String message) {
        System.out.println("[" + level + "] " + message);
    }
}

// Adapter
class LoggerAdapter implements Logger {
    private final ThirdPartyLogger thirdPartyLogger;

    public LoggerAdapter(ThirdPartyLogger thirdPartyLogger) {
        this.thirdPartyLogger = thirdPartyLogger;
    }

    @Override
    public void logInfo(String message) {
        thirdPartyLogger.log("INFO", message);
    }
}

public class MainAdapterExample {
    public static void main(String[] args) {
        ThirdPartyLogger thirdPartyLogger = new ThirdPartyLogger();
        Logger logger = new LoggerAdapter(thirdPartyLogger);
        logger.logInfo("Applicazione avviata");
    }
}

Quando usarlo in Android: per adattare API di librerie esterne a interfacce interne (es. diversi SDK di analytics dietro a una tua interfaccia AnalyticsTracker), per creare Adapter di una RecyclerView.Adapter più riutilizzabili, o per far convivere vecchie e nuove API in fase di migrazione.

Decorator

Descrizione: aggiunge responsabilità a un oggetto in modo dinamico, avvolgendolo in un altro oggetto che implementa la stessa interfaccia.

Problema che risolve: quando vuoi estendere il comportamento di un oggetto senza usare l’ereditarietà (o quando non è possibile modificare la classe originale), e vuoi poter combinare diversi comportamenti decorativi.

UML (semplificato)

Component <----------------------+
+ operation()                    |
      ^                          |
      |                          |
ConcreteComponent         Decorator
                               ^
                               |
                      ConcreteDecorator

Esempio Java (decorare un DataSource con cache):

interface DataSource {
    String getData();
}

class NetworkDataSource implements DataSource {
    @Override
    public String getData() {
        System.out.println("Richiesta rete...");
        return "Dati dal server";
    }
}

abstract class DataSourceDecorator implements DataSource {
    protected final DataSource wrappee;

    protected DataSourceDecorator(DataSource wrappee) {
        this.wrappee = wrappee;
    }
}

class CacheDataSource extends DataSourceDecorator {
    private String cache;

    public CacheDataSource(DataSource wrappee) {
        super(wrappee);
    }

    @Override
    public String getData() {
        if (cache == null) {
            cache = wrappee.getData();
            System.out.println("Memorizzo in cache");
        } else {
            System.out.println("Restituisco dalla cache");
        }
        return cache;
    }
}

public class MainDecoratorExample {
    public static void main(String[] args) {
        DataSource network = new NetworkDataSource();
        DataSource cached = new CacheDataSource(network);

        System.out.println(cached.getData());
        System.out.println(cached.getData());
    }
}

Quando usarlo in Android: per aggiungere cache, logging, retry o metriche a repository, sorgenti dati o client di rete, senza modificare la loro implementazione originale. Molto utile con pattern Repository/UseCase.

Facade

Descrizione: fornisce un’interfaccia semplificata (facciata) a un sottosistema complesso di classi, nascondendone i dettagli.

Problema che risolve: quando il codice client deve interagire con molte classi o API complesse; la facciata incapsula la complessità e offre metodi ad alto livello più facili da usare.

UML (semplificato)

Client ---> Facade ---> SubsystemClassA
                       SubsystemClassB
                       SubsystemClassC

Esempio Java (facciata per un modulo di autenticazione):

class AuthApi {
    public boolean login(String email, String password) {
        System.out.println("Chiamata API login");
        return true;
    }
}

class TokenStorage {
    public void saveToken(String token) {
        System.out.println("Salvo token: " + token);
    }
}

class UserPreferences {
    public void setLoggedIn(boolean loggedIn) {
        System.out.println("Utente loggato: " + loggedIn);
    }
}

// Facade
class AuthManager {
    private final AuthApi authApi = new AuthApi();
    private final TokenStorage tokenStorage = new TokenStorage();
    private final UserPreferences userPreferences = new UserPreferences();

    public boolean login(String email, String password) {
        boolean success = authApi.login(email, password);
        if (success) {
            tokenStorage.saveToken("FAKE_TOKEN");
            userPreferences.setLoggedIn(true);
        }
        return success;
    }
}

public class MainFacadeExample {
    public static void main(String[] args) {
        AuthManager authManager = new AuthManager();
        boolean loggedIn = authManager.login("user@example.com", "password");
        System.out.println("Login riuscito: " + loggedIn);
    }
}

Quando usarlo in Android: per incapsulare l’accesso a moduli complessi (es. autenticazione, pagamenti, sincronizzazione dati, player multimediali) dietro a una singola classe semplice da usare nelle Activity/Fragment/ViewModel.

Proxy

Descrizione: fornisce un sostituto (proxy) di un altro oggetto per controllarne l’accesso, aggiungere funzionalità (lazy loading, cache, sicurezza) o gestire oggetti remoti.

Problema che risolve: quando non vuoi/puoi accedere direttamente a un oggetto (perché costoso da creare, remoto, o perché devi controllarne l’accesso) e hai bisogno di un intermediario trasparente per il client.

UML (semplificato)

Client ---> Proxy ---> RealSubject
           ^
           |
        Subject (interfaccia comune)

Esempio Java (proxy con controllo di permessi):

interface PhotoStorage {
    void deletePhoto(String id);
}

class RealPhotoStorage implements PhotoStorage {
    @Override
    public void deletePhoto(String id) {
        System.out.println("Foto " + id + " eliminata dal server");
    }
}

class AuthProxyPhotoStorage implements PhotoStorage {
    private final PhotoStorage realPhotoStorage;
    private final boolean isAdmin;

    public AuthProxyPhotoStorage(PhotoStorage realPhotoStorage, boolean isAdmin) {
        this.realPhotoStorage = realPhotoStorage;
        this.isAdmin = isAdmin;
    }

    @Override
    public void deletePhoto(String id) {
        if (!isAdmin) {
            System.out.println("Permesso negato per eliminare la foto " + id);
            return;
        }
        realPhotoStorage.deletePhoto(id);
    }
}

public class MainProxyExample {
    public static void main(String[] args) {
        PhotoStorage real = new RealPhotoStorage();
        PhotoStorage proxyUser = new AuthProxyPhotoStorage(real, false);
        PhotoStorage proxyAdmin = new AuthProxyPhotoStorage(real, true);

        proxyUser.deletePhoto("123");
        proxyAdmin.deletePhoto("123");
    }
}

Quando usarlo in Android: per aggiungere controllo di permessi, caching o lazy loading a repository o client di rete, per simulare servizi remoti in test, o per gestire accesso a risorse pesanti (es. caricamento di immagini ad alta risoluzione solo quando necessario).

Design Pattern Java: Pattern Comportamentali

Observer

Descrizione: definisce una dipendenza uno-a-molti tra oggetti, in modo che quando un oggetto (soggetto) cambia stato, tutti gli osservatori vengano notificati automaticamente.

Problema che risolve: quando molti oggetti devono reagire al cambiamento di stato di un altro oggetto, senza creare forti accoppiamenti tra loro. È alla base di eventi, callback e sistemi reattivi.

UML (semplificato)

Subject <---------------------+
+ attach(Observer)           |
+ detach(Observer)           |
+ notify()                   |
      ^                      |
      |                      |
ConcreteSubject        Observer
                       + update()
                            ^
                            |
                    ConcreteObserver

Esempio Java (osservatori di cambiamento stato):

import java.util.ArrayList;
import java.util.List;

interface Observer {
    void update(int newValue);
}

interface Subject {
    void attach(Observer observer);
    void detach(Observer observer);
    void notifyObservers();
}

class Counter implements Subject {
    private final List<Observer> observers = new ArrayList<>();
    private int value = 0;

    public void increment() {
        value++;
        notifyObservers();
    }

    public int getValue() {
        return value;
    }

    @Override
    public void attach(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void detach(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers() {
        for (Observer o : observers) {
            o.update(value);
        }
    }
}

class ConsoleObserver implements Observer {
    private final String name;

    public ConsoleObserver(String name) {
        this.name = name;
    }

    @Override
    public void update(int newValue) {
        System.out.println(name + " osserva nuovo valore: " + newValue);
    }
}

public class MainObserverExample {
    public static void main(String[] args) {
        Counter counter = new Counter();
        counter.attach(new ConsoleObserver("Observer1"));
        counter.attach(new ConsoleObserver("Observer2"));

        counter.increment();
        counter.increment();
    }
}

Quando usarlo in Android: è alla base di LiveData, Flow, RxJava e listener vari (click, testo, ecc.) ed è approfondito nelle nostre guide Java sulla programmazione reattiva. Usalo quando devi notificare più parti dell’app di un cambiamento di stato (es. dati caricati, user login/logout) in modo disaccoppiato.

Strategy

Descrizione: definisce una famiglia di algoritmi, li incapsula e li rende intercambiabili a runtime, consentendo al client di scegliere la strategia da usare.

Problema che risolve: quando hai diverse varianti di un algoritmo (es. ordinamento, validazione, formattazione) e vuoi poterle sostituire facilmente senza modificare il codice client.

UML (semplificato)

Context ---> Strategy
                ^
                |
          ConcreteStrategyA
          ConcreteStrategyB

Esempio Java (strategie di validazione password):

interface PasswordStrategy {
    boolean isValid(String password);
}

class SimplePasswordStrategy implements PasswordStrategy {
    @Override
    public boolean isValid(String password) {
        return password != null && password.length() >= 6;
    }
}

class StrongPasswordStrategy implements PasswordStrategy {
    @Override
    public boolean isValid(String password) {
        if (password == null || password.length() < 8) return false;
        boolean hasDigit = password.matches(".*\\d.*");
        boolean hasUpper = password.matches(".*[A-Z].*");
        return hasDigit && hasUpper;
    }
}

class PasswordValidator {
    private PasswordStrategy strategy;

    public PasswordValidator(PasswordStrategy strategy) {
        this.strategy = strategy;
    }

    public void setStrategy(PasswordStrategy strategy) {
        this.strategy = strategy;
    }

    public boolean validate(String password) {
        return strategy.isValid(password);
    }
}

public class MainStrategyExample {
    public static void main(String[] args) {
        PasswordValidator validator = new PasswordValidator(new SimplePasswordStrategy());
        System.out.println(validator.validate("abc123")); // true

        validator.setStrategy(new StrongPasswordStrategy());
        System.out.println(validator.validate("abc123")); // false
        System.out.println(validator.validate("Abc12345")); // true
    }
}

Quando usarlo in Android: per gestire diverse strategie di caching, sincronizzazione, layouting, formattazione date/stringhe, logica di pricing o feature variabili in base all’ambiente (dev/stage/prod) o a impostazioni utente.

Command

Descrizione: incapsula una richiesta (comando) come un oggetto, permettendo di parametrizzare gli invocatori, fare queue, log, undo/redo delle operazioni.

Problema che risolve: quando vuoi disaccoppiare chi emette una richiesta da chi la esegue, o quando hai bisogno di storicizzare, accodare o annullare operazioni (es. azioni utente).

UML (semplificato)

Invoker ---> Command ---> Receiver
              ^
              |
        ConcreteCommand

Esempio Java (comandi undoable su un editor di testo):

import java.util.Stack;

interface Command {
    void execute();
    void undo();
}

class TextEditor {
    private final StringBuilder text = new StringBuilder();

    public void append(String s) {
        text.append(s);
    }

    public void deleteLast(int count) {
        text.delete(text.length() - count, text.length());
    }

    public String getText() {
        return text.toString();
    }
}

class AppendCommand implements Command {
    private final TextEditor editor;
    private final String text;

    public AppendCommand(TextEditor editor, String text) {
        this.editor = editor;
        this.text = text;
    }

    @Override
    public void execute() {
        editor.append(text);
    }

    @Override
    public void undo() {
        editor.deleteLast(text.length());
    }
}

class CommandManager {
    private final Stack<Command> history = new Stack<>();

    public void executeCommand(Command command) {
        command.execute();
        history.push(command);
    }

    public void undoLast() {
        if (!history.isEmpty()) {
            history.pop().undo();
        }
    }
}

public class MainCommandExample {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();
        CommandManager manager = new CommandManager();

        manager.executeCommand(new AppendCommand(editor, "Hello "));
        manager.executeCommand(new AppendCommand(editor, "World"));
        System.out.println(editor.getText());

        manager.undoLast();
        System.out.println(editor.getText());
    }
}

Quando usarlo in Android: per implementare azioni undo/redo (es. in editor, canvas di disegno), per queue di job (es. sincronizzazioni, upload), o per incapsulare azioni UI (es. comandi eseguiti dal ViewModel raggruppando logica e parametri).

Template Method

Descrizione: definisce lo scheletro di un algoritmo in un metodo di una classe base, delegando ad alcune operazioni astratte (override nelle sottoclassi) i dettagli che possono cambiare.

Problema che risolve: quando hai algoritmi con struttura comune ma passi specifici diversi tra implementazioni, e vuoi evitare di duplicare la logica generale.

UML (semplificato)

AbstractClass
+ templateMethod()  (final)
+ step1()
+ step2()
      ^
      |
ConcreteClassA  (override step1/step2)
ConcreteClassB

Esempio Java (processo di caricamento dati con passi personalizzabili):

abstract class DataLoader {
    // Template method
    public final void load() {
        showLoading();
        String raw = fetchData();
        Object parsed = parseData(raw);
        showResult(parsed);
        hideLoading();
    }

    protected void showLoading() {
        System.out.println("Mostro loading...");
    }

    protected void hideLoading() {
        System.out.println("Nascondo loading...");
    }

    protected abstract String fetchData();
    protected abstract Object parseData(String raw);
    protected abstract void showResult(Object result);
}

class UserDataLoader extends DataLoader {
    @Override
    protected String fetchData() {
        System.out.println("Chiamata API utenti...");
        return "{name: 'Mario'}";
    }

    @Override
    protected Object parseData(String raw) {
        return "Utente: Mario"; // parsing fittizio
    }

    @Override
    protected void showResult(Object result) {
        System.out.println("Mostro risultato: " + result);
    }
}

public class MainTemplateMethodExample {
    public static void main(String[] args) {
        DataLoader loader = new UserDataLoader();
        loader.load();
    }
}

Quando usarlo in Android: per riutilizzare il flusso comune di operazioni (es. mostrare/hide loading, chiamare repository, gestire errori) con passi personalizzabili per diverse schermate o tipi di dati. In Android moderno però spesso è sostituito da composizione e callback/strategie.

Iterator

Descrizione: fornisce un modo per accedere sequenzialmente agli elementi di una collezione senza esporre la rappresentazione interna.

Problema che risolve: quando vuoi percorrere strutture dati complesse (non necessariamente array/list) in modo uniforme e indipendente dalla loro implementazione interna.

UML (semplificato)

Iterator
+ hasNext()
+ next()
      ^
      |
ConcreteIterator

Aggregate
+ createIterator()
      ^
      |
ConcreteAggregate

Esempio Java (iteratore personalizzato su una collezione semplice):

interface MyIterator<T> {
    boolean hasNext();
    T next();
}

interface MyIterableCollection<T> {
    MyIterator<T> iterator();
}

class StringCollection implements MyIterableCollection<String> {
    private final String[] items;

    public StringCollection(String[] items) {
        this.items = items;
    }

    @Override
    public MyIterator<String> iterator() {
        return new ArrayIterator();
    }

    private class ArrayIterator implements MyIterator<String> {
        private int index = 0;

        @Override
        public boolean hasNext() {
            return index < items.length;
        }

        @Override
        public String next() {
            return items[index++];
        }
    }
}

public class MainIteratorExample {
    public static void main(String[] args) {
        StringCollection collection = new StringCollection(new String[]{"uno", "due", "tre"});
        MyIterator<String> it = collection.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }
}

Quando usarlo in Android: in genere usi gli iteratori già forniti dalle collezioni Java/Kotlin. Il concetto è utile quando crei collezioni personalizzate (es. wrapper su cursori DB, risultati paginati) e vuoi fornire un modo standard per scorrerle senza esporre i dettagli interni.

Riepilogo dei Design Pattern Java

I design pattern sono strumenti fondamentali per strutturare applicazioni Android/Java robuste, estensibili e facili da manutenere. Non vanno usati in modo dogmatico, ma come soluzioni da tenere a mente quando incontri problemi di progettazione ricorrenti. Conoscere i pattern creazionali, strutturali e comportamentali ti aiuta a dare un nome alle soluzioni e a comunicare meglio con il tuo team.

La tabella seguente riassume i pattern visti, con tipo, problema principale che risolvono e un tipico caso d’uso in Android:

PatternTipoProblema principaleUso tipico in Android
SingletonCreazionaleUn’unica istanza condivisaClient di rete, database, repository globali
Factory MethodCreazionaleCreazione oggetti delegata a sottoclassiFactory per ViewModel, repository, sorgenti dati
Abstract FactoryCreazionaleFamiglie di oggetti correlatiVarianti di backend, temi o provider
BuilderCreazionaleCostruzione di oggetti complessiDialog, notifiche, client di rete, configurazioni
PrototypeCreazionaleClonare oggetti costosi/complessiConfig di mappe, animazioni, modelli dati complessi
AdapterStrutturaleInterfacce incompatibiliAdattare SDK di terze parti a interfacce interne
DecoratorStrutturaleAggiungere comportamento dinamicamenteCache, logging, retry su repository/client di rete
FacadeStrutturaleSemplificare sottosistemi complessiGestori di autenticazione, pagamenti, sincronizzazione
ProxyStrutturaleControllare accesso a un oggettoControllo permessi, lazy loading, cache remota
ObserverComportamentaleNotificare cambi di stato a molti osservatoriLiveData, Flow, listener di eventi UI
StrategyComportamentaleAlgoritmi intercambiabiliStrategie di validazione, caching, sincronizzazione
CommandComportamentaleIncapsulare richieste come oggettiUndo/redo, queue di job, azioni ViewModel
Template MethodComportamentaleScheletro di algoritmo riutilizzabileFlussi di caricamento dati, pipeline comuni
IteratorComportamentaleIterare collezioni nascondendo dettagli interniWrapper su cursori DB o collezioni personalizzate

Usare consapevolmente questi pattern, uniti a buone pratiche di architettura (MVVM, Clean Architecture, ecc.), ti permetterà di scrivere app Android più scalabili, testabili e facili da evolvere nel tempo.

Domande frequenti sui Design Pattern in Java

Cosa sono i design pattern in Java?

I design pattern in Java sono soluzioni generali e riutilizzabili a problemi ricorrenti di progettazione software. Non sono frammenti di codice da copiare e incollare, ma modelli concettuali che descrivono come organizzare classi, oggetti e responsabilità per ottenere codice più chiaro, estensibile e manutenibile.

Quali sono i design pattern più usati in Android?

In Android i design pattern Java più usati sono i pattern creazionali come Singleton e Builder (es. per client di rete e dialog), i pattern strutturali come Adapter e Facade (per integrare SDK e semplificare moduli complessi) e i pattern comportamentali come Observer e Strategy (alla base di LiveData, Flow e logiche di presentazione flessibili).

Qual è la differenza tra Factory Method e Abstract Factory?

Factory Method definisce un unico metodo di creazione che le sottoclassi implementano per restituire un singolo tipo di prodotto concreto. Abstract Factory, invece, espone più metodi di creazione per ottenere un’intera famiglia di oggetti correlati tra loro (bottoni, checkbox, temi, backend, ecc.), mantenendo coerenti le combinazioni di prodotti usate dal client.

Quando non dovrei usare i design pattern?

Non dovresti usare i design pattern in modo dogmatico o prematuro su codice semplice. Se una soluzione diretta è chiara, leggibile e poco soggetta a cambiamenti, introdurre un pattern può aggiungere solo complessità inutile. I pattern sono utili quando emergono problemi di estendibilità, riuso, testabilità o quando il dominio è abbastanza complesso da giustificarne l’adozione.

I design pattern in Kotlin sono gli stessi che in Java?

Dal punto di vista concettuale, i design pattern in Kotlin sono gli stessi che in Java, perché risolvono gli stessi problemi di progettazione. Tuttavia, alcune caratteristiche del linguaggio (data class, funzioni di estensione, sealed class, coroutine) permettono implementazioni più concise o alternative idiomatiche che riducono la necessità di pattern espliciti come Builder o Template Method.

Commenti

Rispondi

Scopri di più da App Tutorial

Abbonati ora per continuare a leggere e avere accesso all'archivio completo.

Continua a leggere