Of Patterns and Images
Consider the following instance of the proxy pattern representing the structure of images in a website:
Correction: The load method should not be part of the class Image. There is just one load method needed, that could be placed in any of the two classes or could even be a static creation method in the real image. No client should need to know about this method.
Take a look at the cyclic dependency expressed in the following UML class diagram:
Can you think of a way to break this cycle without loosing any functionality?
Hint: there is a well-known design pattern that is typically used in situations like this.
The Art of States
Back to our coffee machine. In the last assignment, we introduced a simple framework representing the boundaries and entities of the system. In this assignment, we want to actually implement the use case
as exactly as possible. How do we achieve this?
We already mentioned that the control class is responsible for actually “running” a use case. This class defines how the system reacts to events (e.g. user actions, time passage, …) from the outside.
In most non-trivial systems, the same event may trigger different reactions depending on what
state it is currently in. On the other hand, certain events also may trigger
transitions between different states. And once we are reasoning about states and transitions between them, we certainly should take advantage of UML State Chart Diagrams.
We prepared a state chart diagram that already captures most of the BuyDrink use case.
Hint: It is a good Idea to solve this task in parallel with Task 20 - most of the what is missing in the diagrams is already implemented in the source code we prepared for you and vice versa.
State Machine “Buy Drink”
Sub State Machine “Process Order (Paper Cup)“
Sub State Machine “Pay Cash”
Now that we know
exactly how the CM2K should react to events depending on its state,
we need a way to map this knowledge into code.
To get you started, we already prepared part of the mapping for you.
The following paragraph sheds some light on our particular implementation. You probably should read it to better understand how to update the code. The remaining two paragraphs contain additional motivation for our approach, as well as some reflection about what we did so far - and more importantly why we did it. They are not essential for this task.
We use a single control class BuyDrinkControl to take care of the whole use case. However, we factor out the behaviour associated to individual
states into separate classes, to improve readability and maintainability.
We employ a variant of what is known as the
State Pattern. Applied to our system, it looks like this:
The central idea is this: BuyDrinkControl delegates the state-dependend processing of messages to an object of type ControlState that represents its current state. For (nearly) each state in the state chart diagram, we have a subtype of ControlState.
Note that in our particular variant of the pattern, the concrete states decide what transitions are to occur in response to some event. This is determined by the return value of the delegatee methods. There also exist other variants of the same pattern, where the context class, i.e. BuyDrinkControl, is responsible for this.
Implementing a state machine is really straight forward, even with plain old procedural-style programming: Use a global variable to represent the state and than apply lots of SWITCH or IF-THEN-ELSE constructs to implement the state-dependent behaviour - voilà, there's your state machine. Not very OO, though.
What we do not like about this approach, is that the connection between the source code and our model is obscured to some degree: The code realizes the behaviour associated to a given state is scattered all over the place. And vice versa:the realization of behaviour associated to many different states is tangled within each single method.
We think that the state pattern solves this problem in a rather elegant way.
That's right, traceability. Now what is that all about? Let's take a look on the artefacts we created so far:
See how all the pieces neatly fit together? At least they should. Ideally, we should be able to trace back any change in the code to some change in the requirements model - some change in a use case, some scenario being added or removed, stuff like this. On the other hand, if we change something in the model, we want to know exactly what parts of the code are affected.
Why is traceability this desirable? Because
change is the only constant, and we need to deal with it efficiently. Let's see an example.
In its current implementation, the system does not handle credit card payment yet. So we have at least one scenario that is not covered correctly. What is the problem? We check the use case - it coveres the respective scenarios allright. Next we check the state chart diagram - the respective events are correctly handled there, too. So it must be the implementation that does not adhere to the specification.
How do we find the code that needs fixing? Simple. We use the state chart diagram as a map and walk along the “path” induced by the events in the scenario. The state you arrive in when the error occurs, is the one you need to worry about. In our case, it's “wait for money” - it does not correctly handle the “insert card” event. This is what we need to find and fix in the code. Since in our case, the mapping of states to code is obvious, finding should be a matter of seconds, and fixing one of minutes.
Another example: The customer found some error in the requirements specification which requires us to change the use case model. Let's say some scenario is added. We know how to update the use case model. Next we have to run the scenario through our state machine and probably need to make some changes there to make sure it is handled correctly. Finally, we have adapt to our implementation to reflect the changes in the model. Traceability ensures that all these steps can be done with minimal effort and risk of breaking something.
There are many more examples. Yet another important one will probably be addressed in the next assignment: Unit Testing. Imagine having unit tests for all of your scenarios. Now you change something, propagate the change through your model down into the code. You run your tests. A few of them fail. No big deal: you know exactly which scenarios are affected. Walk through the model once more, fix the problem, rerun the tests, and so further.
It may sound cumbersome and time-consuming. It is. The claim we make is that it is even more time consuming to ignore traceability. The actual level of detail in your models is subject to discussion - many modern approaches suggest more light-weight models. As almost anything, this depends very much on the concrete problem at hand.