Last time we had a small discussion about the merits of keeping inheritance hierarchies shallow and avoiding the creation of deep, multi-level structures. This is all fine and dandy, but how do we actually achieve this end? After all, many people are keenly aware of the DRY principle whether by name or just intuition. We do not like writing the same code twice. Most programmers have a keen distaste for the infamous copy/paste operation, realizing the shortsighted-ness of its use.
Therefore inheritance to the rescue!...Right? Not necessarily. Inheritance can seem to be built for code reuse but in fact it is not. It is built for abstraction. It is built for polymorphism. It is built to allow collaborative code which operates on the interface of the base class to be able to operate on more specific implementations of the interface. It is built not to reuse the code but to allow the interface of the base class to be used by other code.
So how do we then achieve the code reuse we desire? Keep calm, the classical approach is to favor composition instead. This means that we reuse pieces of code not by inheriting it but by holding onto a would be superclass as part of a would be subclass.
Thinking about celestial bodies like the last post, one way this could be achieved is as follows:
First we include our base class:
classdef CelestialBody properties Mass end end
In order to include the rounded behavior in this class structure, lets include a RoundedBody class which contains no implementation.
classdef RoundedBody properties(Abstract) Radius end end
Since RoundedBody is purely abstract we are clearly not including it in our inheritance structure for code reuse purposes and our intent is much more clear.
Ok, now let's start creating some implementation:
classdef Sphere properties Radius end end
Here we have a Sphere class which gives us the implementation we need and do not want to reimplement. Furthermore this class may have nothing to do with celestial bodies and is more centered around geometry, primitive shapes, and so forth. This class may even live outside of our celestial bodies application and in a shapes library. This is great news since the shapes library is a core library that is specifically intended for reuse and has broad application beyond just celestial body modeling. It has codified the best known way to handle shape operations, and has been battle tested and edge case proven. This is what we should be using.
...and we use it through composition:
classdef Star < CelestialBody & RoundedBody properties(Dependent) Radius end properties(Access=private) SphereDelegate = Sphere; end methods function radius = get.Radius(star) radius = star.SphereDelegate.Radius; end function star = set.Radius(star, radius) star.SphereDelegate.Radius = radius; end function radiate(star) end end end
As you can see Star now exhibits RoundedBody behavior with a Sphere implementation. It is important to note the use of the Dependent property. For efficiency and maintainability reasons, we want there to be only one truth as to what the Radius is. Dependent properties allow this because there is no storage allocated for them, but they still behave like properties from the user's point of view. When user accesses or modifies them, MATLAB dispatches to the getter or setter, respectively. This is similar to getProperty or setProperty methods in a traditional OO language like C++ or Java® because the caller of C++/Java style get/set method wrappers doesn't know anything about the implementation. The caller does not know whether the getter is just accessing an allocated property of the instance, whether it is calculating it on the fly each time, or whether it is using a collaborator as we have done here. In this case the setter and getter delegate to the private SphereDelegate property. The user of the Star is none the wiser, and yet we have allowed code reuse of the Sphere code. Using Dependent properties for this operation in MATLAB as opposed to writing C++ or Java style set/get methods is very important because it allows powerful MATLAB indexing and vectorized operations on the property.
Here are a few more of our celestial modeling classes under the approach:
classdef Planet < CelestialBody & RoundedBody properties Moons end properties(Dependent) Radius end properties(Access=private) SphereDelegate = Sphere; end methods function radius = get.Radius(planet) radius = planet.SphereDelegate.Radius; end function planet = set.Radius(planet, radius) planet.SphereDelegate.Radius = radius; end end end classdef Moon < CelestialBody & RoundedBody properties HostPlanet end properties(Dependent) Radius end properties(Access=private) SphereDelegate = Sphere; end methods function radius = get.Radius(moon) radius = moon.SphereDelegate.Radius; end function moon = set.Radius(moon, radius) moon.SphereDelegate.Radius = radius; end end end classdef Asteroid < CelestialBody properties Shape end end
These classes together form the following structure:
As you can see this approach does lead to a bit more boiler plate code. Performing this delegation from a code writing perspective is not entirely free, although the extra code you do have to write is simple and not likely to contain bugs.
However, a key benefit we get here is the ability to separate the code we wish to share from the interface we wish to declare for ourselves. In our example, even though these classes have precisely the same functionality as Sphere, they are not substitutable for Sphere! Code which operates on these celestial bodies cannot, by design, just operate on any Sphere in the world, including Basketballs and Tomatoes. Similarly, these objects cannot be used in other environments that operate on Spheres and instead are restricted to the domain in which they are designed. Indeed it is even clear from the class diagram that the Sphere interface is not connected to the CelestialBody hierarchy. Composition is a much looser form of coupling than inheritance. While we still are able to reuse the code desired, we don't let these rounded celestial bodies interact with other software modules in ways that are neither designed nor understood.
What are your thoughts on this approach? Do you mind the boiler plate in exchange for the freedom and flexibility that composition provides?
To leave a comment, please click here to sign in to your MathWorks Account or create a new one.