Ruby makes it easy to write concise code. This is a benefit of the language and the ecosystem. Matz focuses on “making programs succinct” and Rails boasts that it lets you build “in a matter of days” what used to take months.

Concise code can have a dark side. Convenient interfaces can tuck away complexity and side effects that might surprise you later. Brevity in software comes at the cost of diligence both from developers and reviewers. It is especially important to understand how your abstractions work and the business rules they implicitly handle.

Moving Fast

Imagine you are adding a new feature to your Ruby on Rails web application. This feature breaks down into three small tasks:

Integrate with an internal API which provides information about the current user

Use information about the current user in order add a welcome message to the header of each page

Display a flag alongside the message corresponding to the user’s country field

The current user JSON looks like this

{ "status" : "success" , "data" : { "name" : { "first" : "Edmond" , "last" : "O'Connell" }, "address" : { "street1" : "53236 Camilla Light" , "street2" : null , "city" : "Pierceville" , "state" : "NJ" , "country" : "United States" } } }

To integrate with the API you create three simple classes with ActiveModel::Model :

class User include ActiveModel :: Model attr_accessor :address , :name end class Name include ActiveModel :: Model attr_accessor :first , :last end class Address include ActiveModel :: Model attr_accessor :street1 , :street2 , :city , :state , :country end

To extract the user data you use the new #dig method introduced in Ruby 2.3:

User . new ( name: Name . new ( response . dig ( 'data' , 'name' )), address: Address . new ( response . dig ( 'data' , 'address' ))) )

Finally, you add a current_country view helper method and create a new view partial:

module UserHelper def current_country return 'Unknown' unless current_user current_user . address . country end end

<div id= "user-welcome" > <% if current_user %> <span> Welcome back <%= current_user . name . first %> ! </span> <% end %> <div id= "user-welcome-flag" > <%= image_tag ( "/imgs/flags/ #{ current_country } .png" ) %> </div> </div>

Breaking Things

A few weeks pass and you find out that some pages rendered the message “Welcome back !” and a broken image in place of the flag. The internal API encountered its own error and returned

{ "status" : "error" , "message" : "Internal server error" }

Oddly enough this did not break your code:

response = { 'status' => 'error' , 'message' => 'Internal server error' } name = response . dig ( 'data' , 'name' ) # => nil address = response . dig ( 'data' , 'address' ) # => nil user = User . new ( name: Name . new ( name ), address: Address . new ( address )) user . name # => #<Name:0x0011910412163> user . address # => #<Address:0x0011910412163> user . name . first # => nil user . address . country # => nil

Feeling a bit embarrassed by the bug you reflect on how you could prevent similar issues in the future:

What if the internal API renames the country field to country_code ? That would also silently break the view. Can I only avoid these cryptic bugs by being vigilant about every external dependency?

Reflection

The features in Ruby and Rails which let you write concise code can also let you cut corners. Consider our Name class and how the corresponding response data was originally extracted:

class Name include ActiveModel :: Model attr_accessor :first , :last end module ResponseHandler def self . extract_name ( response ) Name . new ( response . dig ( 'data' , 'name' )) end end

Let’s rewrite Name without ActiveModel or attr_accessor :

class Name # Inlined from Active Model source http://git.io/vuECr def initialize ( params = {}) params . each do | attr , value | self . public_send ( " #{ attr } =" , value ) end if params super () end def first @first end def first = ( first ) @first = first end def last @last end def last = ( last ) @last = last end end

Imagining our code like this is instructive. It seems like three questions are now immediately obvious

Should the initializer invoke setter methods for any key passed to the initializer?

Will Name ever be invoked without arguments?

ever be invoked without arguments? Are these public setter methods necessary or is Name a value object?

Let’s throw out #dig and instead handle each edge case manually.

module ResponseHandler def self . extract_name ( response ) return Name . new ( nil ) unless response . key? ( 'data' ) return Name . new ( nil ) if response [ 'data' ]. empty? Name . new ( response [ 'data' ][ 'name' ]) end end

Expanding this method highlights three distinct outcomes which are each important to consider. The original code properly handled a valid user object but overlooked two important edge cases:

1. API error handling when response['data'] is nil

return Name . new ( nil ) unless response . key? ( 'data' )

This happened when the internal API encountered an error. This condition should instead result in our application notifying the end user of an error.

2. Alternate behavior when a user is not returned

return Name . new ( nil ) if response [ 'data' ]. empty?

This corresponds to the following JSON

{ "status" : "success" , "data" : {} }

This might mean that the current user has not yet logged in. It could also be a buggy response.

Depending on how robust you expect the internal API to be you might want to handle this case independently as well. If this is invalid state then the response handler should raise an error. If it is valid state and you want to handle cases where the user is not logged in then there should be a separate Guest class independent of the User class.

Both of these options are better than implicitly assuming this condition never happens. Once the code embedding your assumption is deployed it is too easy to forget and unknowingly introduce a silent regression in the future.

Conclusions

Ruby certainly makes it easy to write concise code. The question then is how do you reap these benefits without cutting corners accidentally? At BlockScore we have a few practices which help us write better Ruby.

1. Strict and simple dependencies

Active Model’s initializer is permissive and this led to surprising behavior. Consider the benefit of a strict alternative like anima:

# Test cases valid_arguments = { first: 'John' , last: 'Doe' } missing_argument = { first: 'John' } extra_argument = { first: 'John' , last: 'Doe' , nickname: 'Jim' } # With Active Model class Name include ActiveModel :: Model attr_accessor :first , :last end Name . new ( valid_arguments ) # => #<Name:0x0011910412163 @first="John", @last="Doe"> Name . new ( missing_arugment ) # => #<Name:0x0011910412163 @first="John"> Name . new ( extra_argument ) # => NoMethodError: undefined method `nickname=` Name . new ( nil ) # => #<Name:0x0011910412163> Name . new # => #<Name:0x0011910412163> # With Anima class Name include Anima . new ( :first , :last ) end Name . new ( valid_arguments ) # => #<Name first="John" last="Doe"> Name . new ( missing_argument ) # => Anima::Error: Name attributes missing: [:last] Name . new ( extra_argument ) # => Anima::Error: Name attributes missing: [], unknown: [:nickname] Name . new ( nil ) # => NoMethodError: undefined method `keys' Name . new # => ArgumentError: wrong number of arguments (given 0, expected 1)

2. Meticulous code review

An inconspicuous line of code like

Name . new ( response . dig ( 'data' , 'name' ))

can encode multiple important code paths. With Ruby it is especially important to visualize the equivalent “expanded” code.

3. Static analysis

Tools like reek and rubocop are great for learning how to write better code. Reek might point out a design issue before you notice it. Rubocop now goes way beyond style: the next release will include eight new cops for helping you catch bad performing code.

4. Mutation testing

Mutation testing helps me write better Ruby. It sniffs out dead code, helps me find missing tests, and generally helps me think about the assumptions I’ve made.