Isolating Polymorphism «
»


Code: , ,
No comments

I’m reading Code Complete, 2nd ed 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 Employees 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.

Leave a Reply

Your email address will not be published.