Nella pro­gram­ma­zio­ne orientata agli oggetti, i design pattern (in italiano lett. “schemi pro­get­tua­li”) sup­por­ta­no gli svi­lup­pa­to­ri con approcci e modelli ri­so­lu­ti­vi col­lau­da­ti. Una volta trovata la soluzione giusta, non resta che apportare ciascuna singola modifica. Al momento, in tutto, esistono 70 schemi pro­get­tua­li su misura per aree di ap­pli­ca­zio­ne spe­ci­fi­che. Gli strategy design pattern si con­cen­tra­no sul com­por­ta­men­to del software.

Cos’è uno strategy pattern?

Lo strategy pattern è un tipo di design pattern com­por­ta­men­ta­le (modello di com­por­ta­men­to) che offre al software diversi tipi di soluzione. Dietro le strategie c’è una famiglia di algoritmi separati dal programma vero e proprio e autonomi (= scam­bia­bi­li). Gli schemi pro­get­tua­li di strategia includono anche spe­ci­fi­che e as­si­sten­za per gli svi­lup­pa­to­ri. Gli strategy pattern de­scri­vo­no quindi come creare classi, or­ga­niz­za­re un gruppo di classi e creare oggetti. Una pe­cu­lia­ri­tà degli strategy design pattern è la pos­si­bi­li­tà di im­ple­men­ta­re un com­por­ta­men­to del programma e degli oggetti variabile anche mentre il software è in ese­cu­zio­ne.

Come appare la rap­pre­sen­ta­zio­ne UML di uno strategy pattern?

Gli strategy pattern sono ge­ne­ral­men­te pro­get­ta­ti con il lin­guag­gio di mo­del­la­zio­ne grafico UML (Unified Modeling Language). Questo presenta gli schemi pro­get­tua­li tramite una notazione stan­dar­diz­za­ta e utilizza caratteri e simboli speciali. L’UML fornisce vari tipi di diagramma per la pro­gram­ma­zio­ne orientata agli oggetti. Un diagramma di classe con almeno tre com­po­nen­ti di base viene so­li­ta­men­te scelto per rap­pre­sen­ta­re un modello di pro­get­ta­zio­ne della strategia:

  • Context (contesto o classe di contesto)
  • Strategy (strategia o classe di strategia)
  • Con­cre­te­Stra­te­gy (strategia concreta)

Nello strategy design pattern le com­po­nen­ti di base assumono funzioni speciali: i modelli di com­por­ta­men­to della classe di contesto vengono ester­na­liz­za­ti in diverse classi di strategia. Queste classi separate ospitano gli algoritmi noti come Con­cre­te­Stra­te­gy. Se ne­ces­sa­rio, il contesto può accedere alle varianti di calcolo ester­na­liz­za­te (Con­cre­te­Stra­te­gyA, Con­cre­te­Stra­te­gyB, ecc.) tramite un ri­fe­ri­men­to (interno). In tal modo non in­te­ra­gi­sce di­ret­ta­men­te con gli algoritmi, ma con un’in­ter­fac­cia.

L’in­ter­fac­cia della strategia incapsula le varianti di calcolo e può essere im­ple­men­ta­ta da tutti gli algoritmi con­tem­po­ra­nea­men­te. Per l’in­te­ra­zio­ne con il contesto, l’in­ter­fac­cia generica fornisce un unico metodo per l’at­ti­va­zio­ne degli algoritmi Con­cre­te­Stra­te­gy. Le in­te­ra­zio­ni con il contesto includono non solo il richiamo di una strategia, ma anche lo scambio di dati. L’in­ter­fac­cia stra­te­gi­ca è coinvolta inoltre nei cam­bia­men­ti di strategia che possono avvenire anche mentre un programma è in ese­cu­zio­ne.

Fatto

L’in­cap­su­la­men­to impedisce l’accesso diretto agli algoritmi e alle strutture di dati interne. Un’istanza esterna (client, contesto) può uti­liz­za­re solo calcoli e funzioni tramite in­ter­fac­ce definite rendendo così ac­ces­si­bi­li solo i metodi e i dati di un oggetto rilevanti per l’istanza esterna.

Di seguito vi spie­ghe­re­mo come viene im­ple­men­ta­to in un progetto pratico lo schema pro­get­tua­le, uti­liz­zan­do un esempio di strategy pattern.

Lo strategy pattern spiegato con un esempio

Il nostro esempio (basato sul Progetto di studio sullo Strategy Pattern di Philipp Hauer) consiste nel creare un’app di na­vi­ga­zio­ne uti­liz­zan­do uno schema pro­get­tua­le di strategia. L’app deve calcolare un percorso basato sui normali mezzi di trasporto. L’utente può scegliere fra tre opzioni:

  • Pedone (Con­cre­te­Stra­te­gyA)
  • Auto (Con­cre­te­Stra­te­gyB)
  • Trasporto locale (Con­cre­te­Stra­te­gyC)

Tra­sfe­ren­do queste spe­ci­fi­che su un grafico UML, la struttura e la fun­zio­na­li­tà dello strategy pattern richiesto diventano chiare:

Nel nostro esempio, il client è l’in­ter­fac­cia utente grafica (Graphical User Inferface, GUI) di un’app di na­vi­ga­zio­ne con comandi per il calcolo di percorso. Se l’utente effettua una selezione tramite un comando, viene calcolato un percorso specifico. Il contesto (classe Navigator) ha il compito di calcolare e vi­sua­liz­za­re una serie di punti di controllo sulla mappa. La classe Navigator ha un metodo specifico per cambiare la strategia di calcolo percorso attiva. Ciò significa che è possibile passare fa­cil­men­te da una modalità di trasporto all’altra uti­liz­zan­do i comandi del client.

Se, ad esempio, viene attivato un comando cor­ri­spon­den­te con il tasto pedone sul client, viene richiesto il servizio “Calcola percorso pedonale” (Con­cre­te­Stra­te­gyA). Il metodo exe­cu­teAl­go­ri­thm() (nel nostro esempio il metodo: cal­co­la­Per­cor­so (A, B)) accetta un’origine e una de­sti­na­zio­ne e produce una raccolta dei punti di controllo della rotta. Il contesto riceve il comando del client e, in base alle linee guida definite in pre­ce­den­za (Policy), decide la strategia ap­pro­pria­ta (se­tStra­te­gy: Pedone). Tramite Call delega invece la richiesta all’oggetto-strategia e alla sua in­ter­fac­cia.

Con ge­tStra­te­gy() si memorizza la strategia at­tual­men­te se­le­zio­na­ta nel contesto (classe del na­vi­ga­to­re). I risultati dei calcoli Con­cre­te­Stra­te­gy con­flui­sco­no nell’ulteriore ela­bo­ra­zio­ne e nella vi­sua­liz­za­zio­ne grafica del percorso nella app di na­vi­ga­zio­ne. Se l’utente sceglie un percorso diverso, ad esempio facendo clic sul pulsante “Auto” in un secondo momento, il contesto cambia nella strategia richiesta (Con­cre­te­Stra­te­gyB) e avvia un nuovo calcolo tramite un’ulteriore Call. Alla fine della procedura, viene emessa una de­scri­zio­ne del percorso mo­di­fi­ca­ta per il mezzo di trasporto auto.

Nel nostro esempio, la meccanica del pattern può essere im­ple­men­ta­ta con un codice re­la­ti­va­men­te chiaro:

Contesto:

publicclassContext {
    //valore standard predefinito (comportamento predefinito): ConcreteStrategyA
private Strategy strategy = new ConcreteStrategyA(); 
public void execute() { 
//delega il comportamento a un oggetto-strategia
strategy.executeAlgorithm(); 
    }
public void setStrategy(Strategy strategy) {
strategy = strategy;
    }
public Strategy getStrategy() { 
returnstrategy; 
} 
}

Strategy, Con­cre­te­Stra­te­gyA, Con­cre­te­Stra­te­gyB:

interface Strategy { 
public void executeAlgorithm(); 
} 
classConcreteStrategyA implements Strategy { 
public void executeAlgorithm() { 
System.out.println("Concrete Strategy A"); 
    } 
} 
classConcreteStrategyB implements Strategy { 
public void executeAlgorithm() { 
System.out.println("Concrete Strategy B"); 
} 
}

Client:

public class Client {
public static void main(String[] args) {
//Comportamento predefinito
Contextcontext = new Context();
context.execute();
//Modificare il comportamento
context.setStrategy(new ConcreteStrategyB());
context.execute();
}
}

Quali sono i vantaggi e gli svantaggi degli strategy pattern?

I vantaggi di uno strategy pattern diventano evidenti assumendo la pro­spet­ti­va di un pro­gram­ma­to­re e am­mi­ni­stra­to­re di sistema. In generale, la sud­di­vi­sio­ne in moduli e classi autonomi comporta una migliore strut­tu­ra­zio­ne del codice del programma. Nelle sotto-aree de­li­mi­ta­te, il pro­gram­ma­to­re della nostra app di esempio può gestire segmenti di codice più snelli. In questo modo, l’ambito della classe na­vi­ga­to­re può essere ridotto ester­na­liz­zan­do le strategie e non è ne­ces­sa­rio creare sot­to­clas­si nell’ambito del contesto.

Poiché le di­pen­den­ze interne dei segmenti rimangono all’interno del quadro in un codice più snello e ben de­li­mi­ta­to, le modifiche hanno effetti minori. Quindi raramente è ne­ces­sa­ria un’ulteriore, e po­ten­zial­men­te lunga, ri­pro­gram­ma­zio­ne; in alcuni casi può essere anche evitata del tutto. Si può inoltre mantenere più fa­cil­men­te segmenti di codice più chiari nel lungo termine, e quindi sem­pli­fi­ca­re l’iden­ti­fi­ca­zio­ne e la diagnosi dei problemi.

Questo comporta vantaggi anche per il fun­zio­na­men­to, poiché l’app di esempio può avere un’in­ter­fac­cia intuitiva. Uti­liz­zan­do i comandi, gli utenti sono in grado di con­trol­la­re fa­cil­men­te il com­por­ta­men­to del programma (calcolo di percorso) e scegliere fra le varie opzioni.

Poiché il contesto dell’app di na­vi­ga­zio­ne in­te­ra­gi­sce solo con un’in­ter­fac­cia at­tra­ver­so l’in­cap­su­la­men­to degli algoritmi, risulta in­di­pen­den­te dall’im­ple­men­ta­zio­ne concreta dei singoli algoritmi. In caso di modifiche agli algoritmi o in­tro­du­zio­ne di nuove strategie in un secondo momento, il codice del contesto non deve essere ne­ces­sa­ria­men­te mo­di­fi­ca­to. In questo modo, volendo, il calcolo di percorso può essere integrato in modo rapido e semplice con ulteriori Con­cre­te­Stra­te­gy per rotte aeree, trasporto marittimo e traffico a lunga distanza. Le nuove strategie devono sem­pli­ce­men­te im­ple­men­ta­re in modo corretto la strategy interface.

Gli strategy pattern fa­ci­li­ta­no la già difficile pro­gram­ma­zio­ne del software orientato agli oggetti grazie a un’altra ca­rat­te­ri­sti­ca positiva: con­sen­to­no la pro­get­ta­zio­ne di software (moduli) riu­ti­liz­za­bi­li­più volte, il cui sviluppo è con­si­de­ra­to par­ti­co­lar­men­te im­pe­gna­ti­vo. Le classi di contesto correlate possono uti­liz­za­re anche le strategie ester­na­liz­za­te per il calcolo di percorso tramite l’in­ter­fac­cia e non devono più im­ple­men­tar­le da sole.

No­no­stan­te i suoi numerosi vantaggi, gli strategy pattern pre­sen­ta­no però anche alcuni svantaggi. A causa della sua struttura più complessa, la pro­get­ta­zio­ne del software può creare ri­don­dan­ze e inef­fi­cien­ze nella co­mu­ni­ca­zio­ne interna. L’in­ter­fac­cia della strategia generica, che tutti gli algoritmi devono im­ple­men­ta­re allo stesso modo, può quindi risultare so­vra­di­men­sio­na­ta nei singoli casi.

Ecco un esempio: dopo che il contesto ha creato e ini­zia­liz­za­to alcuni parametri, li tra­sfe­ri­sce all’in­ter­fac­cia generica e al metodo in essa definito. La strategia im­ple­men­ta­ta alla fine non necessita ne­ces­sa­ria­men­te di tutti i parametri con­te­stua­li co­mu­ni­ca­ti e quindi non li elabora. Pertanto, l’in­ter­fac­cia fornita non viene sempre uti­liz­za­ta in modo ottimale nello strategy pattern e non è sempre possibile evitare un maggiore sforzo di co­mu­ni­ca­zio­ne con tra­sfe­ri­men­ti di dati non necessari.

Durante l’im­ple­men­ta­zio­ne, c’è anche una stretta di­pen­den­za interna tra il client e la strategia. Poiché il client effettua la selezione e richiede la strategia concreta con un trigger (nel nostro esempio il calcolo del percorso pedonale), deve conoscere le Con­cre­te­Stra­te­gy. Pertanto, è ne­ces­sa­rio uti­liz­za­re questo schema pro­get­tua­le solo se i cam­bia­men­ti nella strategia e nel com­por­ta­men­to sono im­por­tan­ti o fon­da­men­ta­li per l’uso e le fun­zio­na­li­tà del software.

Alcuni degli svantaggi elencati possono essere evitati o com­pen­sa­ti. Il numero di istanze di oggetti che possono ve­ri­fi­car­si in gran numero nello strategy pattern viene spesso ridotto im­ple­men­tan­do un pattern Flyweight. Questa misura ha anche un effetto positivo sull’ef­fi­cien­za e sui requisiti di memoria di un’ap­pli­ca­zio­ne.

Quando si uti­liz­za­no gli strategy pattern?

Come schema pro­get­tua­le di base nello sviluppo del software, lo strategy design pattern non è limitato a un’area specifica di ap­pli­ca­zio­ne. Piuttosto, la natura del problema è decisiva per l’uso del design pattern. Il software che deve risolvere attività e problemi imminenti con va­ria­bi­li­tà, opzioni di com­por­ta­men­to e modifiche, è spe­cia­liz­za­to nello schema pro­get­tua­le.

I programmi che offrono diversi formati di ar­chi­via­zio­ne file o varie funzioni di or­ga­niz­za­zio­ne e ricerca uti­liz­za­no schemi pro­get­tua­li di strategia. Anche nell’area di com­pres­sio­ne dati vengono uti­liz­za­ti programmi che im­ple­men­ta­no diversi algoritmi di com­pres­sio­ne sulla base del schema pro­get­tua­le. Quindi ad esempio è possibile con­ver­ti­re i video nel formato di file sal­va­spa­zio de­si­de­ra­to o ri­pri­sti­na­re i file di archivio compressi (come i file ZIP o RAR) al loro stato originale uti­liz­zan­do speciali strategie di de­com­pres­sio­ne. Un altro esempio potrebbe essere il sal­va­tag­gio di un documento o di un’immagine in diversi formati di file.

Il design pattern è anche coinvolto nello sviluppo e nell’im­ple­men­ta­zio­ne del software di gioco dove deve, ad esempio, reagire in modo fles­si­bi­le alle mutevoli si­tua­zio­ni di gioco in fase di ese­cu­zio­ne. Le at­trez­za­tu­re speciali, i vari per­so­nag­gi, i loro modelli di com­por­ta­men­to o le loro mosse par­ti­co­la­ri possono essere me­mo­riz­za­ti tutti sotto forma di Con­cre­te­Stra­te­gy.

Un’altra area di ap­pli­ca­zio­ne degli strategy pattern sono i software di con­ta­bi­li­tà. Scam­bian­do Con­cre­te­Stra­te­gy si riesce essere fa­cil­men­te ad adattare i tassi di calcolo a gruppi pro­fes­sio­na­li, paesi e regioni. Inoltre, i programmi che traducono i dati in vari formati grafici (come ad esempio grafici a linee, a torta o a barre) uti­liz­za­no strategy pattern.

Troviamo poi ap­pli­ca­zio­ni più spe­ci­fi­che degli strategy pattern nella libreria standard Java (API Java) e nei toolkit GUI Java (ad esempio AWT, Swing e SWT), che uti­liz­za­no un gestore di layout per svi­lup­pa­re e creare in­ter­fac­ce utente grafiche. Questo permette di im­ple­men­ta­re diverse strategie per la di­spo­si­zio­ne delle com­po­nen­ti durante lo sviluppo dell’in­ter­fac­cia. Esistono ulteriori ap­pli­ca­zio­ni degli strategy design pattern nei sistemi di database, driver di di­spo­si­ti­vo e programmi server.

Una pa­no­ra­mi­ca delle proprietà più im­por­tan­ti degli strategy pattern

Nella vasta gamma di design pattern, lo schema pro­get­tua­le di strategia è ca­rat­te­riz­za­to dalle seguenti proprietà:

  • orientato al com­por­ta­men­to (il com­por­ta­men­to e le relative modifiche sono più facili da pro­gram­ma­re e im­ple­men­ta­re, e le modifiche possono essere apportate anche mentre un programma è in ese­cu­zio­ne)
  • orientato all’ef­fi­cien­za (l’ester­na­liz­za­zio­ne sem­pli­fi­ca e ottimizza il codice e la sua ma­nu­ten­zio­ne)
  • orientato al futuro (le modifiche e ot­ti­miz­za­zio­ni possono essere fa­cil­men­te im­ple­men­ta­te anche a medio e lungo termine)
  • mira all’espan­di­bi­li­tà (favorita dal sistema modulare e dall’in­di­pen­den­za di oggetti e classi)
  • mira alla riu­sa­bi­li­tà (ad es. uso multiplo di strategie)
  • mira a ot­ti­miz­za­re l’usabilità, il controllo e la con­fi­gu­ra­bi­li­tà del software
  • richiede con­si­de­ra­zio­ni con­cet­tua­li ap­pro­fon­di­te (cosa, come e dove può essere ester­na­liz­za­to nelle classi di strategia)
In sintesi

Gli strategy pattern con­sen­to­no uno sviluppo software ef­fi­cien­te ed economico nella pro­gram­ma­zio­ne orientata agli oggetti con soluzioni di problemi su misura. Eventuali modifiche e mi­glio­ra­men­ti sono preparati in modo ottimale già nella fase di pro­get­ta­zio­ne. Il sistema, orientato alla va­ria­bi­li­tà e alla di­na­mi­ci­tà, può essere gestito e con­trol­la­to com­ples­si­va­men­te al meglio. Errori e in­con­gruen­ze si risolvono più ra­pi­da­men­te. Le com­po­nen­ti riu­ti­liz­za­bi­li e in­ter­scam­bia­bi­li con­sen­to­no di ri­spar­mia­re sui costi di sviluppo, so­prat­tut­to in progetti complessi con una pro­spet­ti­va a lungo termine. Tuttavia, è im­por­tan­te trovare la giusta quantità. Non è raro che gli schemi pro­get­tua­li vengano usati con un’eccessiva par­si­mo­nia o troppo spesso.

Vai al menu prin­ci­pa­le