Reading through Sandi Metz’s book Practical Object-Oritented Design in Ruby (POODR), I found myself yet again hit with several epiphanies on best practices of how to do things the Sandi Metz way.

In this post, we will expose some problems with creating a Ruby object without using Hashes or keyword arguments, and offer a couple solutions on handling default values when initializing a Ruby object.

To start, let’s begin by creating a basic Ruby Movie class.

class Movie

attr_accessor :title, :year, :runtime







end def initialize(title, year, runtime) @title = title @year = year @runtime = runtimeend end matrix = Movie.new("The Matrix", 1999, 150) matrix.title #=> "The Matrix"

matrix.year #=> 1999

matrix.runtime #=> 150

Pretty basic stuff.

However, as Metz always mentions…

Design is more the art of preserving changeability than it is the act of achieving perfection.

So let’s see what happens if we decide to add a few more attributes to our Movie class.

Now creating movie instances is getting quite confusing and prone to error.

What do the four numbers mean again?

What order must these arguments be set in?

What if we don’t necessarily want to google how much The Matrix made in the box office? What default value should we be putting in there instead?

Using keyword arguments for initialization

To remove argument-order dependencies, we can refactor our #initialize method using keyword arguments like so.



attr_accessor :title, :year, :runtime, :box_office, :oscar_noms



def initialize(

title:,

year: "19XX",

oscar_noms: "0?",

runtime: "Longer than an hour",

box_office: "More than a million?"

)











class Movieattr_accessor :title, :year, :runtime, :box_office, :oscar_nomsdef initialize(title:,year: "19XX",oscar_noms: "0?",runtime: "Longer than an hour",box_office: "More than a million?" @title = title @year = year @runtime = runtime @box_office = box_office @oscar_noms = oscar_noms end

end matrix = Movie.new(

year: 1999,

title: "The Matrix",

runtime: 150,

oscar_noms: 4

box_office: 463517383,

)

Using keyword arguments, we can specify the key and its default values directly into the #initialize method.

More importantly, initializing The Matrix is a lot easier on the eyes now, and best of all, we are no longer chained to a fixed order of submitting arguments into the #initialize method.

Though the code is more verbose than using fixed-order arguments, the refactored #initialize method wins in terms of readability, and preventing any accidental dyslexia. (i.e. swapping order of the variables)

Default value option #2: Using #merge

Another way to specify default values is to create a separate #defaults method and leveraging Ruby’s #merge method as follows:

class Movie

attr_accessor :title, :year, :stars_keanu



def initialize(movie_args)

movie_args = defaults.merge(movie_args)



@title = movie_args[:title] @year = movie_args[:year] @stars_keanu = movie_args[:stars_keanu] end private def defaults

{year: "No earlier than 20th century, stars_keanu: "???"}

end end inception = Movie.new(

year: 2010,

title: "Inception",

stars_keanu: false

) inception.stars_keanu #=> false

The method #merge works by first taking in the default hash returned by #defaults method and merging in arguments fed in by the user.

In other words, any key-value pair presented by the user will take precedence over the default key-value pairs under the #defaults method.

Our #initialize method looks cleaner this way, albeit arguably somewhat harder to follow along than using keyword arguments.

Having said that, creating a separate wrapping method for #defaults and leveraging #merge is recommended if the default values start getting more complex than just simple strings and numbers.

I hope you enjoyed this post. Feel free to leave a few claps down below!