blob: e99a0160ec773f52d8ff38527a1f06c875782b33 [file] [log] [blame]
//////////////////////////////////////////
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
//////////////////////////////////////////
= State Pattern
The http://en.wikipedia.org/wiki/State_pattern[State Pattern] provides a structured approach to partitioning the behaviour within complex systems. The overall behaviour of a system is partitioned into well-defined states. Typically, each state is implemented by a class. The overall system behaviour can be determined firstly by knowing the __current state__ of the system; secondly, by understanding the behaviour possible while in that state (as embodied in the methods of the class corresponding to that state).
== Example
Here is an example:
[source,groovy]
----
include::{projectdir}/src/spec/test/DesignPatternsTest.groovy[tags=state_example,indent=0]
----
Here is the output:
----
offline
error: not connected
connected
"Hello" sent
error: already connected
message received
offline
----
One of the great things about a dynamic language like Groovy though is that we can take this example and express it in many different ways depending on our particular needs. Some potential variations for this example are shown below.
== Variation 1: Leveraging Interface-Oriented Design
One approach we could take is to leverage http://www.pragmaticprogrammer.com/titles/kpiod/index.html[Interface-Oriented Design]. To do this, we could introduce the following interface:
[source,groovy]
----
include::{projectdir}/src/spec/test/DesignPatternsTest.groovy[tags=state_variation1_interface,indent=0]
----
Then our `Client`, `Online` and 'Offline` classes could be modified to implement that interface, e.g.:
[source,groovy]
----
include::{projectdir}/src/spec/test/DesignPatternsTest.groovy[tags=state_variation1_impl,indent=0]
----
You might ask: Haven't we just introduced additional boilerplate code? Can't we rely on duck-typing for this? The answer is 'yes' and 'no'. We can get away with duck-typing but one of the key intentions of the state pattern is to partition complexity. If we know that the __client__ class and each __state__ class all satisfy one interface, then we have placed some key boundaries around the complexity. We can look at any state class in isolation and know the bounds of behaviour possible for that state.
We don't have to use interfaces for this, but it helps express the intent of this particular style of partitioning and it helps reduce the size of our unit tests (we would have to have additional tests in place to express this intent in languages which have less support for interface-oriented design).
== Variation 2: Extract State Pattern Logic
Alternatively, or in combination with other variations, we might decide to extract some of our State Pattern logic into helper classes. For example, we could define the following classes in a state pattern package/jar/script:
[source,groovy]
----
include::{projectdir}/src/spec/test/DesignPatternsTest.groovy[tags=state_variation2_classes,indent=0]
----
This is all quite generic and can be used wherever we want to introduce the state pattern. Here is what our code would look like now:
[source,groovy]
----
include::{projectdir}/src/spec/test/DesignPatternsTest.groovy[tags=state_variation2_impl,indent=0]
----
You can see here the `startFrom` and `transitionTo` methods begin to give our example code a DSL feel.
== Variation 3: Bring on the DSL
Alternatively, or in combination with other variations, we might decide to fully embrace a Domain Specific Language (DSL) approach to this example.
We can define the following generic helper functions (first discussed http://www.bytemycode.com/snippets/snippet/640/[here]):
[source,groovy]
----
include::{projectdir}/src/spec/test/DesignPatternsTest.groovy[tags=state_variation31,indent=0]
----
[source,groovy]
----
include::{projectdir}/src/spec/test/DesignPatternsTest.groovy[tags=state_variation32,indent=0]
----
Now we can define and test our state machine like this:
[source,groovy]
----
include::{projectdir}/src/spec/test/DesignPatternsTest.groovy[tags=state_variation33,indent=0]
----
This example isn't an exact equivalent of the others. It doesn't use predefined `Online` and `Offline` classes. Instead it defines the entire state machine on the fly as needed. See the http://www.bytemycode.com/snippets/snippet/640/[previous reference] for more elaborate examples of this style.
See also: <<_model_based_testing_using_modeljunit,Model-based testing using ModelJUnit>>