Uniform Abstraction Level Part 2: Blank
Video transcript & code
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
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
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
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
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
Blank fields instead of
nil fields. We do this with a customized initializer, being careful to call through to the
super inherited from
Then, after the
super initializer has had a chance to initialize fields, we use struct's square-bracket assignment to default the fields to
(If this square bracket business looks weird to you, you might want to watch the episode on
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
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!