Martin Doms' Design Study

From CSSEMediaWiki
Revision as of 23:43, 29 September 2010 by Martin Doms (Talk | contribs)
Jump to: navigation, search

Contents

The problem

This design study is based on Trip, a project that my team in the COSC325 full-year Group Project built. The full system is a round-trip engineering tool that allows users to generate and modify UML diagrams from existing code, and push modifications through to the code. The program exists as a plugin for Eclipse written in Java.

The aspects of the project I am working on redesigning are the model and command infrastructure. The existing system is a transactional system, which means that the user makes arbitrary modifications to the UML representation, and at any time can commit those modifications to the code as a single unit of work.

There are two main problems with the existing system that I wish to solve. The first is in the model. The model is essentially a simplified representation of the code which is derived from a much more complex abstract syntax tree, provided by the Eclipse platform. The main problem with the current model is that due to poor design decisions early in the project (to be discussed later), modifications and extensions to the model are difficult. Many changes to the model result in bugs that would not exist in a more robust system.

The second issue is the command system. In the current system, UI actions result in the generation of Command objects which act on the model and the underlying code. For example, if the user renames a class on the UML diagram, the rename action afforded by the UI generates a rename Command object which is executed. There are no dependencies between the UI actions and the model, as the command infrastructure acts as an insulating layer between them. Commands can also be generated by events that occur on the file system, such as a file system listener creating a remove type Command object when the user deletes a source file.

Because of the transactional nature of the program, actions that occur on the UI currently generate two commands: a command that changes the UI (called a CommittableCommand) and the command to push that change into the code (called a CommitCommand). The CommittableCommand object is in charge of creating its own CommitCommand. This system worked well early in the project but as the program grew it has resulting in an explosion of CommittableCommand and CommitCommand subtypes which have become difficult to manage and often duplicate code.

The old command system became messy and unwieldy to word with. The details of this system are not relevant, this is just a demonstration of the difficulty of working with the previous design.

The Redesign

The following are two UML diagrams showing the redesign of the system, followed by an explanation of them.

Model

Newly designed model for Trip. Click image for larger view.

The model UML diagram is necessarily complex, but is not difficult to understand. JavaComponent is the base class for all elements that are represented in a Java program or source file. JavaComponents can be composed of other JavaComponents (for example, JavaClasses are composed of JavaMethods, JavaFields and inner JavaClasses). Other elements are not composed of JavaComponents, such as JavaMethods, JavaParameters and JavaFields.

It could make semantic sense to say that JavaMethods are composed of JavaParameters, but I elected to keep methods as leaf nodes that contain parameters as lists because the order of a parameter is important in a method declaration, and at the abstract level I defined child nodes in the composite as Collections, not Lists.

Decorator pattern allows us to treat types that have array or parameterized decorations as identical when it suits us, allowing for relationships like this.
The decorator pattern was used under JavaType to model arrays and generic types. The main reason for this is that for the purposes of this model we often want to treat certain types as identical to their array or generic versions, while still being able to leverage the additional behaviour and data of the decorated versions. For example, if we are modelling a class diagram in which ClassA contains a field ClassB[10], then we still want to maintain an association between ClassA and ClassB on the diagram, and it would make no sense to have a class ClassB[10] on the class diagram. The decorator pattern allows this to be done fairly easily.

JavaRelationships are simple types that relate two JavaComponents. Each relationship has a source and target JavaComponent. In a UML class diagram these would be used for things like inheritance and dependency relationships, as shown in my UML diagram. They could also be used to model relationships between packages, messaging relationships and any kind of relationship where two Java elements are involved. Relationships in this manner are implicitly directed (ie, they have a source and a target) but clients are free to ignore this directionality.

In this model, relationships are generated and instantiated lazily as needed. Client code would be advised not to store collections of relationships, but allow JavaComponents to supply the relationships on demand. For example, asking a JavaClass for all of its source relationships would result in a collection containing relationships representing its superclass connection, implementation connections and association connections sourced from itself.

Because of this behaviour, clients can easily extend the system to allow for additional relationship types. For example if the client was modelling a UML Sequence Diagram, they would be free to extend JavaRelationship with a MessageRelationship subclass and extend the behaviour of JavaType to allow for this.

In this current version of the design I have stubbed out the JavaEnum class because the current version of Trip does not support Enums. I suspect that all that is required to extend the system to support enums is one more subclass of JavaMember but I have not yet investigated this fully.

Commands

Newly designed model for Trip. For the purposes of this discussion I have shown only a very small subset of the commands which would be involved in this hierarchy, but in the final product any action the user performs that will influence the model would use one or more command objects to execute the action.

In the wider context of the project I am working on, interactions with the Model occur through commands. The benefits of this are

  • Full separation of the UI from the model. Actions on the UI result in instantiation and execution of Command objects. Changes in the model can update the UI via the Observer pattern. This allows clients to choose from a number of different presentation patterns, including Presentation Model or MVC.
  • Clients can choose to implement a command stack or use an existing command stack in another framework, allowing for simple implementation of undo and redo operations.
  • Commands can behave in a transactional manner, and commands can be composed into larger composite commands allowing for complex transactions composed from simple and easy to write commands.

In the Trip project for which this system is designed, some commands result in immediate changes in the model, while others require commands to be queued for later committing. An example of this is a Rename Class command. The class on the diagram that the user updates must be updated in the model immediately to reflect the change to the user, but the change is not pushed to code until the user commits this change.

To achieve this, we generate three commands in the action. The RenameTypeCommand renames the type in the model. A RenameTypeInCodeCommand renames the type in the code through a data layer (IModelProvider, not shown, as it is out of the scope of this design project). These two commands are added to a TransactionalCommand, which is a type of command which has an immediate and a queued component. When the TransactionalCommand is executed, it executes the immediate command immediately, and queues the queued command in the transaction with which it is associated. Either of these commands could be a compound command, so a single transactional command could have multiple effects.

One of the biggest advantages to using the Command pattern is the ability to define actions on the model in a behaviour-centric (as opposed to data-centric) way. As an example, if the user issues an action to rename a class, the user interface programmer defines this behaviour as a RenameClassCommand, rather than calling a [modelObject].setName() method. This declarative style of programming can lead to fewer errors and abstracts any difficult behaviour involved with model actions into small, well-defined classes.


Note than on the diagram I have not shown any dependencies between the model and command objects. This was done to keep the diagrams understandable. Almost every command will have some dependency or containment relationship with one or more model objects, but each command will have different dependencies because they are object-specific. The model and commands are tightly coupled necessarily.

Patterns and Practices

The following design patterns can be found in this design:

Composite

The model implements composite pattern at the top level of abstraction. JavaComponents may be either leaf or composite components, with composite components containing a collection of JavaComponents. This is used because of the tree-like nature of a Java program, with packages nested inside of projects, types nest within packages etc. The reason I didn’t use regular containment (ie, packages containing a Set of types, etc.) is because of the complex nature of this tree – the rules around which items can be nested in which are complicated. For example, classes can be nested within classes, packages within packages and classes within classes, but not packages within classes.

We can control which components are allowed to be nested in which with business rules in the addChild method.

I have also used a modified version of Composite pattern in the command system. The CompoundCommand class can be thought of as a composite of commands and can even be composed of other CompositeCommands. In this case we have not used a particular leaf class, as all non-compound commands are assumed to be leafs.

Decorator

The decorator pattern is used to add behaviour and data to generic and array types in the model. For a complete description see the Model section above. This implementation of the decorator pattern is a classical one with nothing fancy going on. I could also have chosen to use Decorator for JavaParameterizedMethod in a similar way. I elected not to do this because I do not foresee other method decorators in this system and I wanted to Eliminate irrelevant classes. The UML class diagram above does not show the full implementation of Decorator (overridden methods are not shown). This has been done for simplicity in the diagram, and you may assume that the decorator has been implemented correctly.

Command

Quite obviously, the Command system implements the Command pattern .The advantages of this were described above, in the Command section. In this implementation the following items correspond to these Gang of Four labels

  • Command: Command.
  • ConcreteCommand: Any of the concrete subclasses of command.
  • Client: The calling application.
  • Invoker: Probably supplied by the client application. This could be either the same module as Client, or more often, a command stack of some kind to allow for undo and redo operations.
  • Receiver: Model objects.

Observer

The model shown does not implement the full observer pattern, as clients are expected to supply the observers. However all model objects supply the standard Java Beans PropertyChangeSupport object to allow the Observer pattern to be used. In this case, model objects are Subjects in Gang of Four parlance, with JavaComponent being the abstract Subject and concrete implementations acting as ConcreteSubject classes.

Other important maxims and heuristics

  • Dependency inversion principle (violated) – This principle is slightly violated in this design. When children are added to concrete instances of JavaComponents, these concrete components must be aware of which kinds of concrete classes are allowable as children. For example, if the client tries to add a method as a direct child of a package, the system will throw an exception. Even though in most parts of the system we rely on abstractions as much as possible, in this case concrete classes must be aware of what other concrete classes are allowed.
  • Liskov substitution principle (violated) – This principle is violated for a similar reason to the above. We can not necessarily substitute any concrete JavaComponent for any other when generating a valid Java syntax tree.
  • Avoid concrete base classes – All base classes in this design are abstract. This allows maximum flexibility for any clients wanting to extend this model. The only exception to this rule is JavaParameterizedMethod which directly extends JavaMethod. In this case it did not seem useful to extend an abstract base JavaMethod with a concrete Method class which added no functionality (see Riel's Heuristic 3.7, Eliminate irrelevant classes).
  • Open closed principle – This principle was important from the very beginning of this design. The reason is that a model like this could easily be expected to be extended later to allow more complex and more granular models. The model is easily extensible without modification.
Personal tools