CodeGym /Java Blog /Random-IT /Modello di progettazione strategica
John Squirrels
Livello 41
San Francisco

Modello di progettazione strategica

Pubblicato nel gruppo Random-IT
CIAO! Nella lezione di oggi parleremo del modello strategico. Nelle lezioni precedenti, abbiamo già brevemente familiarizzato con il concetto di eredità. Nel caso l'avessi dimenticato, ti ricorderò che questo termine si riferisce a una soluzione standard per un'attività di programmazione comune. In CodeGym, diciamo spesso che puoi cercare su Google la risposta a quasi tutte le domande. Questo perché il tuo compito, qualunque esso sia, probabilmente è già stato risolto con successo da qualcun altro. I modelli sono soluzioni collaudate per le attività più comuni o metodi per risolvere situazioni problematiche. Queste sono come "ruote" che non hai bisogno di reinventare da solo, ma devi sapere come e quando usarle :) Un altro scopo dei pattern è promuovere un'architettura uniforme. Leggere il codice di qualcun altro non è un compito facile! Ognuno scrive codice diverso, perché lo stesso compito può essere risolto in molti modi. Ma l'uso di modelli aiuta diversi programmatori a comprendere la logica di programmazione senza approfondire ogni riga di codice (anche quando lo vedono per la prima volta!) Oggi esaminiamo uno dei modelli di progettazione più comuni chiamato "Strategia". Modello di progettazione: Strategia - 2Immagina di scrivere un programma che lavorerà attivamente con gli oggetti Conveyance. Non importa cosa faccia esattamente il nostro programma. Abbiamo creato una gerarchia di classi con una classe genitore Conveyance e tre classi figlie: Sedan , Truck e F1Car .

public class Conveyance {

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {

       System.out.println("Braking!");
   }
}

public class Sedan extends Conveyance {
}

public class Truck extends Conveyance {
}

public class F1Car extends Conveyance {
}
Tutte e tre le classi figlie ereditano due metodi standard dal genitore: go() e stop() . Il nostro programma è molto semplice: le nostre auto possono solo andare avanti e frenare. Continuando il nostro lavoro, abbiamo deciso di dotare le auto di un nuovo metodo: fill() (che significa "riempire il serbatoio del gas"). L'abbiamo aggiunto alla classe genitore Conveyance :

public class Conveyance {

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {

       System.out.println("Braking!");
   }
  
   public void fill() {
       System.out.println("Refueling!");
   }
}
Possono davvero sorgere problemi in una situazione così semplice? Infatti hanno già... Modello di progettazione: Strategia - 3

public class Stroller extends Conveyance {

   public void fill() {
      
       // Hmm... This is a stroller for children. It doesn't need to be refueled :/
   }
}
Il nostro programma ora ha un mezzo di trasporto (un passeggino) che non si adatta bene al concetto generale. Potrebbe avere pedali o essere radiocomandato, ma una cosa è certa: non avrà spazio per versare benzina. La nostra gerarchia di classi ha causato l'ereditarietà di metodi comuni da parte di classi che non ne hanno bisogno. Cosa dovremmo fare in questa situazione? Bene, potremmo sovrascrivere il metodo fill() nella classe Stroller in modo che non accada nulla quando provi a rifornire di carburante il passeggino:

public class Stroller extends Conveyance {

   @Override
   public void fill() {
       System.out.println("A stroller cannot be refueled!");
   }
}
Ma questa difficilmente può essere definita una soluzione di successo se non altro per duplicare il codice. Ad esempio, la maggior parte delle classi utilizzerà il metodo della classe genitore, ma il resto sarà costretto a sovrascriverlo. Se abbiamo 15 classi e dobbiamo sovrascrivere il comportamento in 5-6 di esse, la duplicazione del codice diventerà piuttosto estesa. Forse le interfacce possono aiutarci? Ad esempio, in questo modo:

public interface Fillable {
  
   public void fill();
}
Creeremo un'interfaccia Fillable con un metodo fill() . Quindi, quei mezzi di trasporto che devono essere riforniti di carburante implementeranno questa interfaccia, mentre altri mezzi di trasporto (ad esempio, il nostro passeggino) no. Ma questa opzione non ci soddisfa. In futuro, la nostra gerarchia di classi potrebbe crescere fino a diventare molto ampia (immagina solo quanti diversi tipi di mezzi di trasporto ci sono nel mondo). Abbiamo abbandonato la versione precedente che prevedeva l'ereditarietà, perché non vogliamo sovrascrivere fill()metodo molte, molte volte. Ora dobbiamo implementarlo in ogni classe! E se ne avessimo 50? E se verranno apportate modifiche frequenti al nostro programma (e questo è quasi sempre vero per i programmi reali!), dovremmo correre attraverso tutte le 50 classi e modificare manualmente il comportamento di ciascuna di esse. Quindi cosa, alla fine, dovremmo fare in questa situazione? Per risolvere il nostro problema, sceglieremo un modo diverso. Vale a dire, separeremo il comportamento della nostra classe dalla classe stessa. Che cosa significa? Come sai, ogni oggetto ha uno stato (un insieme di dati) e un comportamento (un insieme di metodi). Il comportamento della nostra classe di trasporto è costituito da tre metodi: go() , stop() e fill() . I primi due metodi vanno bene così come sono. Ma sposteremo il terzo metodo fuori dal fileClasse di trasporto . Questo separerà il comportamento dalla classe (più precisamente, separerà solo una parte del comportamento, poiché i primi due metodi rimarranno dove sono). Quindi dove dovremmo mettere il nostro metodo fill() ? Non mi viene in mente niente :/ Sembra che sia esattamente dove dovrebbe essere. Lo sposteremo in un'interfaccia separata: FillStrategy !

public interface FillStrategy {

   public void fill();
}
Perché abbiamo bisogno di una tale interfaccia? È tutto semplice. Ora possiamo creare diverse classi che implementano questa interfaccia:

public class HybridFillStrategy implements FillStrategy {
  
   @Override
   public void fill() {
       System.out.println("Refuel with gas or electricity — your choice!");
   }
}

public class F1PitstopStrategy implements FillStrategy {
  
   @Override
   public void fill() {
       System.out.println("Refuel with gas only after all other pit stop procedures are complete!");
   }
}

public class StandardFillStrategy implements FillStrategy {
   @Override
   public void fill() {
       System.out.println("Just refuel with gas!");
   }
}
Abbiamo creato tre strategie comportamentali: una per le auto ordinarie, una per le ibride e una per le auto da corsa di Formula 1. Ogni strategia implementa un diverso algoritmo di rifornimento. Nel nostro caso, mostriamo semplicemente una stringa sulla console, ma ogni metodo potrebbe contenere una logica complessa. Cosa facciamo dopo?

public class Conveyance {

   FillStrategy fillStrategy;

   public void fill() {
       fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }
  
}
Utilizziamo la nostra interfaccia FillStrategy come campo nella classe genitore Conveyance . Nota che non stiamo indicando un'implementazione specifica: stiamo usando un'interfaccia. Le classi di auto avranno bisogno di implementazioni specifiche dell'interfaccia FillStrategy :

public class F1Car extends Conveyance {

   public F1Car() {
       this.fillStrategy = new F1PitstopStrategy();
   }
}

public class HybridCar extends Conveyance {

   public HybridCar() {
       this.fillStrategy = new HybridFillStrategy();
   }
}

public class Sedan extends Conveyance {

   public Sedan() {
       this.fillStrategy = new StandardFillStrategy();
   }
}

Diamo un'occhiata a quello che abbiamo!

public class Main {

   public static void main(String[] args) {

       Conveyance sedan = new Sedan();
       Conveyance hybrid = new HybridCar();
       Conveyance f1car = new F1Car();

       sedan.fill();
       hybrid.fill();
       f1car.fill();
   }
}
Uscita console:

Just refuel with gas! 
Refuel with gas or electricity — your choice! 
Refuel with gas only after all other pit stop procedures are complete!
Grande! Il processo di rifornimento funziona come dovrebbe! A proposito, nulla ci impedisce di utilizzare la strategia come parametro nel costruttore! Ad esempio, in questo modo:

public class Conveyance {

   private FillStrategy fillStrategy;

   public Conveyance(FillStrategy fillStrategy) {
       this.fillStrategy = fillStrategy;
   }

   public void fill() {
       this.fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }
}

public class Sedan extends Conveyance {

   public Sedan() {
       super(new StandardFillStrategy());
   }
}



public class HybridCar extends Conveyance {

   public HybridCar() {
       super(new HybridFillStrategy());
   }
}

public class F1Car extends Conveyance {

   public F1Car() {
       super(new F1PitstopStrategy());
   }
}
Eseguiamo il nostro metodo main() (che rimane invariato). Otteniamo lo stesso risultato! Uscita console:

Just refuel with gas! 
Refuel with gas or electricity — your choice! 
Refuel with gas only after all other pit stop procedures are complete!
Il modello di progettazione della strategia definisce una famiglia di algoritmi, li incapsula ciascuno e garantisce che siano intercambiabili. Permette di modificare gli algoritmi indipendentemente da come vengono utilizzati dal cliente (questa definizione, presa dal libro "Head First Design Patterns", mi sembra ottima). Modello di progettazione: Strategia - 4Abbiamo già specificato la famiglia di algoritmi a cui siamo interessati (modi per rifornire le auto) in interfacce separate con implementazioni diverse. Li abbiamo separati dalla macchina stessa. Ora, se dobbiamo apportare modifiche a un particolare algoritmo di rifornimento, non influirà in alcun modo sulle nostre classi di auto. E per ottenere l'intercambiabilità, dobbiamo solo aggiungere un singolo metodo setter alla nostra classe Conveyance :

public class Conveyance {

   FillStrategy fillStrategy;

   public void fill() {
       fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }

   public void setFillStrategy(FillStrategy fillStrategy) {
       this.fillStrategy = fillStrategy;
   }
}
Ora possiamo cambiare strategia al volo:

public class Main {

   public static void main(String[] args) {

       Stroller stroller= new Stroller();
       stroller.setFillStrategy(new StandardFillStrategy());

       stroller.fill();
   }
}
Se i passeggini iniziano improvvisamente a funzionare a benzina, il nostro programma sarà pronto a gestire questo scenario :) E questo è tutto! Hai imparato un altro modello di progettazione che sarà senza dubbio essenziale e utile quando lavorerai su progetti reali :) Alla prossima volta!
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION