Our action starts when we already have a hierarchy of classes representing different kind of ducks. On top we have Duck, a class implementing the plain vanilla duck, that is extended to change some specific aspects of more detailed ducks. We could have, for instance, MallardDuck and RedheadDuck, overriding the required methods to get some different details.
We face a change, we want add a new functionality to the ducks, to let them fly. A natural solution would be change the base class of the duck hierarchy, adding a fly() method to Duck. But not always this is a good idea. In our case, our duck hierarchy includes also peculiar ducks, as RubberDuck, that is not a duck in the living bird sense of the term. For what we care here, this particular duck has no use of fly(). But adding a fly() method to Duck implies that now even our rubber ducks could fly.
A quick solution to this issue would be overriding the fly() method in each class derived from Duck that does have a different behavior. This is OK for a small hierarchy, but in our case we have a huge number of different duck classes, and we are expecting a lot of changes in the classes structure in the future. In such a case, this approach could lead to a maintenance nightmare.
An alternative solution would be to take out from the Duck class definition all the methods that could be present or not in the derived ducks and make them available through interfaces. So, MallardDuck would extends Duck and implement the interfaces Flyable and Quackable (for the methods fly() and quack()), RubberDuck would implement just Quackable, and so on. But this solution would be even more error prone and hard to mantain that the original one.
There is no Multiple Inheritance (MI) in Java, so even if we wanted we can't use it here. Using MI we could have designed the ducks in a way that they compose in their behavior a part that is common to all the ducks, and we expect not to vary (at least not frequently) in the course of the development, and other elements that we think are more subject to change.
But here it would be more useful to put in the superclass Duck the information relative to the fact that we have a reference to external classes that provide us part of the functionality for the actual duck. The idea is to have a Duck that has as protected members a number of interfaces, each of them for a specific behavior that we want to manage in our actual duck.
Any of this interface would be the root of a hierarchy of classes that would provide the actual functionality for ducks. So, for instance, the interface FlyBehaviour will provide a method fly() that will be implemented in the classes FlyWithWings and FlyNoWay. Any real class derived from Duck should take care of setting the interfaces defined in the Duck superclass using one of this classes.
We can see it in this way: the Duck class delegates a few of its behaviors (the ones we want to keep more easy to change) to external classes. It is a task of the classes derived from Duck to specify in their constructor what the real behavior should be.
Let's sketch how this should work. About flying, the class Duck should do something like that:
public abstract class Duck { // 1 protected FlyBehaviour fly; // ... public void doFly() { fly.doIt(); // 2 } // ... }1. it doesn't make any sense to instantiate a Duck, so we explicitly mark the class as abstract
2. the implementation of the flying is simply delegated to the FlyBehaviour hierarchy
The FlyBehaviour hierarchy should be like this:
public interface FlyBehaviour { void doIt(); } public class FlyWithWings implements FlyBehaviour { public void doIt() { System.out.println("flying with wings"); } } public class FlyNoWay implements FlyBehaviour { public void doIt() { System.out.println("not flying"); } }Given that, we should define a real duck in this way:
public class DuckMallard extends Duck { public DuckMallard() { fly = new FlyWithWings(); } }And we would use it like that:
Duck d = new DuckMallard(); d.doFly();But there is more. Having implemented our Duck hierarchy in this way it is easy to change the behavior of a duck at execution time. It is enough to provide a setFlyingBehaviour() method that would set the FlyBehaviour member accordingly with our current requirement.
So, extracting the changing behaviors from the Duck hierarchy and putting them in their specific hierarchies, and shifting that relation in the field of composition has the result that we get a lot of more flexibility.
No comments:
Post a Comment