|
Section 25:
|
[25.5] Can you provide an example that demonstrates the above guidelines?
Suppose you have land vehicles, water vehicles, air vehicles, and space
vehicles. (Forget the whole concept of amphibious vehicles for this example;
pretend they don't exist for this illustration.) Suppose we also have
different power sources: gas powered, wind powered, nuclear powered, pedal
powered, etc. We could use multiple inheritance to tie everything
together, but before we do, we should ask a few tough questions:
- Will the users of LandVehicle need to have a
Vehicle& that refers to a LandVehicle object? In particular,
will the users call methods on a Vehicle-reference and expect
the actual implementation of those methods to be specific to
LandVehicles?
- Ditto for GasPoweredVehicles: will the users want a
Vehicle reference that refers to a GasPoweredVehicle object,
and in particular will they want to call methods on that Vehicle
reference and expect the implementations to get overridden by
GasPoweredVehicle?
If both answers are "yes," multiple inheritance is probably the best
way to go. But before you close the door on the alternatives, here are a few
more "decision criteria." Suppose there are N geographies (land, water, air,
space, etc.) and M power sources (gas, nuclear, wind, pedal, etc.). There are
at least three choices for the overall design: the bridge pattern, nested
generalization, and multiple inheritance. Each has its pros/cons:
- With the bridge pattern, you create two distinct hierarchies: ABC
Vehicle has derived classes LandVehicle, WaterVehicle,
etc., and ABC Engine has derived classes GasPowered,
NuclearPowered, etc. Then the Vehicle has an Engine*
(that is, an Engine-pointer), and users mix and match vehicles and
engines at run-time. This has the advantage that you only have to write N+M
derived classes, which means things grow very gracefully: when you add a new
geography (incrementing N) or engine type (incrementing M), you need add only
one new derived class. However you have several disadvantages as well: you
only have N+M derived classes which means you only have at most N+M overrides
and therefore N+M concrete algorithms / data structures. If you ultimately
want different algorithms and/or data structures in the N*M combinations,
you'll have to work hard to make that happen, and you're probably better off
with something other than a pure bridge pattern. The other thing the bridge
doesn't solve for you is eliminating the nonsensical choices, such as pedal
powered space vehicles. You can solve that by adding extra checks when the
users combine vehicles and engines at run-time, but it requires a bit of
skullduggery, something the bridge pattern doesn't provide for free. The
bridge also restricts users since, although there is a common base class above
all geographies (meaning a user can pass any kind of vehicle as a
Vehicle&), there is not a common base class above, for example, all
gas powered vehicles, and therefore users cannot pass any gas powered vehicle
as a GasPoweredVehicle&. Finally, the bridge has the advantage that
it shares code between the group of, for example, water vehicles as well as
the group of, for example, gas powered vehicles. In other words, the various
gas powered vehicles share the code in derived class
GasPoweredEngine.
- With nested generalization, you pick one of the hierarchies as
primary and the other as secondary, and you have a nested hierarchy. For
example, if you choose geography as primary, Vehicle would have
derived classes LandVehicle, WaterVehicle, etc., and those
would each have further derived classes, one per power source type. E.g.,
LandVehicle would have derived classes GasPoweredLandVehicle,
PedalPoweredLandVehicle, NuclearPoweredLandVehicle, etc.;
WaterVehicle would have a similar set of derived classes, etc. This
requires you to write roughly N*M different derived classes, which means
things don't grow gracefully when you increment N or M, but it gives you the
advantage over the bridge that you can have N*M different algorithms and data
structures. It also gives you fine granular control, since the user cannot
select nonsensical combinations, such as pedal powered space vehicles, since
the user can select only those combinations that a programmer has decided are
reasonable. Unfortunately nested generalization doesn't improve the problem
with passing any gas powered vehicle as a common base class, since there is no
common base class above the secondary hierarchy, e.g., there is no
GasPoweredVehicle base class. And finally, it's not obvious how to
share code between all vehicles that use the same power source, e.g., between
all gas powered vehicles.
- With multiple inheritance, you have two distinct hierarchies, just
like the bridge, but you remove the Engine* from the bridge and
instead create roughly N*M derived classes below both the hierarchy of
geographies and the hierarchy of power sources. It's not as simple as this,
since you'll need to change the concept of the Engine classes. In
particular, you'll want to rename the classes in that hierarchy from, for
example, GasPoweredEngine to GasPoweredVehicle; plus you'll
need to make corresponding changes to the methods in the hierarchy. In any
case, class GasPoweredLandVehicle will multiply inherit from
GasPoweredVehicle and LandVehicle, and similarly with
GasPoweredWaterVehicle, NuclearPoweredWaterVehicle, etc. Like
nested generalization, you have to write roughly N*M classes, which doesn't
grow gracefully, but it does give you fine granular control over both which
algorithm and data structures to use in the various derived classes as well as
which combinations are deemed "reasonable," meaning you simply don't create
nonsensical choices like PedalPoweredSpaceVehicle. It solves a
problem shared by both bridge and nested generalization, namely it allows a
user to pass any gas powered vehicle using a common base class. Finally it
provides a solution to the code-sharing problem, a solution that is at least
as good as that of the bridge solution: it lets all gas powered vehicles share
common code when that is desired. We say this is "at least as good as the
solution from the bridge" since, unlike the bridge, the derived classes can
share common code within gas powered vehicles, but can also, unlike with the
bridge, override and replace that code in cases where the shared code is not
ideal.
The most important point: there is no universally "best" answer.
Perhaps you were hoping I would tell you to always use one or the other of the
above choices. I'd be happy to do that except for one minor detail: it'd be a
lie. If exactly one of the above was always best, then one size would
fit all, and we know it does not.
So here's what you have to do: T H I N K. You'll have to make a
decision. I'll give you some guidelines, but ultimately you will have to
decide what is best (or perhaps "least
bad") for your situation.
|