Isolating Polymorphism
I’m reading [Code Complete, 2nd ed]{href”http:=”” www.cc2e.com=”” “=””} and it’s been like catching up with an old friend. I remember reading the first edition at my first tech job fifteen years ago. At the time a lot of it went by me, but rereading I can see a lot of things I had to learn on my own.
Or that I now have subtle opinions on. For example:
Inherit — When Inheritance Simplifies the Design
In designing a software system, you’ll often find objects that are much like other objects, except for a few differences. In an accounting system, for instance, you might have both full-time and part-time employees. Most of the data associated with both kinds of employees is the same, but some is different. In object-oriented programming, you can define a general type of employee and then define full-time employees as general employees, except for a few differences, and part-time employees also as general employees, except for a few differences. When an operation on an employee doesn’t depend on the type of employee, the operation is handled as if the employee were just a general employee. When the operation depends on whether the employee is full-time or part-time, the operation is handled differently.
Defining similarities and differences among such objects is called “inheritance” because the specific part-time and full-time employees inherit characteristics from the general-employee type.
Straightforward, right? Done it a thousand times:
class Employee attr_reader :name, :hours_this_week
def initialize name, hours_this_week @name, @hours_this_week = name, hours_this_week end end
class PartTimeEmployee < Employee def max_hours_per_week 30 # thanks, obama end end
class FullTimeEmployee < Employee def max_hours_per_week 60 end end
This code conforms to the Liskov Substitution Principle:
What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
Programming has shifted from sounding mathy to sounding businessy, so nowadays this is generally written more like, “When you have multiple subclasses, the calling code shouldn’t care which subclass it’s using.”
So here’s an example:
class Shift attr_reader :hours_long end
class Employee # this conforms trivially, because it doesn’t use anything that differs # between the subclasses def initials name.split(“ “).map {|s| s[0] }.join(‘’) end
# this conforms because it works with both PartTime and FullTime without modification def can_take? shift shift.hours_long + hours_this_week <= max_hours_per_week end end
This is fine (for example code), but I wouldn’t write this code anymore. I’ve been leaning so hard toward composition instead of inheritance that I’d write:
class Employee attr_reader :name, :hours_this_week, :schedule_type
def initialize name, hours_this_week, schedule_type @name, @hours_this_week, @schedule_type = name, hours_this_wek, schedule_type end
def initials name.split(“ “).map {|s| s[0] }.join(‘’) end
def can_take? shift shift.hours_long + hours_this_week <= schedule_type.max_hours_per_week end end
class PartTimeSchedule def max_hours_per_week 30 end end
class FullTimeSchedule def max_hours_per_week 60 end end
It’s a small difference, but I’ve isolated the polymorphism. The big improvements are that the concept now has a name and can be tested in isolation.
I don’t want to think about different types of Employee
s and whether the hours they work might affect how their initials are extracted. Conversely, I want it to be clear when I’m digging into polymorphic code because I have to read every implementation to understand the calling code.
Inheritance makes a distinction central to an object’s definition, regardless of importance. It might barely be appropriate if this code is in an app all about scheduling employee shifts at a busy construction site. It would be distracting and needlessly complex if this was an app for managing delivery logistics and storage that happened to have a little code to schedule employees to match arriving cargo.
Ultimately, inheritance should be used for code reuse, never for domain concepts. When you are providing functionality for very similar classes (like a set of Repository
classes, one per domain model), inheritance saves a lot of boilerplate and indirection. When you use inheritance for domain concepts it conflates your concepts (like a person and their schedule) and adds needless complexity.