In Progress
Unit 1, Lesson 1
In Progress

Uniform Abstraction Level Part 2: Blank

Video transcript & code

In the last episode, we were working on this Course class.

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

  def duration=(new_duration)
    self[:duration] = Duration(new_duration)
  end
end

Both attributes of this domain model now automatically filter user input to convert it into Whole Value. A Whole Value, as you may recall from episode #401, is a value object that in the words of Ward Cunningam, "captures the whole quantity with all of its implications".

By way of demonstration: when we assign a new name to a course object, it gets auto-converted to a Name object.

One of the special abilities of this Whole Value object is that it is capable of telling us whether it is an exceptional value. This is something we make use of in our views and controller code.

Likewise, we can assign a duration, and see it converted to a Whole Value that also knows it is un-exceptional.

And if we provide input that can't be parsed into a known type of duration, we get an ExceptionalValue that holds onto the original user input, but also lets view code know there's a problem with this field.

require "./models_uniform"

c = Course.new
c.name = "Coffee Quaffing 101"
c.name                          # => Name(Coffee Quaffing 101)
c.name.exceptional?             # => false
c.duration = "20 days"
c.duration                      # => Days(20)
c.duration = "a few weeks"
c.duration                      # => #<ExceptionalValue:0x00555b5db96370 @raw="a few weeks", @reason="Unrecognized format">
c.duration.to_s                 # => "a few weeks"
c.duration.exceptional?         # => true
c.duration.reason               # => "Unrecognized format"

So at this point we've raised all of the possible values this model can contain to an equivalent, high level of abstraction. We're no longer dealing in primitives like strings and integers. These are rich, semantically meaningful objects that carry useful information that view code can make good use of.

Or, at least, that's the theory. The truth is, we've missed something. Can you guess what it is?

There's one other possible type for Course attributes. When we first instantiate a Course, both its name and duration are nil.

And since, in episode #432, we removed the global Object monkey-patch that used to kludge support for this method onto every object, nil no longer responds to exceptional?.

require "./models_uniform"
require "./course_noblank"

c = Course.new
c.name      # => nil
c.duration  # => nil
c.name.exceptional?             # => NoMethodError: undefined method `exceptional?' for nil:NilClass

# ~> NoMethodError
# ~> undefined method `exceptional?' for nil:NilClass
# ~>
# ~> xmptmp-in160262Bv.rb:7:in `<main>'

This is going to cause problems when the form is showing a brand-new, blank course.

So what do we do now? I mean, we could monkey-patch just NilClass, but we just got rid of a monkey-patching solution.

Let's talk about what the nil value means here. Does it simply mean… nil? Of course not. It has a specific meaning in this context.

As Sandi Metz is fond of saying, in object-oriented design, nothing is something. What does this nothing mean? I'd say it's a flag or placeholder indicating a particular state of the name or duration fields.

What state is that, exactly? Well, what do we call it when a physical, paper form has yet to be filled out?

"Blank", exactly! The nil stands in for a blank field. Now that we've identified the implicit concept, let's create an object for it!

We'll go ahead and derive it from our WholeValue base class.

Remember, a blank value should not be considered exceptional. It may not be valid for a finalized Course to have blank fields. But ExceptionalValue objects are only for representing user input values that the system was unable to interpret or coerce. So it's appropriate that this class inherit the WholeValue implementation of #exceptional?.

We also add a to_s converter that just returns an empty string.

class Blank < WholeValue
  def to_s
    ""
  end
end

Now we need to make sure that a new Course has Blank fields instead of nil fields. We do this with a customized initializer, being careful to call through to the super inherited from Struct.

Then, after the super initializer has had a chance to initialize fields, we use struct's square-bracket assignment to default the fields to Blank values.

(If this square bracket business looks weird to you, you might want to watch the episode on Struct, #20.)

Course = Struct.new(:name, :duration) do
  def initialize(*)
    super
    self[:name]     ||= Blank.new
    self[:duration] ||= Blank.new
  end

  def name=(new_name)
    self[:name] = Name(new_name)
  end

  def duration=(new_duration)
    self[:duration] = Duration(new_duration)
  end
end

And now we've finally arrived at our goal: a Course class that deals entirely in domain-level concepts. A brand new Course has blank fields.

After assignment, the name and duration fields are filled with Whole Value objects that encapsulate the semantics of their respective contents.

And all of these values can be queried for whether they are exceptional.

require "./models_uniform"

c = Course.new
c.name      # => #<Blank:0x0056361d1c7150>
c.duration  # => #<Blank:0x0056361d1c7100>

c.name = "Thumbtwiddling 400"
c.name                          # => Name(Thumbtwiddling 400)
c.duration = "2 months"
c.duration                      # => Months(2)

c.name.exceptional?             # => false
c.duration.exceptional?         # => false
Course.new.name.exceptional?    # => false

At this point, let's fire up our server.

We click to add a new course.

Just as in previous episodes, the fields show up blank. But this time, we know they are backed by objects that explicitly represent the Blank status.

We fill in a name, and an invalid duration, and submit.

We get the same form back again, with an problem report.

This time we enter a recognizable duration, and successfully submit the form.

Let's take one last look at the model class powering this interaction.

In a conventional ActiveRecord-style model, this model would contain validation declarations in order to verify the format of the duration field and report any problems to the view layer. But this model has remained blissfully free of any validation code so far, because it delegates the interpretation of valid input values to Whole Value collaborators.

That's not to say that we won't ever have to deal with validations at the business model level. There are some validations which are specific to the model and don't make sense to push down to the field value level. But for right now, it's interesting to see how far we've gotten without any kind of traditional validation machinery.

Now, it's possible that you're staring at this and saying: "that just looks weird and different from any domain modeling I've ever done". Unfortunately, the domain layer in a lot of modern application frameworks is so infected by nitty-gritty database semantics, that talking in terms of "strings" and "integers" and other primitive types just seems like the "normal" thing to do.

What I've tried to give you a taste of in the last few episodes is classic OO domain modeling as elucidated by Ward Cunningham, one of the great minds in object-oriented design or indeed in programming in general. I hope it inspires you to revisit how you model domain concepts in your applications.

If you're intrigued by this approach, I know that there's a good chance you're now wondering how to pragmatically bring it into the Ruby on Rails world. That's a topic I hope to tackle in some future episodes. But for now, happy hacking!

Responses