Multiple Inheritance Functionality via Strategy Pattern in Java
In object-oriented programming, multiple inheritance, in which a class can inherit from more than one parent, is a powerful but also controversial feature.
Languages like C++ and Python support it, while Java deliberately avoids it due to the incurred additional complexity, ambiguity, and rigidity of the architecture.
Problems such as the “diamond problem” (the ambiguity introduced when a class inherits from two classes that have a common ancestor) often forecast deeply rooted flaws in the architecture.
Hence, in this post, we explore some possible flaws multiple inheritance can introduce using the example of Shape + Color class requirements and examine how the Strategy Pattern can solve these issues through composition.
Multiple Inheritance on the Shape + Color Problem
Imagine we're building a graphics application which needs to draw various coloured shapes. We might start with simple classes like Circle and Square for shapes, and Red and Blue for colours.
If our language supported multiple inheritance, we might try to create a RedCircle by inheriting from both Red and Circle and BlueSquare by inheriting from Square and Blue.
RedCircle vs CircleRed . class Circle {
void draw() {
System.out.println("Drawing a circle");
}
}
class Square {
void draw() {
System.out.println("Drawing a square");
}
}
class Red {
void applyColor() {
System.out.println("Applying red color");
}
}
class Blue {
void applyColor() {
System.out.println("Applying blue color");
}
}
// This won't compile in Java, but let's imagine it could
class RedCircle extends Circle, Red {
@Override
void draw() {
super.draw();
super.applyColor();
}
}
class BlueSquare extends Square, Blue {
@Override
void draw() {
super.draw();
super.applyColor();
}
}This seems straightforward (if it could compile in Java), but it hides several serious issues.
The first is the diamond problem. If both Circle and Red inherit from a common Shape class, and both override a method like draw(), which version should RedCircle use? The compiler can't determine this automatically, leading to potential runtime errors or the need to manually resolve the ambiguity for the compiler by the developers.
Even without that, we face behavioural conflicts. What if Circle and Red both define a getDescription() method? Which one should RedCircle inherit? The developer would need to manually resolve these conflicts, creating fragile code that's difficult to maintain.
The Strategy Pattern
The Strategy Pattern offers a cleaner approach by using composition instead of inheritance. Instead of inheriting behaviours, we compose them (using interfaces).
First, we define interfaces for our behaviours.
interface Shape {
void draw();
}
interface Color {
void applyColor();
}Then we create concrete implementations.
class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a circle");
}
}
class Square implements Shape {
@Override
public void draw() {
System.out.println("Drawing a square");
}
}
class Red implements Color {
@Override
public void applyColor() {
System.out.println("Applying red color");
}
}
class Blue implements Color {
@Override
public void applyColor() {
System.out.println("Applying blue color");
}
}Having these, we can move on into two possible directions.
- We can create a class
ColoredShapewhich allows us to take in the behaviour ofShapeandColoras constructor arguments, and execute upon them as we need. This enables us to dynamically combine behaviours to create new objects with different colours and shapes as we see fit at runtime. - For some applications, the direct declaration of the Shape + Color classes (as we did for the multiple inheritance example) might however actually be useful. Think of beans in a dependency injection framework, which should be defined via the strategy pattern, but need to be accessible directly via dependency injection.
Let's start with the first one, and examine the latter one afterwards.
class ColoredShape {
private final Shape shape;
private final Color color;
public ColoredShape(Shape shape, Color color) {
this.shape = shape;
this.color = color;
}
public void draw() {
shape.draw();
color.applyColor();
}
}Our ColoredShape class serves as a composition container that combines shape and colour behaviours. This approach immediately solves all the problems we encountered with multiple inheritance.
By accepting Shape and Color implementations as constructor arguments, it creates a flexible system that avoids the pitfalls of multiple inheritance while maintaining clean separation of concerns.
ColoredShape shape = new ColoredShape(new Circle(), new Red());
shape.draw(); // Draws a red circle
shape = new ColoredShape(new Circle(), new Blue());
shape.draw(); // Draws a blue circleAs discussed above, while the flexible ColoredShape approach is powerful, there are scenarios where we might want to create concrete implementations of specific shape-colour combinations.
This is particularly useful when:
- Specific combinations are used frequently in your application
- You need to inject these as beans in a dependency injection context
- The combinations have additional behaviour beyond simple composition
- You want to enforce certain combinations at compile time
If something along those lines is applicable to our case, we might want to consider defining concrete implementations like RedCircle or BlueSquare classes, but still make use of composition over inheritance via the strategy pattern.
public class RedCircle extends ColoredShape {
public RedCircle() {
super(new Circle(), new Red()); // Define the behaviour of the concrete implementation
}
}
public class BlueSquare extends ColoredShape {
public BlueSquare() {
super(new Square(), new Blue()); // Define the behaviour of the concrete implementation
}
}This approach allows for simplified usage of our system, as clients don't necessarily need to know about the composition details. Additionally, it's dependency injection friendly, as it can be easily defined as beans in Spring or other DI frameworks, as done below.
@Configuration
public class DrawingConfig {
@Bean
public RedCircle redCircle() {
return new RedCircle();
}
@Bean
public BlueSquare blueSquare() {
return new BlueSquare();
}
@Bean
public ColoredShape greenTriangle() {
return new ColoredShape(new Triangle(), new Green());
}
}
@Service
public class DrawingService {
private final RedCircle redCircle;
private final BlueSquare blueSquare;
private final ColoredShape greenTriangle;
@Autowired
public DrawingService(RedCircle redCircle, BlueSquare blueSquare, ColoredShape greenTriangle) {
this.redCircle = redCircle;
this.blueSquare = blueSquare;
this.greenTriangle = greenTriangle;
}
public void drawAll() {
redCircle.draw();
blueSquare.draw();
greenTriangle.draw();
}
}Composition vs Inheritance
While composition offers many advantages, inheritance still has its place. Inheritance works best when there's a clear “is-a” relationship between classes. If we can say that a Dog “is an” Animal, and this relationship is fundamental to our domain model, then inheritance might be appropriate.
However, even in cases where inheritance seems natural, we should be cautious about creating deep hierarchies. Many problems that initially seem to require inheritance can be better solved through composition. The “is-a” relationship is often better expressed as a “has-a” relationship when we look more closely.
Composition shines when we need flexibility and adaptability. If behaviours might change at runtime, or if we need to combine behaviours in various ways, composition is almost always the better choice. The Strategy Pattern is particularly useful when we have algorithms or behaviours that might vary independently of the objects that use them.
Conclusion
The choice between inheritance and composition has profound implications for how easily our code can adapt to changing requirements. While multiple inheritance offers the allure of code reuse through a simple mechanism, the issues it introduces make it a poor choice for most real-world applications.
The Strategy Pattern, through the principle of composition over inheritance, provides a more robust and flexible alternative. It allows us to build systems that are easier to test, extend, and maintain.
By favouring composition, we create software that's not just functional today, but can evolve gracefully as needs change tomorrow.