Design Pattern: Strategy Pattern in Practice

Written by jeremyzhang | Published 2022/02/15
Tech Story Tags: software-development | software-design | python-programming | software-engineering | software-architecture | oop | object-oriented-programming | inheritance

TLDRA practical example explains why and how to use strategy patterns in software design.via the TL;DR App

Developers usually spend more time maintaining and adding new features into an existing system instead of building new ones. It is so critical to have a good system design that is able to accommodate changes. In this post let’s go through one of the most adopted design patterns by starting with a problematic design and evolving gradually to make our system extensible and reusable.

What is Strategy Pattern?

A high-level explanation would be

The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently of clients that use it.

It must be confusing with the above abstraction, but don’t worry, in the following, we will walk through an example and identify the problems and make progress step by step to understand the key principles of the strategy pattern.

Consider an Example

Let’s say we want to introduce a class of Ducks, which includes redhead duck, mallard duck and etc. Each of them would have some commonly shared functionalities like, swim, display, and fly, take a minute and think about how would you address this problem in your system design? (code here)

The easy piece, right? The first thing we can leverage in OO programming is to leverage inheritance, we would have a superclass called Duck and have other subclasses inherit the functionalities of our parent class. Something like this,

class Duck(ABC):
    def __init__(self):
        pass

    @abstractmethod
    def display(self):
        pass

    @abstractmethod
    def swim(self):
        pass

    @abstractmethod
    def quack(self):
        pass


class RedheadDuck(Duck):

    def swim(self):
        return "paddling"

    def quack(self):
        return "redhead quack"

    def display(self):
        return "redhead duck"

Looks all good! But now there comes a new requirement, we want to introduce a new functionality to our ducks, which is fly. What do we do about it? We can have a fly method in our superclass and all child classes would inherit this method automatically.

class Duck(ABC):
    def __init__(self):
        pass

    @abstractmethod
    def display(self):
        pass

    @abstractmethod
    def swim(self):
        pass

    @abstractmethod
    def quack(self):
        pass

    def fly(self):
        return "fly with wings"

That seems no harm, but in the meantime, our products introduced a new kind of duck, rubber duck , by inheriting the Duck class, what we would see is,

class RubberDuck(Duck):
    def display(self):
        return "rubber duck"

    def swim(self):
        return "floating"

    def quack(self):
        return "jioooo"


rubber_duck = RubberDuck()
print(rubber_duck.fly())

# "fly with wings"

Our rubber duck is flying! Which makes no sense, what would you do to solve this issue?

Of course, we can override the fly() method in our rubber duck and make it can not fly, but is it efficient in our future development?

What if in the future we are going to introduce more non-flyable duck types, wooden duck, robotic duck , should we override the fly() method one by one?

Principle One

Here comes our first design principle.

Identify the aspects of your application that vary and separate them from what stays the same.

This is basically saying take the part that would change a lot in the system and make it independent. In our case, since fly is a method that going to vary a lot in our different duck types, we can take it out and make it a separate class and for flyable ducks, they would inherit this class if needed.

Hold on for a minute and reflect on the design, is everything right?

If we have a concrete implementation of the fly method in a separate Flyable class, then every subclass will have the same fly behaviour, what if there are other ducks fly in a different way?

To solve the above issue there comes our second principle.

Principle Two

Program to an interface, not an implementation.

The point is to exploit polymorphism by programming to a supertype so that the actual runtime object isn’t locked into the code.

With the above principles, we can have something like this,

class Duck(ABC):
    def __init__(self):
        pass

    @abstractmethod
    def display(self):
        pass

    @abstractmethod
    def swim(self):
        pass

    @abstractmethod
    def quack(self):
        pass


class Flyable(ABC):
    @abstractmethod
    def fly(self):
        pass


class RedheadDuck(Duck, Flyable):
    def swim(self):
        return "paddling"

    def quack(self):
        return "basic quack"

    def fly(self):
        return "fly with wings"

    def display(self):
        return "redhead duck"


# now rubber duck does not need to fly
class RubberDuck(Duck):
    def display(self):
        return "rubber duck"

    def swim(self):
        return "floating"

    def quack(self):
        return "jioooo"

Now the fly() method is separated away with a Flyable class, and for each of the subclass it needs to implement its own fly method, and, of course, for non-flyable type (our rubber duck), it does not need to implement the Flyable.

So far so good, but troubles never stop. Here comes a new requirement again, we have a bunch of other new ducks, bluehead, purplehead, yellowhead, … ducks, all of them need to fly and they all fly in the same way. By using our current design, we would have things like this,

class RedheadDuck(Duck, Flyable):
    ## omit other methods ...
    def fly(self):
        return "fly with wings"

    def display(self):
        return "redhead duck"


class BlueheadDuck(Duck, Flyable):
    ...
    def fly(self):
        return "fly with wings"

    def display(self):
        return "bluehead duck"

# ... and many more ducks fly in the same way

How does this look to you now? It seems clear that we can reuse the code in the fly, but however, we are not, but copy-pasting the same code everywhere.

How do we resolve this problem and make code reusable?

Now recall our definition of strategy pattern at the beginning.

The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.

Can we have a family of classes that implements the Flyable interface and each of them has different fly behaviour which is reusable in the future?

class Flyable(ABC):
    @abstractmethod
    def fly(self):
        pass


class Duck(ABC):
    @abstractmethod
    def display(self):
        pass

    @abstractmethod
    def swim(self):
        pass

    @abstractmethod
    def quack(self):
        pass


class FlyWithWings(Flyable):
    def fly(self):
        return "fly with wings"


class FlyWithRockets(Flyable):
    def fly(self):
        return "fly with rockets"


class RedheadDuck(Duck, FlyWithWings):

    def swim(self):
        return "paddling"

    def quack(self):
        return "basic quack"

    def display(self):
        return "redhead duck"


# now objects can REUSE the fly method
class BlueheadDuck(Duck, FlyWithRockets):

    def swim(self):
        return "paddling"

    def quack(self):
        return "basic quack"

    def display(self):
        return "blue head duck"

Now for every duck that needs to fly with wings can inherit the FlyWithWings class and with code reusable and without extra burden (each of the subclasses just needs to inherit what is needed).

It is all good now, and we’ve satisfied most of our requirements and changes should be easy to add into our current design, for example, if we need to introduce a backflip functionalities into some of our duck types, we can add a BackFlip behaviour interface and introduce of a family of classes implement it, with no overhead to our current system.

So is inheritance the best for code reuse?

What if our Fly classes also have a bunch of functionalities that are redundant for our subclass, inherit the Fly behaviour class would inherit all the redundant. Can we have the flexibility while removing the redundancy?

Principle Three

Favor composition over inheritance.

With composition instead of inheritance, you can encapsulate the methods that you like instead of inheriting all of them.

Let’s see how it is implemented (full code)

class Flyable(ABC):
    @abstractmethod
    def fly(self):
        pass


class Duck(ABC):
    def __init__(self):
        self.flyable: Flyable = None

    def performFly(self):
        return self.flyable.fly()

    def setFlyBehaviour(self, fly_behaviour: Flyable):
        self.flyable = fly_behaviour


class FlyWithWings(Flyable):
    def fly(self):
        return "fly with wings"


class FlyWithRockets(Flyable):
    def fly(self):
        return "fly with rockets"


class RedheadDuck(Duck):
    def __init__(self):
        super().__init__()
        self.flyable = FlyWithWings()

    def display(self):
        return "redhead duck"

We introduced a variable self.flyable in the superclass and the actual fly behaviour is implemented in the subclass,

self.flyable = FlyWithWings()

And this part is called composition, where instead of inheriting, our redhead duck is composed with the FlyWithWings behaviour and this behaviour is able to change at runtime with the performFly method.

Conclusion

What an amazing design we have achieved here! What we have done (full code),

  • Separated the Fly method that changes from the original class.
  • Defined a Flyable interface and made a collection of classes implements it, each encapsulates different flying behaviour.
  • Reduce the overhead and redundancy using composition.

The strategy pattern really helps in providing flexibility to changes and makes the code reusable.

Reference

[1] https://github.com/ksatria/MK-Design-Pattern/blob/master/Ebook/Head First Design Patterns.pdf


Written by jeremyzhang | Catch up with the world.
Published by HackerNoon on 2022/02/15