In questo articolo parleremo di:
- OOP - Object-oriented programming
- Design Patterns
OOP - Object-oriented programming
Paradigma di programmazione basato su oggetti
Concetti fondamentali
Classe:
-
È un "template" o "blueprint"
-
Definisce la struttura e il comportamento
-
Descrive cosa un oggetto conterrà e come si comporterà
-
È come il progetto di una casa
Contiene:
-
informazioni (attributi)
-
comportamento (metodi)
Oggetto:
- È un'ISTANZA di una classe
- È la realizzazione concreta della classe
- Contiene dati reali
- È come una casa costruita seguendo il progetto
Esempio:
In questo caso abbiamo una classe Pokemon con capacità (metodi) e caratteristiche (attributi) e una istanza di questa classe chiamata pikachu.
Costruttore:
- Metodo per inizializzare un oggetto
Interfacce:
- espongono i servizi offerti da una classe ai suoi utilizzatori
- devono essere rispettate in modo obbligatorio dalla classe
- aggiungono capacità alla classe che le implementa
- L'interfaccia è un contratto che deve essere rispettato dalle classi che decidono di implementarlo.
Livelli di accessibilità:
- public: accessibile da qualsiasi classe
- private: accessibile solo dalla stessa classe
- protected: accessibile solo dal codice nello stessa classe o in una classe derivata
- internal: accessibile solo dal codice nello stesso assembly
Tecniche
-
Incapsulamento: Raggruppa dati e metodi in una classe, nasconde l'implementazione interna, espone solo l'interfaccia necessaria
-
Ereditarietà: Meccanismo per estendere classi esistenti, crea gerarchie di classi.
Esempio:
Il Cane è un'entità concreta e dettagliata, "figlia" dell'entità più astratta Mammifero, la quale si può ricondurre all'entità ancora più astratta Animale.
-
Polimorfismo: Capacità di un oggetto di assumere diverse forme
-
Astrazione: Nascondere dettagli di implementazione, focus su cosa fa, non come lo fa
Design Patterns
Sono una soluzione a dei problema ricorrente nel software design.
Ha 4 elementi principali:
- Il nome del pattern
- Il problema, descrive quando applicare il pattern
- La soluzione, descrive gli elementi del design e le loro relazioni
- Le conseguenze, descrive gli effetti collaterali e i trade-off
Classificazione
- Creazionali (creazione oggetti)
- Strutturali (composizione classi/oggetti)
- Comportamentali (interazione/responsabilità)
Per scope:
- A livello di classe: Pattern che riguardano principalmente le relazioni tra classi (aspetti statici)
- A livello di oggetto: Pattern che riguardano principalmente le relazioni tra oggetti (aspetti dinamici)
Pattern più comuni:
Factory Method (creazionale)
- Delega la creazione effettiva degli oggetti alle sottoclassi
- Sostituisce le chiamate dirette al costruttore (new) con chiamate a un metodo factory
Quando usarlo:
- Quando non sai in anticipo quali tipi di oggetti dovrai creare
- Quando vuoi delegare la creazione di oggetti alle sottoclassi
- Quando hai bisogno di creare famiglie di oggetti correlati
- Quando vuoi nascondere la logica di creazione degli oggetti
Pro
- ✅ Evita un forte accoppiamento tra il creatore e i prodotti concreti
- ✅ Single Responsibility Principle: sposta il codice di creazione in un posto
- ✅ Open/Closed Principle: puoi aggiungere nuovi tipi di prodotti senza rompere il codice esistente
- ✅ Rende il codice più flessibile e riusabile
Contro
- ❌ Può portare a una proliferazione di sottoclassi se servono molti tipi diversi di oggetti
- ❌ Può rendere il codice più complesso introducendo molte nuove interfacce e classi
- ❌ Potrebbe essere "overkill" per casi semplici dove una semplice creazione diretta sarebbe sufficiente
Esempio:
// Questa è la nostra pizza (il prodotto)
interface Pizza {
void prepare();
void bake();
void box();
}
// Queste sono i tipi specifici di pizza
class Margherita implements Pizza {
public void prepare() { System.out.println("Preparazione Margherita: pomodoro e mozzarella"); }
public void bake() { System.out.println("Cottura a 300 gradi"); }
public void box() { System.out.println("Inscatolamento pizza margherita"); }
}
class Pepperoni implements Pizza {
public void prepare() { System.out.println("Preparazione Pepperoni: pomodoro, mozzarella e salame"); }
public void bake() { System.out.println("Cottura a 300 gradi"); }
public void box() { System.out.println("Inscatolamento pizza pepperoni"); }
}
// Questa è la nostra fabbrica di pizze
class PizzaFactory {
// Questo è il Factory Method!
public Pizza createPizza(String type) {
Pizza pizza = null;
if (type.equals("margherita")) {
pizza = new Margherita();
} else if (type.equals("pepperoni")) {
pizza = new Pepperoni();
}
return pizza;
}
}
public class Pizzeria {
public static void main(String[] args) {
PizzaFactory factory = new PizzaFactory();
// Voglio una margherita
Pizza miaPizza = factory.createPizza("margherita");
miaPizza.prepare();
miaPizza.bake();
miaPizza.box();
// Voglio un pepperoni
Pizza altraPizza = factory.createPizza("pepperoni");
altraPizza.prepare();
altraPizza.bake();
altraPizza.box();
}
}
In sintesi:
È come andare in pizzeria: tu ordini solo "una margherita" e non ti preoccupi di come viene fatta ci pensa la cucina (la nostra factory)!
Singleton (creazionale)
Garantisce che una classe abbia una sola istanza, fornisce un punto di accesso globale a quella istanza
- spesso usato per gestire risorse condivise
Quando usarlo:
- Quando deve esistere una sola istanza di una classe
- Quando quella istanza deve essere accessibile da un punto di accesso globale
- Per gestire risorse condivise (es. connessioni DB, configurazioni, pool di oggetti)
- Quando vuoi controllare strettamente come e quando l'istanza viene creata
Pro
- ✅ Garantisce che una classe abbia una sola istanza
- ✅ Fornisce un punto di accesso globale a quella istanza
- ✅ L'oggetto singleton viene inizializzato solo quando serve (lazy loading)
- ✅ Facile da implementare
Contro
- ❌ Viola il Single Responsibility Principle (gestisce sia l'unicità che la funzionalità)
- ❌ Può rendere il codice difficile da testare
- ❌ Può nascondere dipendenze nel codice
- ❌ Richiede trattamento speciale in ambiente multi-thread
Esempio:
// Classe Singleton per gestire un contatore globale
public class Counter {
// Singola istanza della classe (statica e privata)
private static Counter instance;
// Il valore del contatore
private int count;
// Costruttore privato - impedisce la creazione di nuove istanze
private Counter() {
count = 0;
}
// Metodo pubblico per ottenere l'istanza
// Se non esiste la crea, se esiste la restituisce
public static Counter getInstance() {
if (instance == null) {
instance = new Counter();
}
return instance;
}
// Metodi per usare il contatore
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
// Esempio di utilizzo
public class Main {
public static void main(String[] args) {
// Ottiene la prima istanza
Counter counter1 = Counter.getInstance();
counter1.increment(); // count = 1
// Ottiene la seconda istanza (è la stessa!)
Counter counter2 = Counter.getInstance();
counter2.increment(); // count = 2
// Stampa il valore (sarà 2 per entrambi perché è la stessa istanza)
System.out.println(counter1.getCount()); // Output: 2
System.out.println(counter2.getCount()); // Output: 2
// Verifica che sono la stessa istanza
System.out.println(counter1 == counter2); // Output: true
}
}
Un Counter Singleton è come un contatore globale unico: chiunque lo richieda (getInstance) ottiene sempre lo stesso contatore, permettendo di tenere traccia di un valore condiviso in tutta l'applicazione.
In sintesi:
È come avere un'unica copia delle chiavi di casa: tutti sanno dove trovarla (getInstance()) ma ne esiste una sola!
Decorator (strutturale)
- Permette di aggiungere nuove funzionalità a oggetti esistenti dinamicamente
- Fornisce un'alternativa flessibile all'ereditarietà per estendere le funzionalità
- Avvolge l'oggetto originale in oggetti decoratori che aggiungono comportamenti
Quando usarlo:
- Quando vuoi aggiungere responsabilità a oggetti dinamicamente e in modo trasparente
- Quando l'ereditarietà non è pratica o possibile
- Quando hai bisogno di combinare diverse funzionalità in modo flessibile
- Quando vuoi evitare una gerarchia di classi troppo profonda
Pro
- ✅ Maggiore flessibilità rispetto all'ereditarietà statica
- ✅ Single Responsibility Principle: dividi la funzionalità tra classi
- ✅ Aggiungi/rimuovi responsabilità a runtime
- ✅ Combina comportamenti in modo flessibile
Contro
- ❌ È facile avere troppi decoratori piccoli e simili
- ❌ L'ordine di decorazione può essere importante e difficile da gestire
- ❌ Il codice può diventare complesso con molti decoratori
- ❌ Può essere difficile rimuovere uno specifico wrapper
Esempio:
// Componente base
interface Coffee {
String getDescription();
double getCost();
}
// Implementazione concreta base
class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Caffè semplice";
}
@Override
public double getCost() {
return 1.0;
}
}
// Decorator astratto
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
public String getDescription() {
return decoratedCoffee.getDescription();
}
public double getCost() {
return decoratedCoffee.getCost();
}
}
// Decoratori concreti
class Milk extends CoffeeDecorator {
public Milk(Coffee coffee) {
super(coffee);
}
public String getDescription() {
return decoratedCoffee.getDescription() + ", con latte";
}
public double getCost() {
return decoratedCoffee.getCost() + 0.5;
}
}
class Sugar extends CoffeeDecorator {
public Sugar(Coffee coffee) {
super(coffee);
}
public String getDescription() {
return decoratedCoffee.getDescription() + ", con zucchero";
}
public double getCost() {
return decoratedCoffee.getCost() + 0.2;
}
}
// Uso del pattern
public class CoffeeShop {
public static void main(String[] args) {
// Ordiniamo un caffè con latte e zucchero
Coffee coffee = new SimpleCoffee();
coffee = new Milk(coffee);
coffee = new Sugar(coffee);
System.out.println("Descrizione: " + coffee.getDescription());
System.out.println("Costo: €" + coffee.getCost());
// Proviamo una combinazione diversa
Coffee anotherCoffee = new Sugar(new Sugar(new SimpleCoffee()));
System.out.println("Descrizione: " + anotherCoffee.getDescription());
System.out.println("Costo: €" + anotherCoffee.getCost());
}
}
In sintesi:
È come personalizzare un caffè al bar:
parti da un caffè semplice e poi aggiungi gli extra che vuoi (latte, zucchero, etc.)! Ogni extra è un decoratore che "avvolge" il caffè base e aggiunge qualcosa.
Nel mondo reale, il pattern Decorator è molto usato in:
- Java I/O (BufferedReader, InputStreamReader, etc.)
- UI Frameworks (per aggiungere bordi, scrollbar, etc.)
- Web services (per aggiungere caching, logging, autenticazione)
Template Method (comportamentale)
- Definisce lo scheletro di un algoritmo in un metodo
- Delega alcuni passi alle sottoclassi
- Permette alle sottoclassi di ridefinire certi passi dell'algoritmo senza cambiarne la struttura
Quando usarlo:
- Quando hai un algoritmo che ha una struttura fissa ma passi variabili
- Quando vuoi evitare duplicazione di codice tra classi simili
- Quando vuoi permettere alle sottoclassi di estendere solo certi passi dell'algoritmo
- Per implementare parti invarianti una sola volta nella classe base
Pro
- ✅ Elimina la duplicazione del codice
- ✅ Permette variazioni controllate dell'algoritmo
- ✅ Facilita il riuso del codice
- ✅ Implementa il principio "Hollywood" (don't call us, we'll call you)
Contro
- ❌ Può essere difficile da capire il flusso di controllo
- ❌ Alcune implementazioni possono violare il Principio di Liskov
- ❌ La manutenzione può diventare difficile con molte classi
- ❌ Limita la flessibilità a ciò che il template method permette
È come seguire una ricetta di cucina: ci sono passi fissi (aprire il frigo, prendere gli ingredienti, pulire) e passi che variano in base al piatto (tagliare, cuocere, impiattare). Il Template Method definisce i passi fissi e lascia che le sottoclassi implementino quelli variabili!
Esempio:
abstract class Hamburger {
// Questo è il TEMPLATE METHOD!
// Define il "processo" che TUTTI i panini DEVONO seguire
public final void prepareBurger() {
toastBun(); // Passo 1: sempre uguale per tutti
addPatty(); // Passo 2: diverso per ogni panino
addToppings(); // Passo 3: diverso per ogni panino
wrapBurger(); // Passo 4: sempre uguale per tutti
}
// Metodi CONCRETI (implementati nella classe base)
// Questi passaggi sono UGUALI per TUTTI i panini
private void toastBun() {
System.out.println("Tostando il pane per 30 secondi");
}
private void wrapBurger() {
System.out.println("Avvolgendo il panino nella carta");
}
// Metodi ASTRATTI (devono essere implementati dalle sottoclassi)
// Questi passaggi sono DIVERSI per ogni tipo di panino
protected abstract void addPatty(); // Ogni panino ha la sua carne/pollo
protected abstract void addToppings(); // Ogni panino ha i suoi condimenti
}
// Implementazione specifica per BigMac
class BigMac extends Hamburger {
@Override
protected void addPatty() {
System.out.println("Aggiungendo due hamburger di manzo");
}
@Override
protected void addToppings() {
System.out.println("Aggiungendo lattuga, formaggio, cipolla, salsa speciale");
}
}
// Implementazione specifica per McChicken
class McChicken extends Hamburger {
@Override
protected void addPatty() {
System.out.println("Aggiungendo cotoletta di pollo");
}
@Override
protected void addToppings() {
System.out.println("Aggiungendo lattuga e maionese");
}
}
// Come si usa
public class Main {
public static void main(String[] args) {
Hamburger bigMac = new BigMac();
Hamburger mcChicken = new McChicken();
System.out.println("Preparando BigMac:");
bigMac.prepareBurger();
System.out.println("\nPreparando McChicken:");
mcChicken.prepareBurger();
}
}
Esempi nel mondo reale:
- Framework di testing (JUnit)
- Framework web (metodi del ciclo di vita di servlet/componenti)
- Operazioni di database (apertura/chiusura connessioni)
Iterator (comportamentale)
- Fornisce un modo per accedere agli elementi di una collezione sequenzialmente
- Nasconde la struttura interna della collezione
- Sposta la responsabilità di navigazione dalla collezione all'iteratore
Quando usarlo:
- Quando vuoi accedere agli elementi di una collezione senza esporre la sua struttura interna
- Quando vuoi supportare diversi tipi di attraversamento per la stessa collezione
- Quando vuoi avere un'interfaccia uniforme per attraversare diverse strutture dati
- Quando hai bisogno di più iteratori attivi contemporaneamente sulla stessa collezione
Pro
- ✅ Single Responsibility Principle: separa l'attraversamento dalla collezione
- ✅ Open/Closed Principle: nuovi tipi di collezioni e iteratori senza modificare il codice esistente
- ✅ Supporta diversi tipi di attraversamento contemporaneamente
- ✅ Semplifica il codice client unificando l'accesso alle collezioni
Contro
- ❌ Può essere esagerato per collezioni semplici
- ❌ Usare un iteratore non è efficiente come attraversare alcuni tipi di collezioni direttamente
- ❌ Può creare troppi oggetti iteratore in memoria
- ❌ Può essere complesso da implementare per strutture dati non lineari
Un esempio di grafico
Esempio:
// Interfaccia per l'iteratore
interface Iterator<T> {
boolean hasNext();
T next();
}
// Interfaccia per oggetti iterabili
interface Container<T> {
Iterator<T> getIterator();
}
// Implementazione di una collezione specifica
class BookShelf implements Container<String> {
private String[] books;
private int size;
public BookShelf(int size) {
this.books = new String[size];
this.size = 0;
}
public void addBook(String book) {
if (size < books.length) {
books[size]= book;
size++;
}
}
@Override
public Iterator<String> getIterator() {
return new BookShelfIterator();
}
// Implementazione concreta dell'iteratore
private class BookShelfIterator implements Iterator<String> {
private int index;
@Override
public boolean hasNext() {
return index < size;
}
@Override
public String next() {
if (this.hasNext()) {
return books[index++];
}
return null;
}
}
}
// Uso del pattern
public class Library {
public static void main(String[] args) {
BookShelf bookShelf= new BookShelf(4);
bookShelf.addBook("Design Patterns");
bookShelf.addBook("Clean Code");
bookShelf.addBook("Refactoring");
bookShelf.addBook("Clean Architecture");
// Uso dell'iteratore
Iterator<String> iterator = bookShelf.getIterator();
System.out.println("Libri nella libreria:");
while (iterator.hasNext()) {
String book = iterator.next();
System.out.println("- " + book);
}
// Possiamo avere più iteratori contemporaneamente
Iterator<String> iterator1 = bookShelf.getIterator();
Iterator<String> iterator2 = bookShelf.getIterator();
System.out.println("\nPrimo libro con iterator1: " + iterator1.next());
System.out.println("Primo libro con iterator2: " + iterator2.next());
}
}
In sintesi:
È come sfogliare un libro: non ti interessa come sono organizzate le pagine internamente, usi solo il segnalibro per tenere traccia della posizione e vai avanti pagina dopo pagina!
Esempi nel mondo reale:
- Java Collections Framework (List, Set, Map)
- Database cursors
- File systems (per attraversare directory)
- Tutti i foreach loops in Java (utilizzano implicitamente l'Iterator pattern)
Observer (comportamentale)
- Definisce una dipendenza uno-a-molti tra oggetti
- Quando un oggetto cambia stato, tutti i suoi dipendenti vengono notificati e aggiornati automaticamente
- Implementa il pattern di pubblicazione/sottoscrizione
Quando usarlo:
- Quando una modifica a un oggetto richiede modifiche ad altri oggetti
- Quando non sai quanti oggetti devono essere aggiornati
- Quando vuoi accoppiamento debole tra oggetti
- Per implementare comunicazione event-driven tra componenti
Pro
- ✅ Accoppiamento debole tra subject e observer
- ✅ Supporta il broadcast di comunicazioni
- ✅ Open/Closed Principle: puoi aggiungere nuovi observer senza modificare il subject
- ✅ Stabilisce relazioni oggetto a runtime
Contro
- ❌ Gli observer possono essere notificati in ordine casuale
- ❌ Se non gestito bene, può causare memory leaks (observer non rimossi)
- ❌ Se usato male, può causare update circolari infiniti
- ❌ Può diventare complesso con molti observer o livelli di osservazione
Esempio:
import java.util.ArrayList;
import java.util.List;
// Interfaccia Observer
interface NewsObserver {
void update(String news);
}
// Interfaccia Subject
interface NewsAgency {
void registerObserver(NewsObserver observer);
void removeObserver(NewsObserver observer);
void notifyObservers();
}
// Implementazione concreta del Subject
class NewsChannel implements NewsAgency {
private List<NewsObserver> observers = new ArrayList<>();
private String news;
@Override
public void registerObserver(NewsObserver observer) {
observers.add(observer);
}
@Override
public void removeObserver(NewsObserver observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (NewsObserver observer : observers) {
observer.update(news);
}
}
// Metodo che modifica lo stato e notifica gli observer
public void setNews(String news) {
this.news = news;
notifyObservers();
}
}
// Implementazioni concrete degli Observer
class TVChannel implements NewsObserver {
private String name;
public TVChannel(String name) {
this.name = name;
}
@Override
public void update(String news) {
System.out.println(name + " ha ricevuto la notizia: " + news);
}
}
class NewsWebsite implements NewsObserver {
private String url;
public NewsWebsite(String url) {
this.url = url;
}
@Override
public void update(String news) {
System.out.println(url + " ha aggiornato il sito con: " + news);
}
}
// Uso del pattern
public class NewsSystem {
public static void main(String[] args) {
// Creiamo il subject
NewsChannel newsAgency = new NewsChannel();
// Creiamo gli observer
TVChannel tg1 = new TVChannel("TG1");
TVChannel tg2 = new TVChannel("TG2");
NewsWebsite website = new NewsWebsite("www.news.com");
// Registriamo gli observer
newsAgency.registerObserver(tg1);
newsAgency.registerObserver(tg2);
newsAgency.registerObserver(website);
// Pubblichiamo una notizia
newsAgency.setNews("Breaking News: nuovo pattern Observer scoperto!");
// Rimuoviamo un observer
System.out.println("\nRimuoviamo TG2...");
newsAgency.removeObserver(tg2);
// Pubblichiamo un'altra notizia
newsAgency.setNews("Ultima ora: il pattern Observer funziona benissimo!");
}
}
In sintesi:
È come un servizio di notizie: l'agenzia di stampa (Subject) pubblica notizie e tutti gli abbonati (Observer) le ricevono automaticamente!
Esempi nel mondo reale:
- GUI event handling
- Social media notifications
- Reactive programming frameworks (RxJava)
- Sistemi di eventi in JavaScript
- Sistemi di notifiche push
Strategy (comportamentale)
- Permette di definire una famiglia di algoritmi, incapsularli e renderli intercambiabili
- Gli algoritmi possono variare indipendentemente dai client che li usano
- Consente di selezionare l'algoritmo a runtime
Quando usarlo:
- Quando hai una famiglia di algoritmi simili
- Quando vuoi poter cambiare l'algoritmo a runtime
- Quando un algoritmo usa dati che i client non dovrebbero conoscere
- Quando hai una classe con molti comportamenti che appaiono come statement condizionali
Pro
- ✅ Scambio di algoritmi a runtime
- ✅ Isola l'algoritmo dalla logica che lo usa
- ✅ Evita condizionali complicati
- ✅ Principio di Open/Closed: puoi introdurre nuove strategie senza modificare il codice esistente
Contro
- ❌ Aumenta il numero di oggetti nel sistema
- ❌ Il client deve conoscere le differenze tra le strategie
- ❌ Potrebbe essere overkill per un numero limitato di algoritmi
- ❌ Alcune strategie potrebbero non usare tutti i parametri che ricevono
Esempio:
// Strategy Interface
interface PaymentStrategy {
void pay(int amount);
}
// Concrete Strategies
class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
private String cvv;
private String dateExpiry;
public CreditCardPayment(String cardNumber, String cvv, String dateExpiry) {
this.cardNumber = cardNumber;
this.cvv = cvv;
this.dateExpiry = dateExpiry;
}
@Override
public void pay(int amount) {
System.out.println(amount + "€ pagati con carta di credito");
}
}
class PayPalPayment implements PaymentStrategy {
private String email;
private String password;
public PayPalPayment(String email, String password) {
this.email = email;
this.password = password;
}
@Override
public void pay(int amount) {
System.out.println(amount + "€ pagati con PayPal");
}
}
class BankTransferPayment implements PaymentStrategy {
private String accountNumber;
private String bankCode;
public BankTransferPayment(String accountNumber, String bankCode) {
this.accountNumber = accountNumber;
this.bankCode = bankCode;
}
@Override
public void pay(int amount) {
System.out.println(amount + "€ pagati con bonifico bancario");
}
}
// Context
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
// Client code
public class OnlineStore {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
// Cliente vuole pagare con carta di credito
cart.setPaymentStrategy(new CreditCardPayment("1234-5678", "789", "12/24"));
cart.checkout(100);
// Cliente cambia idea e vuole usare PayPal
cart.setPaymentStrategy(new PayPalPayment("example@email.com", "password"));
cart.checkout(100);
// Un altro cliente paga con bonifico
cart.setPaymentStrategy(new BankTransferPayment("IT123456789", "ABCDEF"));
cart.checkout(200);
}
}
In sintesi:
È come scegliere il metodo di pagamento in un negozio online: puoi pagare con carta, PayPal o bonifico, e puoi cambiare idea fino all'ultimo momento. Il carrello (ShoppingCart) non si preoccupa di come viene effettuato il pagamento, sa solo che deve essere pagato!
Altro esemmpio:
Immagina di avere un'app che deve ordinare una lista di numeri. Puoi ordinarli in modi diversi:
- Dal più piccolo al più grande
- Dal più grande al più piccolo
- Solo i pari prima, poi i dispari
- ecc.
Il pattern Strategy ti permette di:
Definire una famiglia di algoritmi (nell'esempio, i diversi modi di ordinare) Incapsulare ciascun algoritmo (cioè mettere il codice di ogni tipo di ordinamento in una classe separata) Rendere gli algoritmi intercambiabili (puoi cambiare il tipo di ordinamento senza modificare il resto del codice) Esempi nel mondo reale:
Esempio di utilizzo nel mondo reale:
- Algoritmi di compressione (zip, rar, etc.)
- Algoritmi di ordinamento (bubble sort, quick sort, etc.)
- Strategie di cache (LRU, FIFO, etc.)
- Strategie di autenticazione (password, OAuth, biometrica)
- Algoritmi di routing in applicazioni di navigazione
Visitor (comportamentale)
- permette di definire una nuova operazione senza cambiare le classi degli elementi su cui opera
- separare gli algoritmi dagli oggetti su cui operano
- Consente di aggiungere nuove operazioni agli oggetti senza modificarli
- Implementa la double dispatch per selezionare la corretta operazione in base al tipo concreto dell'oggetto
Quando usarlo:
- Quando hai una struttura di oggetti stabile ma devi spesso aggiungere nuove operazioni su di essa
- Quando hai operazioni correlate su oggetti di classi diverse
- Quando le classi della struttura di oggetti hanno interfacce diverse
- Quando vuoi evitare di "sporcare" le classi con operazioni che non sono loro responsabilità primaria
Pro
- ✅ Open/Closed Principle: puoi aggiungere nuove operazioni senza modificare le classi esistenti
- ✅ Single Responsibility Principle: operazioni correlate sono raggruppate in un visitor
- ✅ Accumula stato mentre visita la struttura
- ✅ Può operare su oggetti di classi diverse
Contro
- ❌ Viola l'incapsulamento delle classi visitate
- ❌ Complesso da implementare se la gerarchia di classi cambia spesso
- ❌ Può essere difficile da capire al primo impatto
- ❌ Richiede double dispatch che alcuni linguaggi non supportano nativamente
Esempio:
// Gli elementi che possono essere visitati
interface Animal {
void accept(AnimalVisitor visitor);
}
class Dog implements Animal {
public void accept(AnimalVisitor visitor) {
visitor.visit(this);
}
}
class Cat implements Animal {
public void accept(AnimalVisitor visitor) {
visitor.visit(this);
}
}
// Il visitor
interface AnimalVisitor {
void visit(Dog dog);
void visit(Cat cat);
}
// Un visitor concreto che fa "parlare" gli animali
class SoundVisitor implements AnimalVisitor {
public void visit(Dog dog) {
System.out.println("Il cane fa: Bau!");
}
public void visit(Cat cat) {
System.out.println("Il gatto fa: Miao!");
}
}
// Utilizzo
public class Main {
public static void main(String[] args) {
Animal[] animals = {new Dog(), new Cat()};
AnimalVisitor soundVisitor = new SoundVisitor();
for(Animal animal : animals) {
animal.accept(soundVisitor);
}
}
}
Gli elementi (Animal) dichiarano un metodo accept che accetta un visitor (veterinario) Il visitor dichiara un metodo visit per ogni tipo di elemento Quando aggiungi una nuova operazione (misurare la temperatura, vaccino), crei solo un nuovo visitor Non devi modificare le classi degli elementi esistenti
Esempi nel mondo reale:
- Compilatori (per visitare AST)
- Validatori XML
- Document processors
- UI frameworks (per traversare widget trees)
Differenza tra architettura e design di dettaglio
- L'architettura ti dice "questi sono i macro-componenti e come interagiscono"
- Il design di dettaglio ti dice "questo è come funziona internamente uno specifico componente"