In Progress
Unit 1, Lesson 1
In Progress

Parallel Hierarchy

Video transcript & code

In Episode #401, we talked about the Whole Value pattern. We saw how using objects to represent domain concepts like "course duration" cuts through a tangle of problems caused by using only primitive data types.

Today we return to that scenario. This time, we've expanded the code into a little web application.

As you can see, we have a list of courses.

We can choose to create a course. We're presented with a simple form.

We can fill out the form with a course name and a duration, and add it to the list.

Let's take a look at some of the code supporting this functionality.

In the post handler, we make a new Course object, and assign it values from the request parameters.

Then we add it to a global list of courses, and redirect back to the home pate.

post "/" do
  course = Course.new
  course.name     = params.fetch("name")
  course.duration = params.fetch("duration")
  course_list << course
  redirect to("/")
end

Let's take a look at that Course class.

As you might recall from episode 401, we've decided to handle the possibility of having courses measured in days, weeks, or months by pushing duration semantics down into a Duration object.

To ensure that this is the case, this class has a specialized writer method for the duration attribute.

It passes the incoming value through a Duration conversion function before assigning it.

Course = Struct.new(:name, :duration) do
  def duration=(new_duration)
    self[:duration] = Duration(new_duration)
  end
end

Let's take a look at that function next.

This function is responsible for taking various types of input and ensuring that the output is one of the Duration subtypes.

It starts out by checking to see if the input is already a Duration. If so, it just returns it unchanged. This is consistent with the usual idempotent behavior of conversion functions. (We talked more about conversion functions in episode #207).

If the input isn't a duration to begin with, it's likely to be a string representing a duration. So the next few cases do some very basic regular expression matching against the incoming string.

For instance, if the input consists solely of a number followed by some whitespace and then the word "months", the function constructs a Months duration, using number from the string as the magnitude.

This is one of those cases where ruby's "magic" last-match variables come in particularly handy. For more about these variables, check out episode #406.

What happens if the incoming value is neither a Duration nor a string that can be interpreted as a Duration?

In that case, we raise an error.

def Duration(raw_value)
  case raw_value
  when Duration
    raw_value
  when /\A(\d+)\s+months\z/i
    Months[$1.to_i]
  when /\A(\d+)\s+weeks\z/i
    Weeks[$1.to_i]
  when /\A(\d+)\s+days\z/i
    Days[$1.to_i]
  else
    fail ArgumentError, "Invalid duration: #{raw_value.inspect}"
  end
end

We can see this in action if we return to our app.

If we try to construct a course with an unsupported duration format, we get an application error.

This is less than ideal, from a user experience point of view. If the user types in something the system doesn't understand, we want to present them with a chance to correct the problem. Not with a crash.

So what should we have this code do instead?

Well, clearly raising an exception is causing problems.

When the conversion function can't convert its input, we should have it return some kind of flag value instead.

The classic go-to choice for such a flag value is, of course, good 'ole nil.

def Duration(raw_value)
  case raw_value
  when Duration
    raw_value
 when /\A(\d+)\s+months\z/i
    Months[$1.to_i]
  when /\A(\d+)\s+weeks\z/i
    Weeks[$1.to_i]
  when /\A(\d+)\s+days\z/i
    Days[$1.to_i]
  else
    nil
  end
end

Next, let's update our form template to be aware of the possibility of nil values.

We'll add a check for a nil duration in the code that displays the duration field. If it's nil, that indicates a bad value, so we add a CSS class that will visually flag the problem.

template :course_form do
  <<~EOF
  <h1>Add a course</h1>
  <form action="/" method="POST">
    <div class="form-group">
      <label class="form-label" for="name">Name</label>
      <input class="form-input" type="text" name="name"
             value="<%= course.name %>"/>
    </div>
    <div class="form-group<%= course.duration.nil? && ' has-danger' %>">
      <label class="form-label" for="duration">Duration</label>
      <input class="form-input" type="text" name="duration"
             value="<%= course.duration %>"/>
    </div>
    <div class="form-group">
      <input class="btn btn-primary" name="Create" type="submit"/>
    </div>
  </form>
  EOF
end

Finally, we update the course posting action. Now it will inspect the constructed Course object. If any of the fields are nil after assignment, it will re-render the form instead of accepting the submission.

post "/" do
  course = Course.new
  course.name     = params.fetch("name")
  course.duration = params.fetch("duration")
  if course.values.any?(&:nil?)
    erb :course_form, locals: {course: course}
  else
    course_list << course
    redirect to("/")
  end
end

OK, let's give this new version a try.

We click the button to make a new course. Right away, we can see one problem.

We haven't even tried to submit it yet, and it's already rendering the duration field in red, indicating an error.

This is because we're using nil to indicate a problem in the duration field. But nil is also the default field value in a blank Course.

This is a great illustration of one of the drawbacks of using nil as a flag value for some condition: nil is too common, and it crops up for all kinds of reasons.

But, let's leave this issue for later, and forge ahead. We fill in a course name and an invalid course duration.

Then we submit. As expected, instead of the course list, we are presented with the same form again, with the duration field still highlighted in red.

But there's something missing here. The form is telling us we provided invalid input… but it has lost the input we provided. Instead of being able to edit our entry, we have to reconstruct it from scratch.

This is one of the more obnoxious antipatterns in user experience design. When a form asks me to edit my submission, I want to see my original entries echoed back to me.

Clearly, the approach of rejecting unrecognized input and substituting nil has some serious drawbacks.

We need a way to flag a problem in a submission, and keep the old value around.

Here's one way we could approach this functionality. We could add a list of errors to the Course model.

Then, in the duration writer, we update the logic to check the outcome of the attribute assignment.

If it results in a nil, we go ahead and assign the original, raw string value anyway. But we also add an error related to the duration field.

Course = Struct.new(:name, :duration) do
  def duration=(new_duration)
    unless self[:duration] = Duration(new_duration)
      self[:duration] = new_duration
      errors[:duration] = "Unrecognized duration"
    end
  end

  def errors
    (@errors ||= {})
  end
end

Then we update our template code to look for errors instead of nil values.

While we're at it, now that we have more field error information available to us, we add a new "toast" message to let the user know if there's a problem with any fields.

template :course_form do
  <<~EOF
  <h1>Add a course</h1>
  <form action="/" method="POST">
    <% course.errors.each do |field, problem| %>
      <div class="toast toast-danger"><%= field %>: <%= problem %></div>
    <% end %>
    <div class="form-group">
      <label class="form-label" for="name">Name</label>
      <input class="form-input" type="text" name="name"
             value="<%= course.name %>"/>
    </div>
    <div class="form-group<%= course.errors[:duration] && ' has-danger' %>">
      <label class="form-label" for="duration">Duration</label>
      <input class="form-input" type="text" name="duration"
             value="<%= course.duration %>"/>
    </div>
    <div class="form-group">
      <input class="btn btn-primary" name="Create" type="submit"/>
    </div>
  </form>
  EOF
end

And we likewise update the course post handler, to check for errors and re-render the form if it finds any.

post "/" do
  course = Course.new
  course.name     = params.fetch("name")
  course.duration = params.fetch("duration")
  if course.errors.any?
    erb :course_form, locals: {course: course}
  else
    course_list << course
    redirect to("/")
  end
end

If you work on Rails projects, this pattern is probably looking pretty familiar to you.

Let's run this code.

We can bring up a new course form, and see that it renders blank with no fields flagged.

When we enter course information, including a bad duration, and submit, we get a warning message and the problem field highlighted in red.

Notably, this time our invalid entry is echoed back instead of thrown away, allowing us to edit it to an acceptable value.

This time, when we submit, we see our course accepted into the list.

This version has worked out a lot better.

But, let's take another look at our model code.

The whole point of adding Whole Value objects to represent course durations in the first place, was to make sure that the semantics of a course duration were entirely encapsulated by the value of the attribute.

Before, in episode #401, we had uses this pattern to deal with the fact that duration-related methods kept proliferating and cluttering up our Course model. We're not quite back at that unhappy state of affairs. But we've still had to push some information about the duration value to somewhere outside the duration field value.

And in the process, we've introduced a code smell. It's what I think of as the parallel hierarchy smell. The book Refactoring by Martin Fowler talks about a parallel inheritance hierarchy smell. But it's possible to have other kinds of parallel hierarchies that are equally smelly.

For every field in this model, there is now, potentially, also an entry in the errors list. This smell is a red flag, telling us that we're forcing the model object to manage information on behalf of its collaborators. Information that, perhaps, would be better off pushed down into the field values.

Parallel hierarchies introduce new kinds of coupling into our code. Like a child that's too young to be left alone without its parent, now our field values cannot be left alone without their parent model object. They don't have complete information on their own.

Any helper method we write to render durations will also have to receive the Course object, so we can ask it about any errors associated with the field.

def render_duration(duration, course)
  if course.errors[:duration]
    # ...
  else
    # ...
  end
end

The fact that certain high-profile domain modeling frameworks make this kind of coupling into standard operating procedure, doesn't make this smell any less smelly. In the next episode, we'll see how we can eliminate the smell, using the Exceptional Value pattern. Until then: happy hacking!

Responses