Don’t Get In Too Deep
The conventional wisdom these days seems to be that it is better to go deep, not wide. Whether this is in education, marketing, or general approaches to focused living, there seems to be an theme these days that if you favor breadth over depth the result will be shallow, without substance, and spread thin. The generalist it seems has seen better days in the court of public opinion. Is that true in life? I dunno, perhaps.
Well I'm here to denounce this approach when it comes to class design! Unfortunately, class hierarchies seem to find a way over time to get deeper and deeper. As these hierarchies grow deeper the coupling increases, and inheritance is a very strong form of coupling. The interface grows with each extension point, meaning that a class with 18 levels of superclasses likely has at least 18 methods which are called and used by even more clients. Often these clients only operate on a small part of the interface and yet an accidental coupling inevitably creeps in because it can. All clients have access to the entire interface rather than just the one thing they asked for.
Deep hierarchies also rely heavily on the categorization occurring correctly down through the subclasses and the generalization up through the superclasses. In theory this works great, because, well, we humans are naturally adept at categorizing things right? (*cough cough* platypus *cough cough*)
OK, so a platypus is a classic poster child for categorization gone wrong. Mammals are supposed to give live birth, right? Well, no of course, but this may be an easy mistake to make. You might say that a taxonomy which states that all mammals give live birth is an incorrect taxonomy, not a fundamental problem of taxonomies in general, and you'd be right. However, once a piece of software starts connecting and integrating with the rest of the software system changing your classification becomes difficult because you have clients that start to depend on this classification. Said another way, the 19 century taxonomy that did not correctly account for the platypus could be adjusted and corrected with the new information presented to it. However, it is not so easy to change software systems that are depended on by many downstream clients. Often times in software poor categorization decisions need to be lived with and worked around for a long time.
This happens All.The.Time.
For example, take a look at a simple model of some celestial bodies. First let's start with the following classes which seem like a reasonable categorization:
classdef CelestialBody properties Mass end end classdef GravitationallyRoundedBody < CelestialBody properties Radius end end classdef Asteroid < CelestialBody properties Shape end end classdef Moon < GravitationallyRoundedBody properties HostPlanet end end classdef Star < GravitationallyRoundedBody methods function radiate(star) end end end classdef Planet < GravitationallyRoundedBody properties Moons end end classdef TerrestrialPlanet < Planet properties Crust end end classdef RingedPlanet < Planet properties Rings end end classdef GasGiant < RingedPlanet end classdef IceGiant < RingedPlanet end
I went over to the file exchange and downloaded the UMLgui submission so that we could visualize these classes a bit more easily (Hint you should too!). With this UI we can generate the following class diagram from this simple code:
Everything here at first glance seems entirely rational. Go ahead and pick any spot in that hierarchy and ask yourself whether each class "isa" more specialized version of its superclass. Similarly, if you are a fan of Liskov there doesn't seem to be any obvious substitutability violations.
With This Ring...
However, we can easily poke some holes into this simple structure. For example, what about the rings? Well, this model worked quite well when you think about the planets in our own solar system, it is indeed the gas/ice giants that have rings so it seems to work well...that is until NASA discovers that Rhea may have some rings!
Now we are placed in an unfortunate situation that is common with deep inheritance hierarchies. The problem here is that Rhea is a moon! We may have some code that operates on celestial bodies with rings and we'd like to support Rhea in such code. However, the class which defines what it means to have rings is actually a planet. Rhea can't (shouldn't!) derive from RingedPlanet because that would give Rhea moons! Furthermore, we can't move RingedPlanet higher in the hierarchy to allow Rhea to access this feature because then TerrestrialPlanet and Star would have rings, not to mention the fact that this would apply to all moons, not just rare moons like Rhea which (might?) contain them.
We can play this game with other features of the interface as well. What about crust? Most moons have a crust like the terrestrial planets, but how do we include the crust both above the Planet interface so that moons have access to this property as well as below the Planet interface so that the gas/ice giants don't inherit it. How might we add an atmosphere to these models? The planets have one (oops! not Mercury) and the moons don't (wait, what about Titan?). Don't even think about looking outside of our own limited solar system, this structure will fall apart quickly. When we try to structure our classes in a pure hierarchy we inevitably fail as we see new requirements and try to apply our software structure to new features and ideas.
This example demonstrates the principle of favoring class hierarchies that are wide rather than deep. Shallow and wide hierarchies contain concrete classes that are coupled to a low number of superclasses. They can opt into precisely the interfaces they need without adding unnecessary baggage. These interfaces themselves can be small, scoped, and modular such that subclasses can pick and choose just the pieces of the interface that apply directly to them.
We can make this class structure much more flexible by flattening the hierarchy and bringing some of these key abilities into their own independent base classes. For example we can remove two levels of hierarchy and solve the problems discussed by simply pulling the gravitationally rounded properties and whether the body has rings their own independent interfaces. This looks like this:
classdef CelestialBody properties Mass end end classdef Rounded properties Radius end end classdef Asteroid < CelestialBody properties Shape end end classdef Moon < CelestialBody & Rounded properties HostPlanet end end classdef Star < CelestialBody & Rounded methods function radiate(star) end end end classdef Planet < CelestialBody & Rounded properties Moons end end classdef TerrestrialPlanet < Planet properties Crust end end classdef HasRings properties Rings end end classdef GasGiant < Planet & HasRings end classdef IceGiant < Planet & HasRings end
This code produces the following hierarchy:
With this structure, it becomes trivial to give a moon some rings!
Now there are some different strategies for flattening these hierarchies, but hopefully I've motivated the desire to keep them flat in the first place. We can work through some strategies in another post.
Do you have any deep hierarchy horror stories? How about feel good romances with nice shallow designs? Love to hear about em...
To leave a comment, please click here to sign in to your MathWorks Account or create a new one.