Why do we need Design Patterns? - Part 2

Part 1: Why do we need Design Patterns? Inheritance isn't enough!!!

So we know using inheritance has not worked out very well, since the duck behavior keeps changing across the subclasses, and it's not appropriate for all subclasses to have those behaviors.

The Flyable and Quackable interface sounded promising at first - except Java interfaces typically have no implementation code, so no code reuse. And whenever you need to modify a behavior, you're often forced to track down and change it in all the different subclasses where that behavior is defined,probably introducing new bugs along the way :)))

image.png

After a long time, some Godfathers from US or somewhere I don't know, they invented the Design Principle:

image.png

In other words, 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.

As simple as this concept is, it forms the basis for almost every design patterns. All patterns provide a way to let some part of a system vary independently of all other parts.

image.png

Okay, time to pull the duck behavior out of the Duck classes!

Separating what changes from what stays the same

Different from the problems with fly() and quack(), the Duck class is working well and there are no other parts of it that appear to vary or change frequently. So we're going to leave the Duck class alone.

Separate the parts that change from those that stay the same

So we create two sets of classes (totally apart from Duck), one for fly and one for quack. Each set of classes will hold all the implementations of the respective behavior.

image.png

So how are we going to design the set of classes that implement the fly and quack behaviors?

We know that we want to assign behaviors to the instances of Duck, instantiate a new Duck instance with a specific type of flying behavior and then we want to change the behavior dynamically.

Let's look at the second Design Principle

image.png

Ex:

  • Programming to an implementation:

image.png

  • Programming to an interface/superclass:

image.png


We'll use an interface to represent each behavior - for instance, FlyBehavior and QuackBehavior - each implementation of a behavior will implement one of those interfaces.

image.png

So this time it won't be the Duck classes that will implement the flying and quacking interfaces. Instead, we create a set of behavior classes.

And a behavior does not come either from a concrete implementation in the superclass Duck or by providing a specialized implementation in the subclass ifself. So we do not rely on an implementation.

With the new design, the actual implementation of the behavior won't be locked into the Duck subclass.

image.png

With this design, other types of objects can reuse our fly and quack behaviors because these behaviors are no longer hidden away in our Duck classes!

And we can add new behaviors without modifying any of our existing behavior classes or touching any of the Duck classes that use flying behaviors.

Integrating the Duck Behaviors

1. We'll add two instance variables of type FlyBehavior and QuackBehavior

  • remove the fly() and quack() methods from the Duck class
  • replace them with two similar methods, called performFly() and performQuack()

image.png

2. Implement performQuack()

public abstract class Duck {
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;

    // rather than handling the quack behavior itself,
    // the Duck object delegates that behavior to the object
    // referenced by quackBehavior
    public void performQuack() {
        quackBehavior.quack();
    }
}

In this part of code, we don't care what kind of object the concrete Duck is, all we care about is that it knows how to quack().

public class VietnamDuck extends Duck {

    // VietnamDuck inherits the quackBehavior and flyBehavior instance variables
    // from class Duck
    public MallardDuck() {
        // use Quack class to handle its quack
        // so when performQuack is called, the responsibility for the quack
        // is delegated to the Quack object
        quackBehavior = new Quack();
        // similar with flyBehavior
        flyBehavior = new FlyWithWings();
    }

    public void display() {
        System.out.println("I'm a real Mallard duck");
    }
}

It looks good :)) But I said that we should NOT program to an implementation, right? But in my constructor, I am making a new instance of a concrete Quack implementation class!

Yeah, you can set the duck's behavior type through a setter method on the Duck class, rather than by instantiating it in the duck's constructor.

So I add two new methods to the Duck class

    public void setFlyBehavior(FlyBehavior fb) {
        flyBehavior = fb;
    }

    public void setQuackBehavior(QuackBehavior qb) {
        quackBehavior = qb;
    }

So just call the duck's setter method.

Take a look at a big picture

image.png

So you can see that HAS-A relationship can be better than IS-A. Instead of inheriting and implementing their behavior, the ducks get their behavior by being composed with the right behavior object.

This is an important technique, is the basis of our third design principle:

image.png

As you've seen, creating systems using composition gives you a lot more flexibility, lets you change behavior at runtime as long as the object you're composing with implements the correct behavior interface.

Summary

Phewww!!! I just applied the Strategy Pattern to solve code reuse and changing requirement problems. And I showed you that Inheritance is not enough to deal with it.

We need design patterns to create a software which is ready to scale and easy to maintain, adapt with changes.

See you in my next post!!