My real scenario is a bit hard to explain so I'll map it to a more recognizable domain, say home entertainment equipment:
A particular piece of equipment can offer different services:
A Panasonic XYZ can play DVDs and CDs.
A Sony ABC can only play CDs.
A Hitachi PQR can play DVDs and receive TV.
...
...
Each service (DVD, CD, TV,...) has a default implementation which most models use, but some models have customized versions of particular services.
Interfaces
Models choosing to implement DVD'izable, CD'izable, TV'izable,... contracts would
result in a lot of code duplication between models.
Single Inheritance
A single superclass implementing the default services would allow me to have a single subclass for each model containing all of its custom behaviour. My superclass would however be quite unwieldy and heavier than it need be for models that don't offer all types of services.
Multiple Inheritance
Multiple inheritance with its ability to selectively incorporate the services required and provide default implementations, on the surface seems ideal. I value the cohesiveness of having all PanasonicXYZ's custom functionality in a single class more than the coupling introduced by the inheritance.
But I'm not using C++ (rather PHP) and I sort of feel there's a better way anyway. Nor do I want to use proprietary extensions like mixins or 5.4's traits.
Composition
I see a class explosion with my custom functionality for a particular model scattered over multiple classes--I'd need a PanasonicXYZ_CD class and PanasonicXYZ_DVD class for example, and they'd only ever be used by the PanasonicXYZ object.
Is there a preferable structure?
Edit: I'll have a good think about some of the comments and answers made instead of prematurely commenting.
You can prevent the explosion of a class hierarchy by transforming it into several related hierarchies. Following this approach, we can extract the color-related code into its own class with two subclasses: Red and Blue . The Shape class then gets a reference field pointing to one of the color objects.
To get the higher design flexibility, the design principle says that composition should be favored over inheritance. Inheritance should only be used when subclass 'is a' superclass. Don't use inheritance to get code reuse. If there is no 'is a' relationship, then use composition for code reuse.
One more benefit of composition over inheritance is testing scope. Unit testing is easy in composition because we know what all methods we are using from another class. We can mock it up for testing whereas in inheritance we depend heavily on superclass and don't know what all methods of superclass will be used.
Prefer composition over inheritance as it is more malleable / easy to modify later, but do not use a compose-always approach. With composition, it's easy to change behavior on the fly with Dependency Injection / Setters. Inheritance is more rigid as most languages do not allow you to derive from more than one type.
Use composition Luke:
Object classes represents a role in your system. Even when is natural to think about a "Device", for OOP purposes is better to think about what are the roles of your device: DVD Player, TV Receiver, CD Player, etc.
It doesn't matter that a single device does all of them, thinking in the roles that the object is going to have will help you to end with single responsibility objects:
class SonyTvWithDVDPlayer {
DVDPlayer asDVDPlayer();
Tv asTv();
}
In that way is easy to refactor common functionality, you can have a GenericDVDPlayer
and return that in the asDVDPlayer
method.
If you want to allow a more dynamic usage, like asking to a Device
which functionality it supports, you can use a kind of Product Trader, for example:
interface MultifunctionDevice {
<T> T as(Class<T> functionalitySpec) throws UnsupportedFunctionalityException
}
and them in the code you could do something like this:
device.as(DVDPlayer.class).play(dvd);
See that in this case the "multifunction device" acts like a Product Trader and the DVDPlayer.class
is the spec of the product.
There are a lot of different ways to implement the trader and the spec, one is to use the Visitor pattern. But I found that in a lot of cases (when you want to be able to configure your "multifunction devices" dynamically) you can do something like this:
class MultifunctionDevice {
Iterable<Device> devices;
<T extends Device> T as(Class<T> spec) {
for (Device dev : devices) {
if (dev.provides(spec)) return dev;
}
throw new UnsupportedFunctionalityException(spec);
}
}
This combined with a Builder and a fluent API makes easy to define different devices without class explosion:
dvdAndTv = new DeviceBuilder("Sony All in one TV and DVD")
.addPart(new SonyDvdPlayer())
.addPart(new SonyTv())
.build();
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With