Goal: build a duck pond simulation game, where the game can show a large variety of duck species.
Problems and Potential Solutions
Solution: Let’s use standard OO techniques and created one Duck Superclass from which all other duck types inherit:
- All ducks quack and swim. The superclass takes care of the implementation code;
- the
display()method is abstract, since all duck subtypes look different; - each subtype is responsible for implementing its own
display()behaviour;
Now we want to provide the ducks the ability to fly. We add this new method in the Duck superclass:
Problem: by putting fly() in the superclass, he gave the flying ability to ALL ducks including those that shouldn’t fly. Moreover, some ducks (such as wooden decoy and rubber ducks don’t even quack).
Solution: override fly and quack methods:
Problem: you are forced to possibly override fly() and quack() for every new Duck subclass that’s ever added to the program forever.
Inheritance
Inheritance probably wasn’t the answer.
Solution: we need a clearer way to have only some (but not all) of the duck types fly or quack. We can take the fly() and quack() out of the Duck superclass, and make Flyable and Quackable interfaces with respective methods so that only the ducks that are supposed to fly will implement that interface and have those methods:
Problem: abstracting flying and quacking behaviour solves part of the problem (no inappropriately flying rubber ducks), but it makes the maintenance a nightmare. Indeed:
- since they’re interface, you need to implement code for flying and quacking behaviour for each duck types (potential duplicated code);
- whenever you need to modify a behaviour, you’re often forced to track down and change it in all the different subclasses when that behaviour is defined.
Solution:
First Design Principle
Identify the aspects of your application that vary and separate them from what stays the same.
Another way of thinking about this principle: take the parts that vary and encapsulate them, so that later you can alter or extend the parts that vary without affecting those that don’t.
In our case, since we know that fly() and quack() are the parts of the Duck class that vary across ducks, we’re going to create two sets of classes, one for fly and one for quack. Each set of classes will hold the implementation of the respective behaviour. For instance, we might have:
- a class implementing quacking
- a class implementing squeaking
- a class implementing silence
- …
What we want to do is:
- assign specific implementations of each behaviour to all possible
Duckinstances; - add the possibility to change the implementation of a specific behaviour of duck instances dinamically.
Second Design Principle
Program to an interface, not an implementation.
We’ll use an interface to represent each behaviour (such as FlyBehaviour and QuackBehaviour), and each implementation of a behaviour will implement one of those interfaces:
With this approach, the Duck subclasses will use a behaviour represented by an interface (FlyBehaviour and QuackBehaviour), so that the actual implementation of the behaviour won’t be locked into the Duck subclasses.
New Approach
From now on, the Duck behaviours will live in a separate class - a class that implements a particular interface.
That way, the Duck classes won’t need to know any of the implementation details for their own behaviours.
With this design, other types of objects can reuse our fly and quack behaviours because these behaviours are no longer hidden away in our Duck classes.
And we can add new behaviours without modifying any of our existing behaviour classes or touching any of the Duck classes that use flying behaviours.
Integrating the Duck Behaviours
Here’s the key: a Duck will now delegates its flying and quacking behaviours, instead of using quacking and flying methods defined in the Duck class (or subclass).
Add instance variables of type FlyBehaviourand QuackBehaviour
Here’s the new Duck class:
- each concrete duck object will assign to
FlyBehaviourandQuackBehaviourvariables a specific behaviour at runtime, likeFlyWithWings.
Implement performQuack and performFly
from abc import ABC, abstractmethod
class QuackBehaviour(ABC):
@abstractmethod
def quack(self):
pass
class FlyBehaviour(ABC):
@abstractmethod
def fly(self):
pass
class Duck():
def __init__(self, quackBehaviour, flyBehaviour):
self.quackBehaviour = quackBehaviour
self.flyBehaviour = flyBehaviour
def performQuack(self):
self.quackBehaviour.quack()
def performFly(self):
self.flyBehaviour.fly()To perform a quack (the same for fly), a Duck just asks the object that is referenced by quackBehaviour to quack() for it. So, we don’t care what kind of object the concrete Duck is, all we care about is that it knows how to quack().
How the quackBehaviour and flyBehaviour instance variable are set?
Let’s create concrete implementation of these behaviours and a specific type of duck called MallardDuck:
class Quack(QuackBehaviour):
def quack(self):
print("Quack! Quack!")
class Squeak(QuackBehaviour):
def quack(self):
print("Squeak! Squeak!")
class MuteQuack(QuackBehaviour):
def quack(self):
print("I'm not able to quack.")
class FlyWithWings(FlyBehaviour):
def fly(self):
print("I'm flying with wings.")
class FlyNoWay(FlyBehaviour):
def fly(self):
print("I'm not able to fly.")
class MallardDuck(Duck):
def __init__(self):
super().__init__(quackBehaviour=Quack(), flyBehaviour=FlyWithWings())
def display(self):
print("I'm a real Mallard Duck")When a MallardDuck is instantiated, its constructor initializes the duck with specific behaviors: it sets the quackBehaviour to an instance of the Quack class, which defines the sound “Quack! Quack!”, and the flyBehaviour to an instance of the FlyWithWings class, which defines the action of flying with wings. This setup allows the MallardDuck to perform its specific quacking and flying behaviors when the corresponding methods are called. Let’s prove it:
aMallardDuck = MallardDuck()
aMallardDuck.performFly()
aMallardDuck.performQuack()Output:
I'm flying with wings.
Quack! Quack!
Setting behaviour Dinamically
It might be more useful to set the duck’s behaviour type through a setter method on the Duck class, rather than instantiating in in the duck’s constructor:
class Duck():
def __init__(self, quackBehaviour, flyBehaviour):
self.quackBehaviour = quackBehaviour
self.flyBehaviour = flyBehaviour
def performQuack(self):
self.quackBehaviour.quack()
def performFly(self):
self.flyBehaviour.fly()
def setFlyBehaviour(self, fb):
self.flyBehaviour = fb
def setQuackBehaviour(self, qb):
self.quackBehaviour = qbWe can call these new methods anytime we want to change the behaviour of the duck. Let’s prove that by creating a new duck class:
class ModelDuck(Duck):
def __init__(self):
super().__init__(quackBehaviour=Quack(), flyBehaviour=FlyNoWay())
def display(self):
print("I'm a real Model Duck")
aModelDuck = ModelDuck()
aModelDuck.performFly()
aModelDuck.performQuack()Output:
I'm not able to fly.
Quack! Quack!
As expected, this new duck is not able to fly because its constructor sets the flyBehaviour to an instance of the FlyNoWay. But now we can dynamically change its flying behaviour:
aModelDuck.setFlyBehaviour(FlyWithWings())
aModelDuck.performFly()
aModelDuck.performQuack()Output:
I'm flying with wings.
Quack! Quack!
The Big Picture on Encapsulated behaviours
Here’s the entire reworked class structure:
- ducks extending
Duckclass; - fly behaviours implementing
FlyBehaviourand quack behaviours implementingQuackBehaviourNote that different arrows represent different relationship between objects:
- IS-A relationship:
- represents Inheritance;
- indicates that a subclass inherits behaviour and attributes from a superclass;
- example: a
MallardDuckis-aDuck; - it’s represented by the following arrow:
- HAS-A relationship:
- represents Composition;
- indicates that a class contains or is composed of another class;
- example: a
Duckhas-aFlyBehaviour; - it’s represented by the following arrow:
HAS-A is better than IS-A
With the HAS-A relationship, each duck has a FlyBehaviour and a QuackBehaviour to which delegates flying and quacking.
When you put two classes together like this you’re using Composition. Instead of inheriting the behaviour, the ducks get their behaviour by being composed with the right behaviour object.
Third Design Principle
Favor Composition over Inheritance.
Strategy Pattern
What we have applied is called Strategy Pattern.
Strategy Pattern Definition
The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.