Modèle de conception notable.Cette section présente des exemples de quelques modèles de conception (Design Pattern) qui sont plus représentatif et bien connu. En apprenant chacun de ces modèles, vous pourrez développer votre vocabulaire de modèle et de l'ajouter à votre boîte à outils de conception de logiciels. Chaque modèle comprend, et discuté par la suite, une description du problème, ce que le modèle permet de résoudre, les principes fondamentaux de la conception mise en œuvre dans le modèle, et les classes qui composent le modèle et comment ils travaillent ensemble. Les modèles de conception abordes sont: Adapter, Model-View-Controller, Command, Strategy, and Composite. Chaque modèle est discuté avec un texte descriptif et un diagramme montrant la structure ainsi que les exemples de classes et leur rôle dans le modele correspondant. Les points principaux, dans chaque cas est de reconnaître la façon dont ces classes collaborent à résoudre un problème spécifique. AdapterAn Adapter allows components with incompatible interfaces to communicate. The Adapter pattern is a great example of how to use object-oriented design concepts. For one reason, it's very straightforward. At the same time, it's an excellent example of three important design principles: delegation, inheritance, and abstraction. Figure 3-7 shows the class structure of the Adapter pattern as well as the example classes used in this example. The four classes that make up the Adapter pattern are the Target, Client, Adaptee, and Adapter. Again, the problem the Adapter pattern is good at solving is incompatible interfaces. In this example, the Adaptee class does not implement the target interface. The solution will be to implement an intermediary class, an Adapter, that will implement the target interface on behalf of the Adaptee. Using polymorphism, the client can use either the Target interface or the Adapter class with little concern over which is which. TargetStart off with the Target interface. The Target interface describes the behavior that your object needs to exhibit. It is possible in some cases to just implement the Target interface on the object. In some cases it is not. For example, the interface could have several methods, but you need custom behavior for only one. The java.awt package provides a Window adapter for just this purpose. Another example might be that the object you want to adapt, called the Adaptee, is vendor or legacy code that you cannot modify: package wrox.pattern.adapter; ClientNext, look at the client code using this interface. This is a simple exercise of the methods in the interface. The compete() method is dependent on the Tricks interface. You could modify it to support the Adaptee interface, but that would increase the complexity of the client code. You would rather leave the client code unmodified and make the Adaptee class work with the Tricks interface: AdapteeNow the Adaptee is the code that you need to use, but it must exhibit the Tricks interface without implementing it directly: package wrox.pattern.adapter; AdapterAs you can see from the OldDog class, it does not implement any of the methods in the Tricks interface. The next code passes the OldDog class to the Adapter, which does implement the Tricks interface: package wrox.pattern.adapter; The Adapter can be used anywhere that the Tricks interface can be used. By passing the OldDogTricksAdapter to the DogShow class, you are able to take advantage of all the code written for the Tricks interface as well as use the OldDog class unmodified. The next section of code looks at how to establish the associations and run the example: package wrox.pattern.adapter; Model-View-ControllerThe purpose of the Model-View-Controller (MVC) pattern is to separate your user interface logic from your business logic. By doing this it is possible to reuse the business logic and prevent changes in the interface from affecting the business logic. MVC, also known as Model-2, is used extensively in web development. For that reason, Chapter 8 is focused completely on this subject. You can also learn more about developing Swing clients in Chapter 4. Figure 3-8 shows the class structure of the Model-View-Controller pattern along with the classes implementing the pattern in this example. This pattern example will be a simple Swing application. The application will implement the basic login functionality. More important than the functionality is the separation of design principles that allow the model (data), controller (action), and the view (swing form) to be loosely coupled together. Model-View-Controller is actually more than a simple pattern. It is a separation of responsibilities common in application design. An application that supports the Model-View-Controller design principle needs to be able to answer three questions. How does the application change the model? How are changes to the model reflected in the view? How are the associations between the model, view, and controller classes established? The next sections show how these scenarios are implemented in this example using a Swing application. Scenario 1: Changing the ModelChanges to the model are pushed from the outside in. The example uses Java Swing to represent the interface. The user presses a button. The button fires an event, which is received by the controlling action. The action then changes the model (see Figure 3-9). Scenario 2: Refreshing When the Model ChangesThe second scenario assumes that the model has been updated by an action. The views might need to know this information, but having the model call the view directly would break the MVC separation principle requiring the model to have knowledge of the view. To overcome this, Java provides the Observer Design pattern, allowing changes from the model to "bubble out" to the view components. All views that depend on the model must register as a ChangeListener. Once registered, the views are notified of changes to the model. The notification tells the view to pull the information it needs directly from the model (see Figure 3-10). Scenario 3: Initializing the ApplicationThe third scenario shows how to initialize the action, model, and view objects and then establish dependencies between the components (see Figure 3-11). The views are registered with the model and the actions are registered with the views. The application class coordinates this. Having discussed the collaboration scenarios between the model, view, and controller components, the next sections delve into the internals of each component, starting with the model. ModelThe Model can be any Java object or objects that represent the underlying data of the application, often referred to as the domain model. This example uses a single Java object called Model. The functionality of the Model in this example is to support a login function. In a real application, the Model would encapsulate data resources such as a relational database or directory service: package wrox.pattern.mvc; The first thing of interest in the Model is the PropertyChangeSupport member variable. This is part of the Event Delegation Model (EDM) available since JDK 1.1. The EDM is an event publisher-subscriber mechanism. It allows views to register with the Model and receive notification of changes to the Model's state: private PropertyChangeSupport changeSupport= new PropertyChangeSupport(this); Notice that the setLoginStatus() method fires a property change: public void setLoginStatus(boolean status) { This addPropertyChangeListener() is the method that allows each of the views interested in the model to register and receive events: public void addPropertyChangeListener(PropertyChangeListener listener) { Notice that there are no references to any user interface components from within the Model . This ensures that the views can be changed without affecting the operations of the model. It's also possible to build a second interface. For example, you could create an API using Web Services to allow automated remote login capability. ViewThe View component of the application will consist of a Swing interface. Figure 3-12 shows what the user will see when the application is run. There are two JPanel components that make up the user interface. The first is the CenterPanel class that contains the login and password text boxes. The second is the WorkPanel that contains the login and exit command buttons as well as the CenterPanel. The CenterPanel is a typical user data entry form. It's important to notice that there is no code to process the login in this class. Its responsibility is strictly user interface: package wrox.pattern.mvc; The next user interface component, WorkPanel, contains CenterPanel. Notice that there are no references to the WorkPanel from the CenterPanel. This is an example of composition, allowing the CenterPanel to be switched out for another form or viewed in a different frame: package wrox.pattern.mvc; As you can see from the class declaration, the WorkPanel is a Swing component. In addition, it also implements the PropertyChangeListener interface. This allows the WorkPanel to register with the application model and have change notifications published to it when the Model changes. The WorkPanel is registered with the Model as a PropertyChangeListener. This allows the interface to change without affecting the domain Model, an example of low-coupled design: public class WorkPanel extends JPanel implements PropertyChangeListener { When the Model changes, the propertyChange() method is called for all classes that registered with the Model: public void propertyChange(PropertyChangeEvent evt) { The addButton() method allows you to do two things. First, you can configure any number of buttons. Second, it provides the action classes. They specify the work each performs when the button is pressed. The action represents the final part of the MVC pattern: the Controller. The Controller is discussed in the next section: ControllerThe purpose of the Controller is to serve as the gateway for making changes to the Model. In this example, the Controller consists of two java.swing.Action classes. These Action classes are registered with one or more graphical components via the components' addActionListener() method. There are two Action classes in this application. The first attempts to login with the Model. The second exits the application: package wrox.pattern.mvc; The LoginAction extends the AbstractionAction and overrides the actionPerformed() method. The actionPerformed() method is called by the component, in this case the command button, when it is pressed. The action is not limited to registration with a single user interface component. The benefit of separating out the Controller logic to a separate class is so that the action can be registered with menus, hotkeys, and toolbars. This prevents the action logic from being duplicated for each UI component: public class LoginAction extends AbstractAction { It is common for the Controller to have visibility of both the Model and the relevant views; however, the model cannot invoke the actions directly. Ensuring the separation of business and interface logic remains intact: public LoginAction(Model model, CenterPanel panel) { The ExitAction strictly controls the behavior of the user interface. It displays a message when the Exit button is pressed confirming that the application should close: package wrox.pattern.mvc; Finally, you can view the Application class. The Application class is responsible for initialization, and it creates the associations that establish the MVC separation of logic design principles: package wrox.pattern.mvc; The Swing application creates an association to the Model class, shown in the following code in the application constructor: public Application(Model model) { Then, create the Views to display the Swing interface: CenterPanel center= new CenterPanel(); Create the Action classes that represent the controller and register them with the command buttons: work.addButton("login", new LoginAction(model, center)); Use Swing housekeeping to display the application: getContentPane().add(work); The Model-View-Controller pattern is a combination of best practices in software design. It prompts a separation of concern between the user interface and business layers of an application. This example covered a number of design patterns: composition, action, and event publish-subscribe. The next pattern is the Command pattern, which provides a consistent means of handling user requests. CommandThe Command pattern provides a standard interface for handling user requests. Each request is encapsulated in an object called a command. Figure 3-13 shows the classes involved in the Command pattern. The three classes of the command pattern are the Command, CommandManager, and Invoker. The Command class represents an encapsulation of a single behavior. Each behavior in an application, such as save or delete, would be modeled as a command. In that way the behavior of an application is a collection of command objects. To add behavior to an application, all a developer needs to do is implement additional command objects. The next component in the Command pattern is the CommandManager. This class is responsible for providing access to the commands available to the application. The final component is the Invoker. The Invoker is responsible for executing the command classes in a consistent manner. The next section looks at the anatomy of the Command class. CommandThe first part of the Command pattern is the Command interface identified by a single method: package wrox.pattern.command; The life cycle is different from calling a typical method. For example, if you need to pass in an object parameter like the following method: public void getTotal(Sale) { As a command you would write the following: public CalculateSale implements Command { For the purpose of the example, use an empty command to demonstrate the interaction between the classes in this pattern: package wrox.pattern.command; The next section looks at the class that manages the command for an application. CommandManagerThe CommandManager class will process all requests. Using a HashMap, all of the commands will be initialized before requests are processed, then retrieved by name. They are stored using the add() method, and retrieved through the getCommand() method: package wrox.pattern.command; InvokerA standalone client will demonstrate the execution of the Command pattern. When the Client constructor is called it adds the DefaultCommand to the manager: package wrox.pattern.command; Here, the command mapping has been hard coded. A more robust implementation would initialize the command map from a resource file: <commands> Then, as requests are received by the invoke(String name) method, the command name is looked up in the CommandManager and the Command object is returned: public void invoke(String name) { This is an important part of most web frameworks like Struts or WebWork. In WebWork there is a specific Command pattern component called xWork, which is described in detail in Chapter 8. By handling each request as a Command object, it is possible to apply common services to each cmmand. Some common services could be things such as security, validation, and auditing. The next section of code extends the current Command pattern and implements a ManagedLifecycle interface. This interface will define a set of methods that are called during each request: package wrox.Pattern.command; The ManagedLifecycle interface is a contract between the Command object and the client code. The following is an example command that implements the ManagedLifecycle interface: package wrox.pattern.command; The following code shows initialization and invocation of two types of commands, the standard and managed: package wrox.pattern.command; A new ManagedCommand has been added to the CommandManager: manager.add("managed", new ManagedCommand()); Next, a check is put in place to determine whether the command being executed implements the ManagedLifecycle interface: if (command instanceof ManagedLifecycle) { The calling sequence of the ManagedLifecycle is richer with functionality compared with its single method version. First it passes required application data, calls the initialize method, performs validation, and then calls the execute() method.
StrategyThe Strategy pattern allows you to replace algorithms on the fly. To implement the solution, you represent each algorithm as a Strategy class. The application then delegates to the current Strategy class to execute the strategy-specific algorithm. Figure 3-14 shows the UML for the Strategy pattern alongside the example for this section. A common mistake in domain modeling is the overuse of subtyping. A subtype should be created only when a specific "is-a" type relationship can be described between a subtype and its super-type. For example, when modeling a person within a domain model, it is tempting to create a subtype for each type of person. There is no wrong way of modeling a problem, but in this case each person can take on several roles. This example looks at buyer and seller roles any person might participate in at a given time. This doesn't pass the "is-a" relationship test for subtyping. It is fitting that a person's behavior varies by his role; this concept can be expressed using the Strategy pattern. The example application in this section looks at the roles of buyers and sellers, showing how their differing behavior can be abstracted out into a strategy. Locking each person into one role or the other is a mistake. The ability to switch between the behaviors of classes in a class hierarchy is the motivation for using the Strategy pattern. Figure 3-15 shows the wrong way to model the "plays a role" relationship. The Strategy pattern is made up of an interface that defines the pluggable behavior, implementing subclasses to define the behavior and then an object to make use of the strategy. StrategyThe solution is to model each role as a class and delegate role-specific behavior from the Person class to the Role current state. First, look at the behavior that will differ by the current state object. The example uses the interface Role to declare the strategy behavior, and the two concrete classes, Buyer and Seller, to implement the differing behavior. To provide a little context to the example, the Buyer and Seller are trying to agree on a product price. The isSatisified() method is passed a Product and a Price and both parties must determine if the deal is acceptable: package wrox.pattern.strategy; Of course, the Seller and Buyer have differing objectives. The Seller is looking to make a profit, setting a 20 percent profit margin on any products sold. The following code makes that assumption: package wrox.pattern.strategy; The Buyer, on the other hand, is looking for a product that is within a spending limit. It is important to note that the Buyer class is not limited to the methods described by the Role interface, making it possible to establish the limit member variable in the Buyer class that is not present in the Seller class. The algorithm for what is acceptable is an arbitrary part of this example, but it is set so the Buyer cannot spend above the chosen limit and will not pay more that 200 percent of the initial product cost. The role of Buyer is expressed in the isSatisfied() method: package wrox.Pattern.strategy; The code example that follows uses a class for the abstraction of a product. It's a data object that is part of the scenario. The code is as follows: package wrox.pattern.strategy; The next section looks at the class that uses the pluggable strategy. ContextNext, examine the Person class that manages the Role objects. First, the Person class has an association with the Role interface. In addition, it is important to note that there is a setter and getter for the Role.This allows the person's roles to change as the program executes. It's also much cleaner code. This example uses two roles: Buyer and Seller. In the future, other Role implementing objects such as Wholesaler, Broker, and others can be added because there is no dependency to the specific subclasses: package wrox.pattern.strategy; Another key point is that the satisfied method of the Person class delegates the Role-specific behavior to its Role interface. Polymorphism allows the correct underlying object to be chosen: public boolean satisfied(Product product, double offer) { Now, the code of the pattern has been implemented. Next, view what behavior an application can exhibit by implementing this pattern. To start, you can establish Products, People, and Roles: package wrox.pattern.strategy; You are buying and selling houses. The next step is to establish initial roles and assign the roles to the people. The people will then exhibit the behavior of the role they have been assigned: tim.setRole(new Buyer(500000)); To further demonstrate the capabilities of the Strategy pattern, switch the initial Seller to the Buyer by calling setRole() on the Person object. It is possible to switch to a Buyer without modifying the Person object: allison.setRole(new Buyer(190000)); By implementing the Strategy pattern, it is possible to change an object's behavior on the fly with no effect on its implementation. This is a very powerful tool in software design. In the next section, the composite patterns build on the same principle of abstracting behavior to treat a class hierarchy with a single common interface. CompositeThe Composite design pattern allows you to treat a collection of objects as if they were one thing. In this way you can reduce the complexity of the code required if you were going to handle collections as special cases. Figure 3-16 shows the structure of the Composite pattern in conjunction with the classes implementing the pattern in this example. The example used here to demonstrate this behavior is a portfolio management system that consists of stocks and mutual funds. A mutual fund is a collection of stocks, but you would like to apply a common interface to both stocks and mutual funds to simplify the handling of both. This allows you to perform operations such as calculate Fair Market Value, buy, sell, and assess percent contribution with a common interface. The Composite pattern would clearly reduce the complexity of building these operations. The pattern consists of the Component, Leaf, and Composite classes. Figure 3-16 should look similar to Figure 3-6, where you were first introduced to the inheritance loop concept. ComponentFirst is the component interface; it declares the common interface that both the single and composite nodes will implement. The example is using fairMarketValue, an operation that can be calculated over stocks, mutual funds, and portfolios: package wrox.pattern.composite; LeafThe Leaf class represents the singular atomic data type implementing the component interface. In this example, a Stock class will represent the leaf node of the pattern. The Stock class is a leaf node in that it does not hold a reference to any other Asset objects: package wrox.pattern.composite; Stock price is calculated by multiplying share price and quantity: public double fairMarketValue() { CompositeThe following section declares the Composite object called CompositeAsset. Notice that CompositeAsset is declared abstract. A valid composite asset, such as a mutual fund or portfolio, extends this abstract class: package wrox.pattern.composite; Iterate through the child investments. If one of the child investments also happens to be a composite asset, it will be handled recursively without requiring a special case. So, for example, it would be possible to have a mutual fund comprising mutual funds: public double fairMarketValue() { Once that is complete, what follows is to build the concrete composite objects: MutualFund andPortfolio. Nothing significant is required for the MutualFund class; its behavior is inherited from theCompositeAsset: package wrox.pattern.composite; The Portfolio class extends CompositeAsset as well; the difference is that it calls the superclass directly and modifies the resulting calculation for fair market. It subtracts a two-percent management fee: package wrox.pattern.composite; The only thing left to do is exercise the code. The next class is of an Investor. The Investor is the client code taking advantage of the Composite design pattern: package wrox.pattern.composite; By calling the fair market value on the investor's portfolio, the Composite pattern will be able to traverse the collection of stocks and mutual funds to determine the value of the whole thing without worrying about the object structure: public double calcNetworth(){ With the Composite pattern, it is very easy to simplify operations over complex data structures. |