Do you have an object trying to be something it is not? We often see this when implementing parts of an interface and then throwing an UnsupportedOperationException for methods that cannot be implemented. One example of this in the Java Collections Framework is the Map interface. Interface methods put() and remove() (among others) are only supported by some implementations of Map. While I cannot speculate why it was implemented this way, let’s look at a basic example and different approaches to solving this problem.
Consider this scenario:
You have a Bird class which has methods like eat(Food food), speak(), flapWings(). Your program needs to have multiple types of Birds, with different implementations of eat(Food food), speak(), flapWings(), so these methods are pushed into a Bird interface and specific implementations are created for Robin, Loon, and Penguin. All is well. Until you get a feature request to make some of the birds fly. Robins and Loons both fly, but Penguin cannot.
Approach 1: Add fly() method to Bird interface
Adding the fly() method to the Bird interface appears to be the simplest approach. We could do this, but what is the Penguin implementation of fly()?
public class Penguin implements Bird {
public void fly(Direction direction) {
flapWings();
}
...
}
That bird is going nowhere fast! The client code is expecting a Bird to behave like a Bird, and this implementation implies that all Birds fly.
public class Flock {
public void migrate(){
for (Bird bird: birds){
bird.fly(Direction.SOUTH);
}
}
}
What does the client code do when it misbehaves? Suppose we throw an UnsupportedOperationException when a Penguin tries to fly. Should the client code catch and swallow this exception? This is more work for the client code:
public class Flock {
public void migrate(){
for (Bird bird: birds){
try {
bird.fly(Direction.SOUTH);
} catch (UnsupportedOperationException exception) {
// apparently this bird cannot fly; do nothing
}
}
}
}
Another implementation for this approach would be to have a no op and do nothing when Penguin#fly() is called. This prevents us from forcing the client code to handle non-flyable birds, since the intent of the Bird interface is to be able to treat all Birds the same.
The underlying design smell here is that this design implies that all Birds fly, and we know that not all Birds fly. This approach could be used when the operation will be supported in a future iteration. But let’s be honest. Why stub in functionality now when we know that the only constant in software is change? Requirements change, priorities change, and it is likely that a half started implementation won’t ever be completed. And in this case, I’m pretty sure Penguins won’t be flying anytime soon!
Approach 2: Think about birds in real life
In this approach, let’s think about our objects and their behavior. Not all birds fly, so why make all Bird implementations support it? Knowing this, what behavior about these implementations is similar and what behavior varies? Think about Birds in real life. We know that some birds fly (this is the behavior that varies). All birds eat (this is the similar behavior). In our scenario, we can introduce a second interface, Flyable.
public interface Flyable {
public void fly(Direction direction);
}
The Flyable interface is for behavior that is specific to objects that can fly. In this approach, we leave the Bird interface as is since not all birds fly. The Robin and Loon classes now both implement the Flyable interface and it’s one method.
public class Loon implements Bird, Flyable {
public void fly(Direction direction) {
flapWings();
}
...
}
This interface could even be used for air plane implementations.
public class Jet implements Flyable {
public void fly(Direction direction) {
startThrusters();
}
}
By taking a closer look at our objects and their behavior, we can design well-encapsulated objects from the get go, rather than allowing our objects to impersonate other objects.