In Progress
Unit 1, Lesson 1
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

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
<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.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!