Introduzione ai modelli di design, parte 2: classici della gang di quattro rivisitati

Nella parte 1 di questa serie in tre parti che introduce i modelli di progettazione, ho fatto riferimento a Design Patterns: Elements of Reusable Object-Oriented Design . Questo classico è stato scritto da Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides, che erano conosciuti collettivamente come la Banda dei Quattro. Come la maggior parte dei lettori saprà, Design Patterns presenta 23 modelli di progettazione software che rientrano nelle categorie discusse nella Parte 1: Creazionale, strutturale e comportamentale.

Modelli di design su JavaWorld

La serie di design pattern Java di David Geary è un'introduzione magistrale a molti dei pattern Gang of Four nel codice Java.

Design Patterns è una lettura canonica per gli sviluppatori di software, ma molti nuovi programmatori sono sfidati dal suo formato e ambito di riferimento. Ciascuno dei 23 modelli è descritto in dettaglio, in un formato modello composto da 13 sezioni, che possono essere molto da digerire. Un'altra sfida per i nuovi sviluppatori Java è che i pattern Gang of Four derivano dalla programmazione orientata agli oggetti, con esempi basati su C ++ e Smalltalk, non sul codice Java.

In questo tutorial decomprimerò due dei pattern comunemente usati - Strategy e Visitor - dal punto di vista di uno sviluppatore Java. La strategia è un modello abbastanza semplice che serve come esempio di come bagnare i piedi con i modelli di design GoF in generale; Il visitatore è più complesso e di portata intermedia. Inizierò con un esempio che dovrebbe demistificare il meccanismo del doppio invio, che è una parte importante del pattern Visitor. Quindi dimostrerò il modello Visitor in un caso d'uso del compilatore.

Seguire i miei esempi qui dovrebbe aiutarti a esplorare e utilizzare gli altri pattern GoF per te stesso. Inoltre, offrirò suggerimenti per ottenere il massimo dal libro Gang of Four e concluderò con un riepilogo delle critiche sull'uso dei modelli di progettazione nello sviluppo del software. Questa discussione potrebbe essere particolarmente rilevante per gli sviluppatori che non conoscono la programmazione.

Strategia di disimballaggio

Il modello Strategia consente di definire una famiglia di algoritmi come quelli utilizzati per l'ordinamento, la composizione del testo o la gestione del layout. La strategia consente inoltre di incapsulare ogni algoritmo nella propria classe e renderli intercambiabili. Ogni algoritmo incapsulato è noto come strategia . In fase di esecuzione, un client sceglie l'algoritmo appropriato per i suoi requisiti.

Cos'è un cliente?

Un client è qualsiasi parte di software che interagisce con un modello di progettazione. Sebbene in genere sia un oggetto, un client potrebbe anche essere codice all'interno del public static void main(String[] args)metodo di un'applicazione .

A differenza del motivo Decorator, che si concentra sul cambiamento della pelle o dell'aspetto di un oggetto , Strategy si concentra sul cambiamento delle viscere dell'oggetto , ovvero sui suoi comportamenti mutevoli. La strategia consente di evitare l'utilizzo di più istruzioni condizionali spostando i rami condizionali nelle proprie classi di strategia. Queste classi spesso derivano da una superclasse astratta, a cui il client fa riferimento e utilizza per interagire con una strategia specifica.

Dal punto di vista astratto, strategia comporta Strategy, e tipi.ConcreteStrategyxContext

Strategia

Strategyfornisce un'interfaccia comune a tutti gli algoritmi supportati. Il listato 1 presenta l' Strategyinterfaccia.

Listato 1. void execute (int x) deve essere implementato da tutte le strategie concrete

public interface Strategy { public void execute(int x); }

Laddove le strategie concrete non sono parametrizzate con dati comuni, è possibile implementarle tramite la interfacefunzionalità di Java . Dove sono parametrizzati, dichiareresti invece una classe astratta. Ad esempio, le strategie di allineamento del testo con allineamento a destra, allineamento al centro e giustificazione condividono il concetto di larghezza in cui eseguire l'allineamento del testo. Quindi dichiareresti questa larghezza nella classe astratta.

ConcreteStrategy x

Ciascuno implementa l'interfaccia comune e fornisce un'implementazione dell'algoritmo. Il Listato 2 implementa l' interfaccia del Listato 1 per descrivere una specifica strategia concreta.ConcreteStrategyxStrategy

Listato 2. ConcreteStrategyA esegue un algoritmo

public class ConcreteStrategyA implements Strategy { @Override public void execute(int x) { System.out.println("executing strategy A: x = "+x); } }

Il void execute(int x)metodo nel Listato 2 identifica una strategia specifica. Pensa a questo metodo come un'astrazione per qualcosa di più utile, come un tipo specifico di algoritmo di ordinamento (ad es. Bubble Sort, Insertion Sort o Quick Sort), o un tipo specifico di gestore di layout (ad es. Flow Layout, Border Layout, o Layout della griglia).

Il listato 3 presenta una seconda Strategyimplementazione.

Listato 3. ConcreteStrategyB esegue un altro algoritmo

public class ConcreteStrategyB implements Strategy { @Override public void execute(int x) { System.out.println("executing strategy B: x = "+x); } }

Contesto

Contextfornisce il contesto in cui viene invocata la strategia concreta. Gli elenchi 2 e 3 mostrano i dati passati da un contesto a una strategia tramite un parametro del metodo. Poiché un'interfaccia di strategia generica è condivisa da tutte le strategie concrete, alcune di esse potrebbero non richiedere tutti i parametri. Per evitare parametri sprecati (specialmente quando si passano molti tipi diversi di argomenti solo a poche strategie concrete), si potrebbe invece passare un riferimento al contesto.

Invece di passare un riferimento di contesto al metodo, è possibile memorizzarlo nella classe astratta, rendendo le chiamate al metodo senza parametri. Tuttavia, il contesto dovrebbe specificare un'interfaccia più ampia che includa il contratto per l'accesso ai dati di contesto in modo uniforme. Il risultato, come mostrato nel Listato 4, è un accoppiamento più stretto tra le strategie e il loro contesto.

Listato 4. Il contesto è configurato con un'istanza ConcreteStrategyx

class Context { private Strategy strategy; public Context(Strategy strategy) { setStrategy(strategy); } public void executeStrategy(int x) { strategy.execute(x); } public void setStrategy(Strategy strategy) { this.strategy = strategy; } }

La Contextclasse nel Listato 4 memorizza una strategia quando viene creata, fornisce un metodo per modificare successivamente la strategia e fornisce un altro metodo per eseguire la strategia corrente. Fatta eccezione per il passaggio di una strategia per il costruttore, questo modello può essere visto nel java.awt classe .container, cui void setLayout(LayoutManager mgr)e void doLayout()metodi di specificare e attuare la strategia di layout manager.

StrategyDemo

Abbiamo bisogno di un cliente per dimostrare i tipi precedenti. Il Listato 5 presenta una StrategyDemoclasse client.

Listato 5. StrategyDemo

public class StrategyDemo { public static void main(String[] args) { Context context = new Context(new ConcreteStrategyA()); context.executeStrategy(1); context.setStrategy(new ConcreteStrategyB()); context.executeStrategy(2); } }

Una strategia concreta è associata a Contextun'istanza quando viene creato il contesto. La strategia può essere successivamente modificata tramite una chiamata al metodo di contesto.

Se compili queste classi ed esegui StrategyDemo, dovresti osservare il seguente output:

executing strategy A: x = 1 executing strategy B: x = 2

Rivisitazione del modello Visitor

Il visitatore è il modello di progettazione software finale che appare in Design Patterns . Sebbene questo modello comportamentale sia presentato per ultimo nel libro per ragioni alfabetiche, alcuni credono che dovrebbe arrivare per ultimo a causa della sua complessità. I nuovi arrivati ​​a Visitor spesso hanno difficoltà con questo modello di progettazione del software.

Come spiegato in Design Patterns , un visitatore consente di aggiungere operazioni alle classi senza modificarle, un po 'di magia che è facilitata dalla cosiddetta tecnica del doppio invio. Per comprendere lo schema del visitatore, dobbiamo prima digerire il doppio invio.

Cos'è il doppio invio?

Java e molti altri linguaggi supportano il polimorfismo (molte forme) tramite una tecnica nota come invio dinamico , in cui un messaggio viene mappato a una specifica sequenza di codice in fase di esecuzione. La spedizione dinamica è classificata come spedizione singola o multipla:

  • Single dispatch: Given a class hierarchy where each class implements the same method (that is, each subclass overrides the previous class's version of the method), and given a variable that's assigned an instance of one of these classes, the type can be figured out only at runtime. For example, suppose each class implements method print(). Suppose too that one of these classes is instantiated at runtime and its variable assigned to variable a. When the Java compiler encounters a.print();, it can only verify that a's type contains a print() method. It doesn't know which method to call. At runtime, the virtual machine examines the reference in variable a and figures out the actual type in order to call the right method. This situation, in which an implementation is based on a single type (the type of the instance), is known as single dispatch.
  • Multiple dispatch: Unlike in single dispatch, where a single argument determines which method of that name to invoke, multiple dispatch uses all of its arguments. In other words, it generalizes dynamic dispatch to work with two or more objects. (Note that the argument in single dispatch is typically specified with a period separator to the left of the method name being called, such as the a in a.print().)

Finally, double dispatch is a special case of multiple dispatch in which the runtime types of two objects are involved in the call. Although Java supports single dispatch, it doesn't support double dispatch directly. But we can simulate it.

Do we over-rely on double dispatch?

Blogger Derek Greer believes that using double dispatch may indicate a design issue, which could impact an application's maintainability. Read Greer's "Double dispatch is a code smell" blog post and associated comments for details.

Simulating double dispatch in Java code

Wikipedia's entry on double dispatch provides a C++-based example that shows it to be more than function overloading. In Listing 6, I present the Java equivalent.

Listing 6. Double dispatch in Java code

public class DDDemo { public static void main(String[] args) { Asteroid theAsteroid = new Asteroid(); SpaceShip theSpaceShip = new SpaceShip(); ApolloSpacecraft theApolloSpacecraft = new ApolloSpacecraft(); theAsteroid.collideWith(theSpaceShip); theAsteroid.collideWith(theApolloSpacecraft); System.out.println(); ExplodingAsteroid theExplodingAsteroid = new ExplodingAsteroid(); theExplodingAsteroid.collideWith(theSpaceShip); theExplodingAsteroid.collideWith(theApolloSpacecraft); System.out.println(); Asteroid theAsteroidReference = theExplodingAsteroid; theAsteroidReference.collideWith(theSpaceShip); theAsteroidReference.collideWith(theApolloSpacecraft); System.out.println(); SpaceShip theSpaceShipReference = theApolloSpacecraft; theAsteroid.collideWith(theSpaceShipReference); theAsteroidReference.collideWith(theSpaceShipReference); System.out.println(); theSpaceShipReference = theApolloSpacecraft; theAsteroidReference = theExplodingAsteroid; theSpaceShipReference.collideWith(theAsteroid); theSpaceShipReference.collideWith(theAsteroidReference); } } class SpaceShip { void collideWith(Asteroid inAsteroid) { inAsteroid.collideWith(this); } } class ApolloSpacecraft extends SpaceShip { void collideWith(Asteroid inAsteroid) { inAsteroid.collideWith(this); } } class Asteroid { void collideWith(SpaceShip s) { System.out.println("Asteroid hit a SpaceShip"); } void collideWith(ApolloSpacecraft as) { System.out.println("Asteroid hit an ApolloSpacecraft"); } } class ExplodingAsteroid extends Asteroid { void collideWith(SpaceShip s) { System.out.println("ExplodingAsteroid hit a SpaceShip"); } void collideWith(ApolloSpacecraft as) { System.out.println("ExplodingAsteroid hit an ApolloSpacecraft"); } }

Listing 6 follows its C++ counterpart as closely as possible. The final four lines in the main() method along with the void collideWith(Asteroid inAsteroid) methods in SpaceShip and ApolloSpacecraft demonstrate and simulate double dispatch.

Consider the following excerpt from the end of main():

theSpaceShipReference = theApolloSpacecraft; theAsteroidReference = theExplodingAsteroid; theSpaceShipReference.collideWith(theAsteroid); theSpaceShipReference.collideWith(theAsteroidReference);

The third and fourth lines use single dispatch to figure out the correct collideWith() method (in SpaceShip or ApolloSpacecraft) to invoke. This decision is made by the virtual machine based on the type of the reference stored in theSpaceShipReference.

Dall'interno collideWith(), inAsteroid.collideWith(this);utilizza il singolo invio per capire la classe corretta ( Asteroido ExplodingAsteroid) contenente il collideWith()metodo desiderato . Poiché Asteroide ExplodingAsteroidsovraccarico collideWith(), il tipo di argomento this( SpaceShipo ApolloSpacecraft) viene utilizzato per distinguere il collideWith()metodo corretto da chiamare.

E con ciò, abbiamo ottenuto un doppio invio. Per ricapitolare, in primo luogo abbiamo chiamato collideWith()in SpaceShipo ApolloSpacecraft, e poi utilizzato il suo argomento e thischiamare uno dei collideWith()metodi Asteroido ExplodingAsteroid.

Quando esegui DDDemo, dovresti osservare il seguente output: