In Progress
Unit 1, Lesson 21
In Progress

Whole Value

Video transcript & code

In the last episode, we were left with a cliffhangar. We had objects representing educational courses, some courses longer, or shorter, than others. And yet somehow, this seemingly simple exercise in domain modeling left us with a number of difficult questions and unsatisfying solutions.

I'm not going to rehash everything we saw in that episode. So if you haven't seen it yet, or you don't remember it, you might want to pause this and go review episode #400 first.

I submit to you that every single non-optimal solution we've come up with can be traced back to a single problem. We're modeling domain concepts as objects, as we should. And yet, we've allowed one core, essential concept in our domain escape us without ever properly modeling it.

What concept is that? It's the concept of duration.

Our application is about managing courses. Courses have durations. Therefore, the idea of a temporal duration should be treated as just as much of a first-class domain element as any other idea we model.

But we haven't done that. Instead, we've used basic integers—effectively primitives, from the point of view of application domain modeling—to stand in for a real duration concept. By inadvertently permitting the code smell of "primitive obsession" into one of our domain models, we exposed ourselves to a whole host of design difficulties.

Let's introduce a proper object to represent durations. Better yet, let's introduce a little family of objects.

We'll start with a base class.

We have an initializer that accepts a magnitude, and then freezes itself to become a immutable value object.

We also provide a reader method for the magnitude.

We have some customized inspect and to_s methods. We'll see the output of those methods in a moment.

We also alias #to_i to return the internal magnitude attribute.

And we provide a special shorthand for instantiating durations, using the square bracket operator.

With the base class in place, we derive unique subclasses for days, weeks, and months.

require "delegate"

class Duration
  def self.[](count)
    self.new(count)
  end

  attr_reader :magnitude

  def initialize(magnitude)
    @magnitude = magnitude
    freeze
  end

  def inspect
    "#{self.class}[#{magnitude}]"
  end

  def to_s
    "#{magnitude} #{self.class.name.downcase}"
  end

  alias_method :to_i, :magnitude
end

class Days < Duration; end
class Weeks < Duration; end
class Months < Duration; end

After switching our courses over to using Duration objects, there is no longer any need for separate getter methods returning the duration in day, week, or month units.

When we ask one course its duration, we see 3 months.

When we ask another, we see 3 weeks.

And when we ask still another, we see 3 days.

We haven't looked at the setup code, but in each course has been assigned a duration in the terms that make the most sense for that course, whether that's days, weeks, or months.

require "./courses5"

COURSES[:potions].duration
# => Months[3]
COURSES[:scrying].duration
# => Weeks[3]
COURSES[:hexes].duration
# => Days[3]

There is no longer any ambiguity about what the setter method might be for making modifications to the duration. Since we use only the #duration getter to see the duration, that means we must set the #duration setter the same way.

require "./courses5"

COURSES[:potions].duration = Months[2]
# => Months[2]

Using duration objects makes a big difference in how we write our display helpers.

There's no longer any need to embed any assumptions about the units returned. We can defer to the duration objects to convert themselves sensibly to strings.

Let's render a couple of courses here. Thanks to the customized #to_s method we defined, one course is displayed in terms of days, and another in terms of months.

require "./courses5"

def render_course_info(course)
  "#{course.title} (#{course.duration})"
end

render_course_info(COURSES[:hexes])
# => "Hexes (3 days)"
render_course_info(COURSES[:potions])
# => "Potions 101 (3 months)"

But sometimes, an object's basic built-in stringification abilities aren't sufficient to render it. Maybe we want to use some kind of special UI controls to render different values. Or maybe we want an internationalized version.

In this case as well, having differentiated duration types helps us out. We can create a generic render_value method, which switches on the type of the value to display different types differently.

Then we can reference this generic helper from our render_course helper.

Now, I'm not going to suggest this is great code. It's switching on object type, and that's a code smell.

But unlike the last time we wrote a helper which switched on its inputs, this one isn't specific to rendering course durations. If we need to, we can use this same helper all over our application, anywhere we need to render a domain value. Because we've embedded semantic meaning into our values, we can be a lot smarter about rendering them wherever they turn up.

require "./courses5"

def render_value(value)
  case value
  when Months
    "#{value.to_i} grueling months"
  when Weeks
    "#{value.to_i} delightful weeks"
  when Days
    "a paltry #{value.to_i} days"
  end
end

def render_course_info(course)
  "#{course.title} (#{render_value(course.duration)})"
end

render_course_info(COURSES[:hexes])
# => "Hexes (a paltry 3 days)"
render_course_info(COURSES[:scrying])
# => "Scrying 101 (3 delightful weeks)"
render_course_info(COURSES[:potions])
# => "Potions 101 (3 grueling months)"

This idea, of transforming a primitive attribute into a domain-specific value object, is known as the "Whole Object Pattern". It's a pattern that was documented by Ward Cunningham. As Ward puts it:

When parameterizing or otherwise quantifying a business… model there remains an overwhelming desire to express these parameters in the most fundamental units of computation. Not only is this no longer necessary…, it actually interferes with smooth and proper communication between the parts of your program and with its users. Because bits, strings and numbers can be used to represent almost anything, any one in isolation means almost nothing. Therefore:

[…]

Construct specialized values to quantify your domain model and use these values as the arguments of their messages and as the units of input and output. Make sure these objects capture the whole quantity with all its implications beyond merely magnitude.

Now, one objection we might have is that our duration objects no longer behave as integers. We can't do math on them, for instance.

But before we go looking for a way to fix this, we should think very carefully about whether it is actually needed. Does our application need to do math on durations? Or are they simply a course attribute that is set, updated, and displayed?

If the latter, we may well be able to get by without giving them any integer-like capabilities at all!

This is one of the greatest insights we can gain when we go from using "primitive" objects to represent domain concepts, to using Whole Values. When we use an integer to model a duration, we start to think that a duration "is" an integer. But is it, really? No! A course duration is a course duration. It is a concept specific to our application domain, and while it may have a magnitude which can be expressed as an integer, that doesn't necessarily mean that a duration itself needs to behave in any way like an integer number. It really depends on our application semantics and requirements.

On the other hand, If we do find we need to do math on durations, we might consider using a unit-of-measure library like the unitwise gem we met in episode #225. A proper unit type can give us important safeguards against, for instance, accidentally treating a measure of days as a measure of months.

require "unitwise"

duration = Unitwise(8, "week")
duration.to_day.to_i
# => 56

When modeling application domains, it is often tempting to define attributes using primitive types. This is often particularly true when we think of models in terms of their corresponding database tables.

But the power of object-oriented programming and domain-driven design is in inventing objects which faithfully model our understanding of domain concepts. And that's just as true for attribute values like durations as it is for larger entities like courses, semesters, or students. Happy hacking!

Responses