patterns03 - behavioural design patterns
DESCRIPTION
An introduction to behavioural design patterns in object orientation. Suitable for intermediate to advanced computing students and those studying software engineering.TRANSCRIPT
Behavioural PatternsMichael Heron
Introduction Creational patterns handle object creation. Behavioural patterns are used to simplify
the complexities of run-time object relationships.
They are often quite abstract. They trade off design complexity for
developer flexibility. The patterns we will discuss today are the
strategy, the memento and the chain of responsibility.
Structural Design Patterns All structural patterns derive from a single
guideline. Isolate variation in classes Create a separate class for each variable
part of a model. If you have a method that must change
dependant on the type of object it is working with… Consider extracting it out and making it a
class of its own.
The Strategy Pattern Imagine the following situation.
A simple role-playing game you developed earlier has taken off, big time. It’s time for an expansion.
Your expansion incorporates different kinds of character. Wizards, Witches, Rogues and Assassins
Each can attack, defend, and cast spells. However, different things can happen
depending on what class you are.
The Strategy Pattern Wizards can
Attack and cast spells, but can’t defend. Assassins can
Attack and defend, but can’t cast spells Rogues can
Attack and defend, but can’t cast spells Witches can
Defend and cast spells, but can’t attack
The Strategy Pattern Each class action is either identical to the others, or
slightly different. Everyone defends the same, but witches cast different
spells to wizards. How do you handle this?
Inheritance? Only works in limited circumstances.
Abstract classes and Interfaces Much duplication across classes.
A combination Can be highly complex and difficult to modify
Something else?
The Strategy Pattern The strategy Pattern is used to decouple the
implementation from the context. Somewhat esoteric pattern, but extremely
powerful. It works by giving each of the actions a class of
its own. DefendAction class AttackAction class SpellAction class
Each of these derive from a common base Action
The Strategy Patternpublic class CharacterType { private AttackAction myAttack; private DefendAction myDefend; private SpellsAction mySpell; public CharacterType (AttackAction a, DefendAction d, SpellsAction s) { myAttack = a; myDefend = d; mySpell = s; } pubic performAttack() { myAttack.doAttack(); } public performDefence() { myDefend.doDefence(); } public performSpell() { mySpell.castSpell(); }}
The Strategy Patternpublic class Rogue extends CharacterType() { public Rogue() { super (new StealthAttack(), new DodgeDefence(), null); }}
public class Wizard extends CharacterType() { public Wizard() { super (new StaffAttack(), null, new DefendSpell()); }}
public class Assassin extends CharacterType() { public Assassin() { super (new DaggerAttack(), new DodgeDefence(), null); }}
public class Witch extends CharacterType() { public Witch() { super (null, new ParryDefence(), new AttackSpell()); }}
The Strategy Pattern Structurally, the strategy pattern allows the
developer to resolve several systemic problems in single inheritance languages. C# and Java
At the cost of (often considerable) obfuscation of code, you gain exceptional control over the structure of objects. The easiest way of thinking about it is that
you have functions that can be swapped in and out as needed.
The Strategy Pattern This benefit extends beyond compile time.
You can actually ‘hot swap’ methods if needed. That in itself is tremendously powerful.
Imagine for example a new spell in the game we have outlined. Shapeshift into one of the other classes.
Very difficult to do well with most techniques available. Extremely simple using the strategy pattern.
Another Example Imagine our simple drawing package.
What if not only the shape was variable, but the algorithm used to draw them was too?
Strategy gives us an easy way of handling that by decoupling the algorithm used to draw the shapes from the classes. Then we can just swap in drawing
algorithms as we need them.
The Memento The memento pattern is used to capture
and restore an object’s internal state. Such as when creating a redo/undo facility
in a program. It is a two object pattern.
The originator is the object with the state we wish to capture.
The caretaker is the object that is about to make the change.
The Memento The caretaker object asks the originator
for a Memento object. This object should not be manipulated by
the caretaker. Should the caretaker wish to revert any
changes made, it passes the memento object back to the originator.
Care must be taken in relationships more complex than a 1-to-1 mapping
The Memento Implementation of the memento is very
context dependant. This is true of all patterns, but especially
so of mementos. Stacking of memento objects permits
successive ‘undo/redo’ operations. Store memento objects in a stack, pop
and push as needed
The Memento Complex object relationships may require
more complex Memento structures. Combinations of patterns can help here.
With simple objects, a Memento may be as simple as storing only a single value. For complex objects, a hashmap of values
can be an easily manipulated vehicle. Ideally state changes will be small at any
one time.
Chain of Responsibility The Chain of Responsibility pattern allows
for multiple objects interested in an event to be ‘chained’ together. Through a series of predecessor and
successor objects. In certain respects, it’s like a linked list. A message is passed to the first object in
the chain. If it can’t handle it, it passes it on to the next
Chain of Responsibilitypublic class BorrowerRisk { private BorrowerRisk predecessor; private BorrowerRisk successor; private int max; public BorrowerRisk (BorrowerRisk p, BorrowerRisk s) { predecessor = p; successor = s; public BorrowerRisk getPredecessor() { return predecessor; public BorrowerRisk getSuccessor() { return successor; } public boolean canBorrow (int amount) { if (amount > max) { if (getPredecessor() != null) { return getPredecessor().canBorrow (amount); } else { return null; } } return true; }}
Chain of Responsibilitypublic static void main (String args[]) { BorrowerRisk noRisk; BorrowerRisk lowRisk; BorrowerRisk highRisk; BorrowerRisk yeGodsRisk; boolean canBorrow; noRisk = new BorrowerRisk (null, lowRisk); lowRisk = new BorrowerRisk (noRisk, highRisk); highRisk = new BorrowerRisk (lowRisk, yeGodsRisk); yeGodsRisk = new BorrowerRisk (highRisk, null); canBorrow = noRisk.canBorrow (10000); }
Chain of Responsibility Simple overview of functionality.
Chains of Responsibility are very flexible. Easy to include:
Termination conditions Conditional passing of requests Masks and Priority levels Multi-object handling of requests
One example of use would be a flexible logging system. Messages sent along the chain with note of importance.
Each logging objects determines whether the event is important enough to handle.
Help systems are another common example.
Summary Behavioural patterns allow you to provide
effective run-time support for complex object communication needs. Some of them allow you to do things that just
can’t be done at compile time at all. The strategy pattern permits ‘hot swapping’ of
algorithms. The memento allows for easy support for
undo/redo The chain of responsibility allows for multi-
facetted handling of messages by objects.