And that’s a Very Good Thing™ for both language communities. Yes, it’s good to know both languages and to use each where they make sense. But They’re Not The Same Thing. Obvious, yes?
Abe Voelker wrote a blog post in which he sang the praises of the Kleisli Gem to, in effect, let you write Haskell code in your Ruby. He started out by demonstrating a “naive” validation object for validating an uploaded image according to four criteria, verifying that the image:
- Has a size of less than one megabyte;
- Is a valid image format;
- Is of type PNG, JPEG, or GIF; and
- Has dimensions of less than 5,000 by 5,000 pixels.
In addition, he states a requirement for “a JSON-friendly version of the validation return value as the website is uploading the image via Ajax”.
Fine. Easy enough, without resorting to exotic “Reese’s Pieces” language concoctions.
The Gist copied at the end of this post is what I’d consider a first draft of a rewritten
AvatarValidator class. It’s a somewhat more readable version of the original; both are still massive SRP violations. They do too much; they have leaky abstractions through and through. But this is still an improvement:
Command Pattern and State Machine
The original code made was clearly a classic command pattern, but implemented in a massive nested-if furball. You had to squint to see that it was implementing a stepwise state machine, with subsequent states being implemented as ever-more-deeply-nested
In the first replacement, the
#validate method (inside the guard clause) is a sequence of calls to
private methods with intention-revealing names that (should) make it easier to find what you’re looking for, or to understand the overall process.
Error Detection and Message Output
The original code has inline, nested
if statements whose
else clauses each build and return literal strings, more than doubling the length of the
#validate method (lines 10-33) compared with the new implementation. Changes to the original output messages do not originate in a “single source of truth”, which complicates and adds risk to change; are you sure you’ve done everything and only what you set out to do? (Remember, there are no tests yet.)
In the new code, any validation error detected will raise a
RuntimeError whose message encodes an identifier for the error, along with a single relevant data item. The handler for that
RuntimeError calls a
private method that centralises all text strings for error messages, making it easy to change later if needed (e.g, for i18n).
What’s Our Input?
The original code passes in an
uploaded_file parameter, which is assigned to an instance variable (at line 7). That ivar is then used, duck-type style, to find its size (by calling the
#size method at line 12) and by calling the
path method of the return object from
#tempfile method at line 15).
In contrast, the first whack at a replacement
AvatarValidator has a nested class,
AvatarValidator::UploadedFile, which reports the
#size (needed for validation) and produces a
MiniMagick::Image instance of the data in the uploaded file (however that file was specified, whether as a blob of data, a filename or an instance of
- There are no tests. Even if there were, it’s tedious/dangerous to attempt to test individual validation steps in isolation.
- The image-dimension validation now separately checks height and width, reporting different error messages when invalid; the original code (see lines 18-19) did not.
AvatarValidatorstill has repetitive code. The internal validation-step methods (
#verify_allowed_format, and (to a slightly lesser degree)
#verify_image_dimensions) each follow the same pattern: bail out if we’re valid, or call
#fail_withif we’re not.
- This code is unloved by RuboCop, a widely-used Ruby style checker and our shop standard. In particular, the offence triggered by the
Assignment Branch Condition size for verify_image_dimensions is too high. [16.16/15]is one I have learned to take seriously. RuboCop’s default trigger value of 15 is in fact higher than what you should see if, say, you were following Sandi Metz’ rules.
A new iteration of the rewrite will soon address these problems, and produce code that is neither a straw man to provide a convenient excuse to switch languages, nor code that would budge the WTF-O-Meter® at your team’s code review.