Revisiting keyword abstract
In section From template method to strategy, we saw how pattern strategy could help eliminate inheritance.
Let us delve into this topic by considering another example: we are building a game which involves moving characters (class Character) on a 2-dimensional canvas (class Canvas). Each character is represented by a bitmap (class Bitmap) placed at a given position (class Position). A character is drawn on the canvas by method draw. Each character has its unique way of moving around. This is modeled by method move, which changes the character's current position and is called by the game engine at each cycle:
Let us delve into this topic by considering another example: we are building a game which involves moving characters (class Character) on a 2-dimensional canvas (class Canvas). Each character is represented by a bitmap (class Bitmap) placed at a given position (class Position). A character is drawn on the canvas by method draw. Each character has its unique way of moving around. This is modeled by method move, which changes the character's current position and is called by the game engine at each cycle:
abstract class Character { private Position currentPosition; private Bitmap bitmap; public Character(Position startingPosition, Bitmap bitmap) { this.currentPosition = startingPosition; this.bitmap = bitmap; } public void draw(Canvas canvas) { canvas.drawImage(this.currentPosition, this.bitmap); } abstract void move(); }
For sake of simplicity, we present only two kinds of characters: a witch and a cauldron. The witch can appear anywhere randomly within the boundaries of the screen, which are delimited by maximumHeight and maximumWidth:
class Witch extends Character { private int maximumHeight; private int maximumWidth; public Witch(int maximumHeight, int maximumWidth, ResourceManager resourceManager) { super(new Position(0, 0), resourceManager.getBitmapForWitch()); this.maximumHeight = maximumHeight; this.maximumWidth = maximumWidth; this.move(); } public void move() { int x = Math.random() * this.maximumWidth; int y = Math.random() * this.maximumHeight; this.currentPosition = new Position(x, y); } }
While the cauldron starts at some initial height and falls down until it reaches the ground, which is placed at some ordinate groundLevel:
class Cauldron extends Character { private int groundLevel; public Cauldron(Position startingPosition, int groundLevel, ResourceManager resourceManager) { super(startingPosition, resourceManager.getBitmapForCauldron()); this.groundLevel = groundLevel; } public void move() { int y = this.currentPosition.getY(); if (y <= this.groundLevel) return; int x = this.currentPosition.getX(); this.currentPosition = new Position(x, y - 1); } }
Abstract base class Character contains the code portions which are common to all characters. The varying parts are encoded in the subclasses. Only, two aspects can vary from a character to another:
- the movement strategy,
- the appearance (its bitmap) and initial position on the screen.
In order to eliminate the subclasses and render class Character concrete, we can thus do two things:
- implement the various movement strategies into autonomous classes,
- convert the constructor's code into pure data and let a factory do the instantiation job.
First, we introduce an interface for movements:
interface IMovement { Position computeNextPosition(Position currentPosition); }
At the moment, we have two movement strategies:
class RandomAppearance implements IMovement { private int maximumHeight; private int maximumWidth; public RandomAppearance(int maximumHeight, int maximumWidth) { this.maximumHeight = maximumHeight; this.maximumWidth = maximumWidth; } public Position computeNextPosition(Position currentPosition) { int x = Math.random() * this.maximumWidth; int y = Math.random() * this.maximumHeight; return new Position(x, y); } class FallingDown implements IMovement { private int groundLevel; public FallingDown(int groundLevel) { this.groundLevel = groundLevel; } public void computeNextPosition(Position currentPosition) { int y = currentPosition.getY(); if (y <= this.groundLevel) return; int x = currentPosition.getX(); return new Position(x, y - 1); } }
Class Character can be made concrete by changing its constructor signature. It now expects an IMovement as an additional parameter:
class Character { private Position currentPosition; private Bitmap bitmap; private IMovement movementStrategy; public Character(Position startingPosition, Bitmap bitmap, IMovement movementStrategy) { this.currentPosition = startingPosition; this.bitmap = bitmap; this.movementStrategy = movementStrategy; } public void draw(Canvas canvas) { canvas.drawImage(this.currentPosition, this.bitmap); } void move() { this.currentPosition = this.movementStrategy .computeNextPosition(this.currentPosition); } }
At last, we replace the two classes by two creation methods available on factory CharacterHatchery:
class CharacterHatchery { private ResourceManager resourceManager; public CharacterHatchery(ResourceManager resourceManager) { this.resourceManager = resourceManager; } public Character hatchCauldron(Position startingPosition, int groundLevel) { IMovement strategy = new FallingDown(groundLevel); Bitmap bitmap = this.resourceManager.getBitmapForCauldron(); return new Character(startingPosition, bitmap, strategy); } public Character hatchWitch(int maximumHeight, int maximumWidth) { IMovement strategy = new RandomAppearance(maximumHeight, maximumWidth); Bitmap bitmap = this.resourceManager.getBitmapForWitch(); Character witch = new Character(new Position(0, 0), bitmap, strategy); witch.move(); return witch; } }
How much better is this design?
We gained some flexibility: it is easy to define a cauldron which could appear randomly on the screen.
We gained some flexibility: it is easy to define a cauldron which could appear randomly on the screen.
Also, the abstract class was removed. Doing so made the dependence from the Character to its siblings explicit: class Character now holds a reference to interface IMovement. This dependence is reverse from what we would normally expect of an inheritance relationship (which usually goes from subclasses to base class). That is a common side-effect of using keyword abstract: it makes the coupling both ways.
This refactoring reduced the scope of the union as much as was possible: instead of being applied the class Character as a whole, it is restricted to method computeNextPosition only. In the end, we get a large common part with small strategies to plug into. This is exactly the philosophy of the variant data-types found in OCaml.
If you like, another example in the same vein can be read here.
Previous
Agile software development Thank you because you have been willing to share information with us. we will always appreciate all you have done here because I know you are very concerned with our.
ReplyDelete