Continuing the tiled map case study, we now look again at requirement F that states that a client should be able to render a map on some output device.
Currently we have rendered the map on a console, but how might we render the tiled map on some other display technology?
we will identify some
issues with our existing rendering approach
and redesign the rendering
using a strategy pattern
to resolve these issues.
Issues with the rendering approach.
Let us look again at how we might render the tiled map on some display technology;
by "display technology" we mean the actual device or media that
presents the map to the user in some visual, aural or tactile manner
using some relevant representation for different terrain tile types.
For example, we might
- print out characters on a console;
- print out names on the console;
- display coloured squares on a JPanel;
- draw some graphic images on some canvas;
- play some audio sequences; and/or
- present some tactile representation on a pin board or Braille device.
You have already implemented a concrete render method within the TiledMap class but
this method is specific to the console display technology and particular character representation.
Consider what we would have to change if we want to
-
print different character representations for the different terrain type values; or
-
render the map on a JPanel,
drawing differently coloured squares for the different terrain tile types.
There may be many other different display technologies and/or terrain type representations
that a client may wish to use in the future.
Currently this would require re-coding of the source code base for our model;
something we wish to avoid as this requires the client to have access to the source code, be able to understand it, amend code, introduce new errors, compromise robustness and all sorts of other nasties.
Part of our development responsibilities is to identify the requirements for future flexibility and build this into our design so that
a client can introduce new funtionality in the future with minimal modifications to the existing design and code base.
Rendering with a strategy pattern.
A strategy design pattern is one of the many design patterns that are used in software development.
This will provide a flexible mechanism for including a rendering engine and, in addition,
will allow us to change the rendering engine dynamically (i.e. while the application is running).
The rendering engine is the render method but in the context of supporting a specific display technology.
Review the
strategy design pattern presentation
(opens in new window) in order to
- understand its UML,
- identify its participants,
- create an implementation, and
- apply the pattern to this specific scenario.
In this chapter we are going to see this design pattern in action by building it from a "bottom-up" perspective .
Following is a step-by-step description of how the strategy pattern for a tiled map rendering engine might be implemented:
-
Define the MapRenderer interface exposing
the single method render (TiledMap aMap).
Do this in a separate 'MapRenderer.java' file.
-
In the TiledMap class,
add the Renderer field of type MapRenderer.
In due course, this field will refer to a concrete class implementing MapRenderer for a specific rendering engine.
-
The TiledMap constructor(s) must be amended to initialise this field.
- In the existing constructor(s) initialize the field to null.
- Add additional overloaded constructor(s) based on the existing parameters but adding a MapRenderer parameter to initialise the field.
-
Include a setter for the mRenderer field; this allows us to change the rendering engine dynamically via client code.
- Add this setter method to the ITiledMap interface.
- Add the method implementation to the tiledMap class.
-
Replace the implementation of the render() method of the tiledMap class to
- do nothing if the mRenderer field is null;
- otherwise, call mRenderer.render (this)
-
The responsibility for rendering the map has now been delegated to the composed mRenderer object.
We should now have something like this ...
class TiledMap {
... ... ...
private MapRenderer mRenderer;
public void setRenderer (MapRenderer aRenderer)
{ mRenderer = aRenderer; }
... ... ...
public void render ()
{
if (mRenderer == null) return;
mRenderer.render (this);
} // end render method.
} // end TiledMap class.
-
So where is the actual rendering done?
Define a concrete class ConsoleRenderer that implements the MapRenderer interface.
class ConsoleRenderer implements MapRenderer
{
@Override
public void render (TiledMap aMap)
{
// variation on the console rendering code you have written previously
} // end render method.
} // end ConsoleRenderer class.
the implementation of the render(...) method in this class
will be similar to that in previous code but
it will not be able to directly access the TiledMap fields since it is not a member of this class.
It will require instead to use the public API of TiledMap on the this parameter.
-
Client code can now use this model in the following way:
final TiledMap map = new tiledMap (100, 600, TerrainType.GRASS, new ConsoleRenderer());
... ... ...
map.render();
... ... ...
map.setRenderer (new OtherRenderer(...) );
map.render();
Assume in the above code that a OtherRenderer class has been defined.
Draw a diagram of the variables, objects and fields created when the above code segment is executed.
-
We now have a flexible map model that allows us to "plug in" different rendering engines.
Illustrate this by creating a client that will
display a map on the console in terms of whether or not the tile areas are passable;
'.' will indicate a passable tile and 'X' an impassable tile.
Remember in an earlier chapter we should have added an isPassable() property to TerrainType.
You can do this by
- defining a rendering engine class that implements the MapRenderer interface for the required display technology & terrain tile representation;
- instantiating an object of this rendering class; and
- composing our map with this rendering object.
Note the above requires no changes to the existing source code, only to new client code.
... and finally ...
In order to clarify the use of the strategy design pattern and the different rendering mechanisms,
consider what you would have to change from your current implementation (both map model code and client code)
if the following scenario arose.
-
there is a requirement for an audio rendering of a computer lab room layout for a blind user.
Each different terrain tile value will have a different sound (spoken text and/or sound effect)
associated with it.
-
Reflect on what you would have to do as a "thought experiment";
considering what source code you would have to amend and what new files and/or code you will require to create.
The following steps are optional.
They outline the development of an implementation.
-
replace the Terraintype enumeration with one that corresponds to the scenario requirements.
-
create a new rendering engine class for the terrain tile representation defined above in some audio context.
this is the most substantive element in this development;
it will require you to use some technology that will allow you to play and control audio.
Following is an example of using the java.applet.AudioClip class that you might find useful:.
- Download the '5_audio.jar' archive file.
- Extract the archive by using the command
jar xf 5_audio.jar
from the command prompt.
- Execute the program using
java AudioClipExample
from the command prompt.
- Examine 'AudioClipExample.java' for useful code snippets.
Note that seperate audio files are used for different sounds and you will have to create these files for the sounds you need.
you will have to decide on how sounds relate to terrain tile types.
-
create a client application that will, amongst other things,
- instantiate a TiledMap object, setting it up with an appropriate map for a computer lab layout;
- instantiate the new rendering class;
- attach the new rendering engine object to the map object; and
- call the render() method of the map.
-
As always, compile, test and reflect on your code.
-
Note that no part of the existing source code base for our tiled map model requires to be modified.
Even TerrainType is not modified, it is replaced.
So all coding is new; TerrainType, the rendering engine, and the client application code.
Reflect on what this means for software maintainability, robustness and flexibility?