Design Patterns Assignment 3
Chris Dent and Haobo Yang
September 23, 2002
This problem is the one we have implemented. Code is referenced below. The other problems also link to code but this one is the “official” sample. The other code was written to experiment with the designs.
KnownSpace Hydrogen API Design:
Write an EntertainmentConsole that can produce copies of a DVDPlayer, a CDPlayer, a VCRPlayer and a TapePlayer. Each of these supports starting, stopping, fast-forwarding and rewinding the media but each has a different look and feel.
A solution to the problem is to use the AbstractFactory design pattern to create players that support the same set of operations but have their own implementations. Using an AbstractFactory makes it possible to create whatever sort of player is needed next from the same client code. It also makes it easy to extend the types of players the client is able to create.
We have also included the Mediator pattern to create a context within which the logic of the interactions between operations on the players is encapsulated. This can be done in a more flexible manner (as explained in the designs for the other problems as well as the Fluency and Hydrogen docs referenced) but a Mediator suffices for this situation.
The Mediator also provides a graphical container for the player.
T Spaces: P. Wyckoff et. al, http://www.research.ibm.com/journal/sj/373/wyckoff.txt
Conversations of Chris Dent with Matt Liggett, Mary Clegg, Ailish Byrne and Nate Johnson
Write a system of classes that performs as an extensible data storage system. Initially the system should understand no operations, but should allow the easy addition of arbitrary operations. The "database" can have any format. Some operations may be compositions of other operations.
The solution to this problem is suggested by T Space (described at the reference above). In that system arbitrary tuples of information may be used as queries, operations or data.
In this solution, an ExtensibleDatabase initially has only one operation, doOperation(), that takes a String and a List of operands that may be of any class.
ExtensibleDatabase makes and keeps a reference to an OperationFactory. The OperationFactory includes a List of OperationHandlers. Different factories are available for different types of datastores.
When the ExtensibleDatabase is asked to doOperation it calls getHandler() on the OperationFactory. OperationFactory in turn calls getHandler() on the first OperationHandler.
getHandler() on OperationHandler returns either null or itself. It returns itself if the name of the operation and the format of the operands matches what it can deal with. If null is returned OperationFactory tries the next OperationHandler in the List. If the end of the list is reached and nothing has matched, an exception is thrown. This search for a handler is an example of the Chain of Responsibility pattern (GoF, p. 223).
When an OperationHandler is returned, it is passed to the ExtensibleDatabase which calls execute(List operands) on it.
Operataions may be added to the ExtensibleDatabase by creating an OperationHandler and adding it to the list on the OperationFactory.
Optimizations can be gained by improving the search for matching commands. For example, indexing based on command name and then searching by arguments would be very helpful.
Conversations of Chris Dent with Matt Ligget, Mary Clegg, Ailish Byrne, Nate Johnson
Create a system for interfacing widget actions and events such that an interface designer can experiment with linking actions on one widget with visual results on another. Any actions can be linked to any results. Do this without requiring common interfaces or recompilation to extend the set of Widgets and actions.
(Based on Fluency, see ref. above)
The solution to this problem lies in revising some of the wording. If we call the actions that occur on a source widget "events" and the results that occur on a target "actions" we are halfway there. This solution uses a modified Mediator pattern, with Observers and widget information holders called Guards.
For all the widgets available, a Guard is created. A Guard contains two maps that have information about the widget:
In this design, where state is not being considered to any great degree, the Guard can be a Singleton as the information it contains need not change.
Action classes exist which encapsulate the actions on destination widgets. These classes are Commands such as those found in the Command pattern.
WidgetListener classes exist which encapsulate the events on the source widgets. They listen for corresponding events on the source widget.
When a linkage between two widgets is required the Guards for each type of widget are created. The Guards create an instance of each the desired Widget. Each Guard is queried to get the relevant WidgetListener and Action.
A WidgetMediator is created that saves the pair. An instance of the Action is created and its target is set to the target Widget. An instance of the WidgetListener is created and is added as a listener to the source Widget. The WidgetMediator adds itself as a listener on the WidgetListener.
When the event on the source Widget fires, the WidgetListener hears it and alerts the WidgetMediator. The WidgetMediator looks in its Event Action map for an Action that corresponds with the class of WidgetListener it is hearing from. If one is found, which should happen, execute() is called on the Action.
execute() on the Action calls the proper action on the target Widget.
This implementation does not, at this point, concern itself with information exchange between source and target, for example text to put in a TextArea. As Events can be passed from the calling Widget, through the WidgetListener and to the Action, it should be possible somewhere in that chain to get the source Widget and retrieve relevant information.
Eric’s Presentation, Design-Patterns Lecture, September 2002.
Write a persistence layer for some application. This layer should allow saving of the current state of the program as well as allow layer clients to cause any persistable object to revert to a previously saved state. The persistence layer should be as transparent as possible to the application, that is, the application should work essentially as it did before it was made persistable, except that any part of it can now restore itself from previous state. To ensure that, persistable objects should not know anything about how their states are being saved and loaded, they must of course have some standard methods to produce a snapshot of their current state and to accept a state snapshot and restore themselves to that state. Further, based on the current state of the application, client objects of the persistence layer may need to save application objects to two or more databases (like switching which bucket a hose pours water into, or even hosing water into two buckets at once). For simplicity, your 'databases' need not be sophisticated; for instance, you could support writing to an XML file as well as a delimited flat file. The switching between these two 'databases', however, can happen at runtime. Further, more kinds of databases (example, Oracle) are bound to be needed for future releases and should be easy to add. Adding new databases should not affect the persistable objects' code in any way at all.
(This solution is heavily based on Eric's solution, with some adjustments to deal with composite PersistableObjects and multiple datastores.)
One solution to this problem is to use a variety of design patterns: Mediator, Memento, AbstractFactory and Singleton. These are used to create a network of PersistableObjects. Each of these PersistableObjects uses Memento to save its state via a PersistenceMediator. The Mediator uses a PersistorFactory (an AbstractFactory) to create a collection of Persistors associated with a particular datastore. The datastore does the business of saving provided state to that database.
We make the following assumptions:
The types of PersistableObjects that will be saved are limited and any changes in that collection will be reflected in the code of the PersistanceMediator. Support for additional datastores is added at compile time (as we'll need new classes for the Persistors) but choice of which datastores are used for saving is made at runtime from the set of available options.
A client creates a collection of PersistableObjects. Later the client chooses to dump the state of the set of PersistableObjects to two datastores. A PersistableObject composed of all the PersistableObjects that should be saved is created.
A StringPersistanceMediator is created. The StringPersistanceMediator has a map of datastore names to classes that implement the AbstractFactory, for example "oracle" -> OraclePersistorFactory and "text" -> TextPersistorFactory. This map could be input from a configuration file.
The state in a PersistableObject has a particular format and destination in the datastore. The PersistorFactories create enough objects to save state for the various types of PersistableObject. These objects are created as Singletons.
The StringPersistanceMediator has methods that mediate between the Memento of a PersistableObject and the Persistor that can save state. In those methods, a List of Persistors, created when the Persistors are generated, is traversed and executeSave() is called on each one to pass state to the Persistors.
Once all the Persistors have been created, the PersistableObject composite, which contains all those that are to be saved, is asked to save(). As a Composite, it asks its children Components to save.
save() returns the database ID of the entry in the database (synchronization of IDs between multiple datastores is an interesting and solvable problem which we do not address here but in general IDs will need to be created prior to data being stored. In the sample code the simple expedient of a static integer is used to create Ids. Cleary this is not a good solution.). The Composite builds a string of IDs. These IDs are saved into the datastores.
The ID of the Composite becomes the key to recovering the state of the application and must be saved for later use when state is loaded by calling the load() method.
The load() method works in much the same way: When the client chooses to revert to a saved state, the StringPersistanceMediator is called with the proper datastore attributes (only one datastore should be chosen) and load() is called on Composite PersistableObject or one of the same Composition (order is important). load() is called on the Components contained in the Composite, in reverse order to ensure that any time-based dependencies are met.