Uvod u dizajnerske uzorke, 2. dio: Ponovno posjećeni klasi klasi četiri

U 1. dijelu ove trodijelne serije koja uvodi uzorke dizajna, osvrnuo sam se na Obrasci dizajna: elementi višestruko objektno orijentiranog dizajna . Ovu su klasiku napisali Erich Gamma, Richard Helm, Ralph Johnson i John Vlissides, koji su bili zajednički poznati kao Banda četvorke. Kao što će većina čitatelja znati, Obrasci dizajna predstavljaju 23 uzorka dizajna softvera koji se uklapaju u kategorije o kojima se raspravlja u 1. dijelu: Kreativni, strukturni i bihevioralni.

Uzorci dizajna na JavaWorldu

Niz dizajnerskih uzoraka Javida Davida Gearyja majstorski je uvod u mnoge uzorke Gang of Four u Java kodu.

Uzorci dizajna kanonsko su štivo za programere, ali mnogi novi programeri izazivaju se referentnim formatom i opsegom. Svaki od 23 uzorka detaljno je opisan, u obliku predloška koji se sastoji od 13 odjeljaka, što može biti puno za probaviti. Još jedan izazov za nove Java programere je taj što uzorci Gang of Four potječu od objektno orijentiranog programiranja, s primjerima koji se temelje na C ++ i Smalltalk, a ne Java kodu.

U ovom uputstvu raspakirat ću dva najčešće korištena uzorka - Strategija i Posjetitelj - iz perspektive Java programera. Strategija je prilično jednostavan obrazac koji služi kao primjer kako navlažiti noge općenito GoF dizajnerskim uzorcima; Posjetitelj je složenijeg i srednjeg opsega. Počet ću s primjerom koji bi trebao demistificirati mehanizam dvostruke otpreme, što je važan dio uzorka posjetitelja. Tada ću pokazati obrazac Visitor u slučaju upotrebe kompajlera.

Slijedenje mojih primjera ovdje trebalo bi vam pomoći da istražite i iskoristite ostale GoF uzorke za sebe. Uz to, ponudit ću savjete kako izvući maksimum iz knjige Gang of Four i završiti sažetkom kritika korištenja obrazaca dizajna u razvoju softvera. Ta bi rasprava mogla biti posebno relevantna za programere koji su novi u programiranju.

Strategija raspakiranja

Uzorak Strategije omogućuje vam definiranje obitelji algoritama poput onih koji se koriste za sortiranje, sastavljanje teksta ili upravljanje izgledom. Strategija vam također omogućuje da svaki algoritam uvrstite u vlastitu klasu i učinite ih zamjenjivima. Svaki enkapsulirani algoritam poznat je kao strategija . Tijekom izvođenja klijent odabire odgovarajući algoritam za svoje zahtjeve.

Što je klijent?

Klijent je bilo koji komad softvera koji stupa u interakciju s uzorkom dizajna. Iako je obično objekt, klijent također može biti kod unutar public static void main(String[] args)metode aplikacije .

Za razliku od uzorka Dekorator, koji se fokusira na promjenu kože predmeta ili izgleda, Strategija se fokusira na promjenu crijeva predmeta , što znači na njegovo promjenjivo ponašanje. Strategija vam omogućuje izbjegavanje upotrebe više uvjetnih izjava premještanjem uvjetnih grana u njihove vlastite klase strategija. Te klase često potječu od apstraktne superklase, koju klijent navodi i koristi za interakciju s određenom strategijom.

Iz apstraktne perspektive, Strategija uključuje Strategy, te vrste.ConcreteStrategyxContext

Strategija

Strategypruža zajedničko sučelje svim podržanim algoritmima. Popis 1 predstavlja Strategysučelje.

Popis 1. void izvršenje (int x) mora biti implementirano svim konkretnim strategijama

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

Tamo gdje konkretne strategije nisu parametrizirane zajedničkim podacima, možete ih implementirati putem Javine interfaceznačajke. Tamo gdje su parametrizirani, umjesto toga biste proglasili apstraktnu klasu. Na primjer, strategije desnog poravnanja, centriranja i opravdanja poravnanja teksta dijele koncept širine u kojoj se izvodi poravnanje teksta. Tako biste ovu širinu deklarirali u apstraktnoj klasi.

ConcreteStrategy x

Svaka implementira zajedničko sučelje i pruža implementaciju algoritma. Na popisu 2 implementirano je sučelje na popisu 1 za opisivanje određene konkretne strategije.ConcreteStrategyxStrategy

Popis 2. ConcreteStrategyA izvršava jedan algoritam

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

void execute(int x)Metoda u Oglas 2 označava određenu strategiju. Zamislite ovu metodu kao apstrakciju za nešto korisnije, poput određene vrste algoritma za sortiranje (npr. Bubble Sort, Insertion Sort ili Quick Sort) ili određene vrste upravitelja izgleda (npr. Layout Flow, Border Layout ili Raspored mreže).

Popis 3 predstavlja drugu Strategyimplementaciju.

Popis 3. ConcreteStrategyB izvršava drugi algoritam

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

Kontekst

Context provides the context in which the concrete strategy is invoked. Listings 2 and 3 show data being passed from a context to a strategy via a method parameter. Because a generic strategy interface is shared by all concrete strategies, some of them may not require all parameters. To avoid wasted parameters (especially when passing many different kinds of arguments to only a few concrete strategies), you could pass a reference to the context instead.

Instead of passing a context reference to the method, you could store it in the abstract class, making your method calls parameterless. However, the context would need to specify a more extensive interface that would include the contract for accessing context data in a uniform manner. The result, as shown in Listing 4, is a tighter coupling between strategies and their context.

Listing 4. Context is configured with a ConcreteStrategyx instance

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; } }

The Context class in Listing 4 stores a strategy when it is created, provides a method to subsequently change the strategy, and provides another method to execute the current strategy. Except for passing a strategy to the constructor, this pattern can be seen in the java.awt .Container class, whose void setLayout(LayoutManager mgr) and void doLayout() methods specify and execute the layout manager strategy.

StrategyDemo

We need a client to demonstrate the previous types. Listing 5 presents a StrategyDemo client class.

Listing 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); } }

A concrete strategy is associated with a Context instance when the context is created. The strategy can be subsequently changed via a context method call.

If you compile these classes and run StrategyDemo, you should observe the following output:

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

Revisiting the Visitor pattern

Visitor is the final software design pattern to appear in Design Patterns. Although this behavioral pattern is presented last in the book for alphabetical reasons, some believe that it should come last due to its complexity. Newcomers to Visitor often struggle with this software design pattern.

As explained in Design Patterns, a visitor lets you add operations to classes without changing them, a bit of magic that is facilitated by the so-called double dispatch technique. In order to understand the Visitor pattern, we need first to digest double dispatch.

What is double dispatch?

Java and many other languages support polymorphism (many shapes) via a technique known as dynamic dispatch, in which a message is mapped to a specific sequence of code at runtime. Dynamic dispatch is classified as either single dispatch or multiple dispatch:

  • 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.

Iznutra collideWith(), inAsteroid.collideWith(this);koristi jednu slanje shvatiti na ispravan klase ( Asteroidili ExplodingAsteroid) sadrži željeni collideWith()način. Zbog Asteroidi ExplodingAsteroidpreopterećenja collideWith(), vrsta argumenta this( SpaceShipili ApolloSpacecraft) koristi se za razlikovanje ispravne collideWith()metode za pozivanje.

I time smo postigli dvostruku otpremu. To recap, najprije pozvao collideWith()na SpaceShipili ApolloSpacecraft, a zatim koristiti svoj argument i thispozvati jedan od collideWith()načina na Asteroidili ExplodingAsteroid.

Kada trčite DDDemo, trebali biste promatrati sljedeći izlaz: