#2 Design Patterns - Structural Design Patterns: Dal Caos all'Armonia

 



Eccoci tornati nel secondo capitolo dei Design Pattern. Questa volta è il turno dei Structural Design Patterns, sono una categoria che si concentrano sulla composizione di classi e oggetti per formare strutture più complesse. Questi pattern mirano a migliorare l'efficienza e la chiarezza del sistema, permettendo di comporre oggetti in modi flessibili. 

Adapter Pattern

L'Adapter Pattern è utilizzato per consentire a due interfacce incompatibili di lavorare insieme. Questo è particolarmente utile quando si utilizzano librerie o classi esterne con interfacce diverse dalla nostra applicazione. Ecco un esempio:

// Vecchia interfaccia incompatibile con il nostro codice
public interface VecchiaInterfaccia {
    void operazioneVecchia();
}

// Nuova interfaccia che il nostro codice deve usare
public interface NuovaInterfaccia {
    void operazioneNuova();
}

// Adapter che collega la vecchia interfaccia alla nuova
public class Adapter implements NuovaInterfaccia {
    private VecchiaInterfaccia vecchiaOggetto;

    public Adapter(VecchiaInterfaccia vecchiaOggetto) {
        this.vecchiaOggetto = vecchiaOggetto;
    }

    @Override
    public void operazioneNuova() {
        vecchiaOggetto.operazioneVecchia();
    }
}

Obiettivo:

Immagina di sviluppare un videogioco in cui hai una libreria di gestione del suono che è progettata per funzionare con una determinata interfaccia audio. Tuttavia, la libreria di gestione del suono utilizza un'interfaccia audio vecchia e obsoleta, ma tu vuoi utilizzare una nuova e più avanzata libreria audio con un'interfaccia diversa. Il problema è che le due interfacce audio sono incompatibili, ma desideri comunque utilizzare la nuova libreria audio senza dover riscrivere l'intero codice del gioco.

Soluzione:

Per risolvere questo problema, abbiamo bisogno di diversi step:

  • Definisci un'interfaccia InterfacciaAudio che rappresenta l'interfaccia audio desiderata che vuoi utilizzare nel tuo gioco. Questa interfaccia sarà incompatibile con la vecchia libreria audio.

  • public interface InterfacciaAudio {
        void playSound(String sound);
        void stopSound();
    }

  • Implementa una classe chiamata LibreriaAudioNuova che rappresenta la tua nuova libreria audio che segue l'interfaccia desiderata

  • public class LibreriaAudioNuova implements InterfacciaAudio {
        // Implementa i metodi dell'interfaccia InterfacciaAudio
        public void playSound(String sound) {
            System.out.println("Suono riprodotto: " + sound);
        }

        public void stopSound() {
            System.out.println("Riproduzione audio interrotta.");
        }
    }
  • Ora, per utilizzare la tua nuova libreria audio nel gioco, crea un adapter che si adatta alla vecchia libreria audio utilizzando l'interfaccia desiderata.

    public class AdapterAudio implements InterfacciaAudio {
        private LibreriaAudioVecchia libreriaAudioVecchia;

        public AdapterAudio(LibreriaAudioVecchia libreriaAudioVecchia) {
            this.libreriaAudioVecchia = libreriaAudioVecchia;
        }

        public void playSound(String sound) { // Adatta il play dell'interfaccia vecchia all'interfaccia desiderata
            libreriaAudioVecchia.play(sound);
        }

        public void stopSound() {
            // Adatta lo stop dell'interfaccia vecchia all'interfaccia desiderata
            libreriaAudioVecchia.stop();
        }
    }

  • Ora puoi utilizzare il tuo adapter per utilizzare la nuova libreria audio nel gioco, anche se la vecchia libreria utilizza un'interfaccia diversa:

    // Creazione dell'adapter per utilizzare la nuova libreria audio
    LibreriaAudioVecchia libreriaAudioVecchia = new LibreriaAudioVecchia();
    InterfacciaAudio adapter = new AdapterAudio(libreriaAudioVecchia);

    // Utilizzo dell'interfaccia desiderata per riprodurre un suono
    adapter.playSound("Effetto sonoro del gioco");

    // Interrompi la riproduzione audio
    adapter.stopSound();
In questo modo, hai risolto il problema di incompatibilità tra la vecchia e la nuova libreria audio nel tuo gioco utilizzando l'Adapter Pattern. L'adapter si adatta all'interfaccia audio vecchia e permette di utilizzare la nuova libreria audio senza dover modificare il resto del codice del gioco.

Decorator Pattern

Il Decorator Pattern è utilizzato per aggiungere funzionalità aggiuntive a oggetti senza modificarne la struttura. Questo è utile quando si desidera estendere le capacità di una classe senza creare sottoclassi. Ecco un esempio:

// Interfaccia Component base
public interface Component {
    void operazione();
}

// Implementazione concreta di Component
public class ComponentConcreto implements Component {
    @Override
    public void operazione() {
        System.out.println("Operazione di ComponentConcreto");
    }
}

// Decorator base
public abstract class Decorator implements Component {
    protected Component componente;

    public Decorator(Component componente) {
        this.componente = componente;
    }

    @Override
    public void operazione() {
        componente.operazione();
    }
}

// Decorator concreto
public class DecoratorConcreto extends Decorator {
    public DecoratorConcreto(Component componente) {
        super(componente);
    }

    @Override
    public void operazione() {
        super.operazione();
        aggiuntaAggiuntiva();
    }

    private void aggiuntaAggiuntiva() {
        System.out.println("Funzionalità aggiuntiva di DecoratorConcreto");
    }
}

Obiettivo:

Immagina di sviluppare un videogioco in cui i personaggi possono acquisire abilità speciali come invisibilità, velocità aumentata e scudi protettivi. Vuoi poter aggiungere queste abilità ai personaggi in modo dinamico durante il gioco senza modificare il codice delle classi base dei personaggi.

Soluzione:

Il Decorator Pattern permette di aggiungere comportamenti aggiuntivi agli oggetti in modo flessibile. Ecco come si può implementare questo pattern:
  • Definisci un'interfaccia comune per i personaggi del gioco

// Interfaccia per i personaggi del gioco
public interface Character {
    void describe();
}

  • Implementa una classe concreta per un personaggio base.
// Implementazione concreta di un personaggio base
public class BaseCharacter implements Character {
    @Override
    public void describe() {
        System.out.println("Sono un personaggio base.");
    }
}

  • Crea una classe astratta CharacterDecorator che implementa l'interfaccia Character e contiene un riferimento a un oggetto Character.
// Classe astratta Decorator che implementa l'interfaccia Character
public abstract class CharacterDecorator implements Character {
    protected Character decoratedCharacter;

    public CharacterDecorator(Character decoratedCharacter) {
        this.decoratedCharacter = decoratedCharacter;
    }

    @Override
    public void describe() {
        decoratedCharacter.describe();
    }
}

  • Implementa i decorator concreti per le abilità speciali.
// Decorator per l'abilità di invisibilità
public class InvisibilityDecorator extends CharacterDecorator {
    public InvisibilityDecorator(Character decoratedCharacter) {
        super(decoratedCharacter);
    }

    @Override
    public void describe() {
        decoratedCharacter.describe();
        System.out.println("Ho l'abilità di invisibilità.");
    }
}

// Decorator per l'abilità di velocità aumentata
public class SpeedDecorator extends CharacterDecorator {
    public SpeedDecorator(Character decoratedCharacter) {
        super(decoratedCharacter);
    }

    @Override
    public void describe() {
        decoratedCharacter.describe();
        System.out.println("Ho l'abilità di velocità aumentata.");
    }
}

// Decorator per l'abilità di scudo protettivo
public class ShieldDecorator extends CharacterDecorator {
    public ShieldDecorator(Character decoratedCharacter) {
        super(decoratedCharacter);
    }

    @Override
    public void describe() {
        decoratedCharacter.describe();
        System.out.println("Ho l'abilità di scudo protettivo.");
    }
}

  • Utilizza i decorator per aggiungere abilità ai personaggi durante il gioco.
public class Game {
    public static void main(String[] args) {
        // Crea un personaggio base
        Character baseCharacter = new BaseCharacter();
       
        // Aggiungi l'abilità di invisibilità al personaggio
        Character invisibleCharacter = new InvisibilityDecorator(baseCharacter);
       
        // Aggiungi l'abilità di velocità aumentata al personaggio invisibile
        Character speedyInvisibleCharacter = new SpeedDecorator(invisibleCharacter);
       
        // Aggiungi l'abilità di scudo protettivo al personaggio con velocità aumentata
        Character shieldedSpeedyInvisibleCharacter = new ShieldDecorator(speedyInvisibleCharacter);
       
        // Descrivi il personaggio finale con tutte le abilità
        shieldedSpeedyInvisibleCharacter.describe();
    }
}

OUTPUT
Sono un personaggio base.
Ho l'abilità di invisibilità.
Ho l'abilità di velocità aumentata.
Ho l'abilità di scudo protettivo.

Composite Pattern

Il Composite Pattern è utilizzato per creare strutture ad albero e trattare oggetti singoli e composizioni di oggetti in modo uniforme. Questo è utile quando si vuole rappresentare una gerarchia di oggetti in un modo che semplifica l'interazione con essi. Ecco un esempio:

// Interfaccia Component per oggetti singoli e composizioni
public interface Component {
    void operazione();
}

// Implementazione concreta di Component per oggetti singoli
public class Foglia implements Component {
    @Override
    public void operazione() {
        System.out.println("Operazione di Foglia");
    }
}

// Implementazione concreta di Component per composizioni
public class Composite implements Component {
    private List<Component> componenti = new ArrayList<>();

    public void aggiungi(Component componente) {
        componenti.add(componente);
    }

    @Override
    public void operazione() {
        System.out.println("Operazione di Composite");
        for (Component componente : componenti) {
            componente.operazione();
        }
    }
}

Obiettivo:

Immagina di sviluppare un videogioco in cui hai diversi tipi di unità, come singole unità (es. soldati) e gruppi di unità (es. plotoni, battaglioni). Vuoi poter trattare sia le singole unità che i gruppi di unità in modo uniforme, ad esempio per ordinarli di attaccare o difendere.

Soluzione:

Il Composite Pattern permette di creare una struttura ad albero di oggetti e di trattare gli oggetti singoli e le composizioni di oggetti in modo uniforme. Ecco come si può implementare questo pattern:
  • Definisci un'interfaccia comune per le unità del gioco.
// Interfaccia per le unità del gioco
public interface Unita {
    void attacca();
    void difendi();
}

  • Implementa una classe concreta per le unità singole
// Implementazione concreta di una singola unità (Soldato)
public class Soldato implements Unita {
    private String nome;

    public Soldato(String nome) {
        this.nome = nome;
    }

    @Override
    public void attacca() {
        System.out.println(nome + " sta attaccando.");
    }

    @Override
    public void difendi() {
        System.out.println(nome + " sta difendendo.");
    }
}

  • Implementa una classe concreta per le unità composite che possono contenere altre unità.
// Implementazione concreta di un gruppo di unità (Composite)
import java.util.ArrayList;
import java.util.List;

public class UnitaComposite implements Unita {
    private List<Unita> unita = new ArrayList<>();

    public void aggiungiUnita(Unita unita) {
        this.unita.add(unita);
    }

    public void rimuoviUnita(Unita unita) {
        this.unita.remove(unita);
    }

    @Override
    public void attacca() {
        for (Unita unita : unita) {
            unita.attacca();
        }
    }

    @Override
    public void difendi() {
        for (Unita unita : unita) {
            unita.difendi();
        }
    }
}

  • Utilizza il Composite Pattern per creare una gerarchia di unità nel gioco.
public class Gioco {
    public static void main(String[] args) {
        // Crea singole unità
        Unita soldato1 = new Soldato("Soldato 1");
        Unita soldato2 = new Soldato("Soldato 2");
        Unita soldato3 = new Soldato("Soldato 3");

        // Crea un gruppo di unità
        UnitaComposite plotone = new UnitaComposite();
        plotone.aggiungiUnita(soldato1);
        plotone.aggiungiUnita(soldato2);

        // Crea un battaglione che include il plotone e un altro soldato
        UnitaComposite battaglione = new UnitaComposite();
        battaglione.aggiungiUnita(plotone);
        battaglione.aggiungiUnita(soldato3);

        // Ordina al battaglione di attaccare
        battaglione.attacca();

        // Ordina al battaglione di difendere
        battaglione.difendi();
    }
}

OUTPUT

Soldato 1 sta attaccando.
Soldato 2 sta attaccando.
Soldato 3 sta attaccando.
Soldato 1 sta difendendo.
Soldato 2 sta difendendo.
Soldato 3 sta difendendo.

Conclusione

I Structural Design Patterns forniscono soluzioni per la composizione di oggetti in modo flessibile, il collegamento di interfacce incompatibili e l'estensione delle funzionalità degli oggetti. Utilizzando questi pattern, è possibile progettare applicazioni più modulari e manutenibili. Scegli il pattern più adatto alle esigenze specifiche del tuo progetto per migliorare la struttura e l'efficienza del codice.

Commenti

Post popolari in questo blog

#4 Space Invaders - Vola navicella....Vola - Unity Tutorial

#1 Design Patterns - Creational Design Patterns: Il Potere di Creare Oggetti con Stile

Inbox & Outbox pattern - La consegna dei messaggi ai tempi moderni