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 :)))
After a long time, some Godfathers from US or somewhere I don't know, they invented the Design Principle:
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.
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.
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
Ex:
- Programming to an implementation:
- Programming to an interface/superclass:
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.
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.
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()
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
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:
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!!