Introduction aux modèles de conception, partie 2: les classiques du gang des quatre revisités

Dans la partie 1 de cette série en trois parties présentant les modèles de conception, j'ai fait référence aux modèles de conception: éléments de conception orientée objet réutilisable . Ce classique a été écrit par Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides, connus collectivement sous le nom de Gang of Four. Comme la plupart des lecteurs le savent, Design Patterns présente 23 modèles de conception de logiciels qui s'inscrivent dans les catégories abordées dans la partie 1: Créatif, structurel et comportemental.

Modèles de conception sur JavaWorld

La série de modèles de conception Java de David Geary est une introduction magistrale à de nombreux modèles de Gang of Four dans le code Java.

Design Patterns est une lecture canonique pour les développeurs de logiciels, mais de nombreux nouveaux programmeurs sont mis au défi par son format de référence et sa portée. Chacun des 23 modèles est décrit en détail, dans un format de modèle composé de 13 sections, ce qui peut être beaucoup à digérer. Un autre défi pour les nouveaux développeurs Java est que les modèles Gang of Four proviennent de la programmation orientée objet, avec des exemples basés sur C ++ et Smalltalk, et non sur du code Java.

Dans ce didacticiel, je décompresserai deux des modèles couramment utilisés - Stratégie et Visiteur - du point de vue d'un développeur Java. La stratégie est un modèle assez simple qui sert d'exemple de la façon de se mouiller les pieds avec les modèles de conception du GoF en général; Le visiteur est plus complexe et de portée intermédiaire. Je vais commencer par un exemple qui devrait démystifier le mécanisme de double répartition, qui est une partie importante du modèle de visiteur. Ensuite, je vais démontrer le modèle de visiteur dans un cas d'utilisation du compilateur.

Suivre mes exemples ici devrait vous aider à explorer et à utiliser les autres modèles GoF pour vous-même. En outre, je vais offrir des conseils pour tirer le meilleur parti du livre Gang of Four et conclure par un résumé des critiques sur l'utilisation des modèles de conception dans le développement de logiciels. Cette discussion pourrait être particulièrement pertinente pour les développeurs novices en programmation.

Stratégie de déballage

Le modèle Stratégie vous permet de définir une famille d'algorithmes tels que ceux utilisés pour le tri, la composition de texte ou la gestion de la mise en page. La stratégie vous permet également d'encapsuler chaque algorithme dans sa propre classe et de les rendre interchangeables. Chaque algorithme encapsulé est appelé stratégie . Au moment de l'exécution, un client choisit l'algorithme approprié pour ses besoins.

Qu'est-ce qu'un client?

Un client est tout logiciel qui interagit avec un modèle de conception. Bien que généralement un objet, un client peut également être du code dans la public static void main(String[] args)méthode d' une application .

Contrairement au modèle Decorator, qui se concentre sur la modification de la peau ou de l'apparence d' un objet , Strategy se concentre sur la modification des tripes de l'objet , c'est-à-dire de ses comportements variables. La stratégie vous permet d'éviter d'utiliser plusieurs instructions conditionnelles en déplaçant les branches conditionnelles dans leurs propres classes de stratégie. Ces classes dérivent souvent d'une superclasse abstraite, que le client référence et utilise pour interagir avec une stratégie spécifique.

Du point de vue abstrait, la stratégie implique Strategy, et types.ConcreteStrategyxContext

Stratégie

Strategyfournit une interface commune à tous les algorithmes pris en charge. Le listing 1 présente l' Strategyinterface.

Listing 1. void execute (int x) doit être implémenté par toutes les stratégies concrètes

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

Lorsque les stratégies concrètes ne sont pas paramétrées avec des données communes, vous pouvez les implémenter via la interfacefonctionnalité Java . Là où ils sont paramétrés, vous déclareriez plutôt une classe abstraite. Par exemple, les stratégies d'alignement à droite, d'alignement au centre et de justification partagent le concept d'une largeur dans laquelle effectuer l'alignement du texte. Vous déclareriez donc cette largeur dans la classe abstraite.

BétonStratégie x

Chacun implémente l'interface commune et fournit une implémentation d'algorithme. Le Listing 2 implémente l' interface du Listing 1 pour décrire une stratégie concrète spécifique.ConcreteStrategyxStrategy

Listing 2. ConcreteStrategyA exécute un algorithme

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

La void execute(int x)méthode du Listing 2 identifie une stratégie spécifique. Considérez cette méthode comme une abstraction pour quelque chose de plus utile, comme un type spécifique d'algorithme de tri (par exemple, tri à bulles, tri par insertion ou tri rapide), ou un type spécifique de gestionnaire de mise en page (par exemple, disposition de flux, disposition de bordure ou Disposition de la grille).

Le listing 3 présente une seconde Strategyimplémentation.

Listing 3. ConcreteStrategyB exécute un autre algorithme

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

Le contexte

Contextfournit le contexte dans lequel la stratégie concrète est invoquée. Les listes 2 et 3 montrent des données passées d'un contexte à une stratégie via un paramètre de méthode. Étant donné qu'une interface de stratégie générique est partagée par toutes les stratégies concrètes, certaines d'entre elles peuvent ne pas nécessiter tous les paramètres. Pour éviter de gaspiller des paramètres (en particulier lorsque vous passez de nombreux types d'arguments à quelques stratégies concrètes uniquement), vous pouvez plutôt passer une référence au contexte.

Au lieu de passer une référence de contexte à la méthode, vous pouvez la stocker dans la classe abstraite, ce qui rend vos appels de méthode sans paramètre. Cependant, le contexte devrait spécifier une interface plus étendue qui inclurait le contrat d'accès aux données de contexte d'une manière uniforme. Le résultat, comme le montre le Listing 4, est un couplage plus étroit entre les stratégies et leur contexte.

Listing 4. Le contexte est configuré avec une instance 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 du listing 4 stocke une stratégie lors de sa création, fournit une méthode pour modifier ultérieurement la stratégie et fournit une autre méthode pour exécuter la stratégie actuelle. Sauf pour le passage d' une stratégie au constructeur, ce modèle peut être vu dans la classe java.awt .Conteneur, dont void setLayout(LayoutManager mgr)et void doLayout()méthodes spécifier et exécuter la stratégie de gestion de la mise en page.

StratégieDémo

Nous avons besoin d'un client pour démontrer les types précédents. Le listing 5 présente une StrategyDemoclasse de client.

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

Une stratégie concrète est associée à une Contextinstance lors de la création du contexte. La stratégie peut être modifiée ultérieurement via un appel de méthode contextuelle.

Si vous compilez ces classes et exécutez StrategyDemo, vous devez observer la sortie suivante:

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

Revisiter le modèle de visiteur

Visiteur est le dernier modèle de conception de logiciel à apparaître dans les modèles de conception . Bien que ce modèle de comportement soit présenté en dernier dans le livre pour des raisons alphabétiques, certains pensent qu'il devrait venir en dernier en raison de sa complexité. Les nouveaux arrivants dans Visitor ont souvent du mal avec ce modèle de conception de logiciel.

Comme expliqué dans Design Patterns , un visiteur vous permet d'ajouter des opérations à des classes sans les changer, un peu de magie qui est facilité par la technique dite de la double répartition. Afin de comprendre le modèle de visiteur, nous devons d'abord digérer la double distribution.

Qu'est-ce que la double expédition?

Java et de nombreux autres langages prennent en charge le polymorphisme (de nombreuses formes) via une technique connue sous le nom de répartition dynamique , dans laquelle un message est mappé à une séquence de code spécifique au moment de l'exécution. L'expédition dynamique est classée en expédition unique ou en expédition multiple:

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

De l'intérieur collideWith(), inAsteroid.collideWith(this);utilise une distribution unique pour déterminer la classe correcte ( Asteroidou ExplodingAsteroid) contenant la collideWith()méthode souhaitée . Parce que Asteroidet ExplodingAsteroidsurcharge collideWith(), le type d'argument this( SpaceShipou ApolloSpacecraft) est utilisé pour distinguer la bonne collideWith()méthode à appeler.

Et avec cela, nous avons accompli une double expédition. Pour résumer, nous avons d' abord appelé collideWith()à SpaceShipou ApolloSpacecraft, puis utilisé son argument et thisd'appeler l' une des collideWith()méthodes Asteroidou ExplodingAsteroid.

Lorsque vous exécutez DDDemo, vous devez observer la sortie suivante: