In Progress
Unit 1, Lesson 21
In Progress

Exceptional Value

Video transcript & code

In the last episode we grappled with how to deal with unrecognizable user input in the context of the Whole Value pattern.

We wound up with a conversion function that returned nil as a flag for a bad value, instead of raising an exception.

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

And, a Course model that manages errors for its fields in a separate, parallel hierarchy to the fields themselves.

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

We talked in that episode about why this is a smell, and about the coupling it introduces. I'm not going to reiterate that discussion today, and I'm also not going to reintroduce our example application. So if you haven't seen that episode yet, you might want to pause right here and then come back.

So, if this is a smell, what do we do to get rid of it?

The answer begins back at our conversion function.

In the previous episode we replaces an exception with a nil return.

But nil is a semantically impoverished type, and as a result we pushed more responsibilities onto the containing model object.

What if we returned something with richer meaning? But what might that be?

Well, what exactly do we need to represent here? We have some input which represents an exception to the input that we know how to convert to a valid duration.

But it's not exceptional in the sense that it represents a program error. After all, having users type in unrecognizable input is a normal part of an application's day. It's not exceptional in the sense that we should be raising an exception.

What if we were to return an Exceptional Value object?

Including the raw value that couldn't be parsed.

And a reason for the value being exceptional?

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
    ExceptionalValue.new(raw_value, reason: "Unrecognized format")
  end
end

Having sketched out the object we want, let's go implement this class.

We give it an initializer that can take a raw value, and a reason.

We make expose the reason using a reader method.

We make it possible to ask the object if it is exceptional, which, of course, it is.

We'll see the reason for this method shortly.

And we also make the object convertible to a string, in which case we simply return the string representation of the raw, original value.

class ExceptionalValue
  def initialize(raw_value, reason: "Unspecified")
    @raw    = raw_value
    @reason = reason
  end

  attr_reader :reason

  def exceptional?
    true
  end

  def to_s
    @raw.to_s
  end
end

Now, how are we going to make use of this new type of object?

Well, first off, let's hop over to our form template.

Where previously we iterated through the course's errors list looking for problem fields, now we just go through the course's fields asking if any are exceptional.

Then, down in the duration field markup, instead of asking the course if there's an error on that field name, we just ask the field value itself whether it is exceptional.

template :course_form do
  <<~EOF
  <h1>Add a course</h1>
  <form action="/" method="POST">
    <% course.to_h.select{|k,v| v.exceptional?}.each do |field, value| %>
      <div class="toast toast-danger"><%= field %>: <%= value.reason %></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.duration.exceptional? && ' 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

Next we turn our attention to the course post handler.

Here again, we replace the query of the errors list with a direct check of the field values, to see if any are exceptional.

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

There's a glaring problem with all of the code we've just introduced.

Course fields may have one of several kinds of value.

When the course is brand new, the fields are nil.

The course name is still represented as a primitive string.

Recognizable course durations are converted to Duration child classes, like Days.

And unrecognizable durations are converted to ExceptionalValue objects.

But so far, we only one kind of these kinds of object responds to the exceptional? predicate message.

require "./models"

course = Course.new
course.name                     # => nil
course.name = "Basketweaving 101"
course.name.class               # => String
course.duration = "2 days"
course.duration                 # => Days[2]
course.duration = "48 hours"
course.duration
# => #<ExceptionalValue:0x0056505abe7408 @raw="48 hours", @reason="Unrecognized format">

For right now, let's address this deficiency the quick-and-dirty way.

We'll monkey-patch Object to add the exceptional? predicate. We'll make it return false.

class Object
  def exceptional?
    false
  end
end

That way, ExceptionalValue objects will return true, and every other kind of object will be considered non-exceptional.

Now we can go back to our Course model, and get rid of all the code for tracking errors in fields.

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

Let's give this new version a whirl. We'll fire up our server, and go to the course listing.

Then we'll click "New Course". We'll enter a course name, and an unrecognizable duration.

We see the form re-rendered, and the problems reported.

When we fix our submission and re-submit, it's accepted.

So we've proven that we haven't broken anything. But behind the scenes, we've improved our design, by representing exceptional values as their own, rich data type. In the process, we got rid of the parallel hierarchy of errors, and enabled our field values to stand on their own.

This pattern isn't something I came up with. Just like the Whole Value pattern, it comes from a paper by Ward Cunningham called "The CHECKS Pattern Language of Information Integrity".

We'll be seeing more patterns from this paper in future episodes.

One code smell that's still present in this code is the monkey-patching of Object.

This smell is indicative of a bigger problem that we have yet to address. But we'll get to that in an upcoming episode. Happy hacking!

Responses