In Progress
Unit 1, Lesson 1
In Progress

Fattr

Video transcript & code

As a rule, Ruby code is quite concise when compared to most other object-oriented programming languages. We don't tolerate a lot of boilerplate and repetition. But even so, there is always room for improvement.

Case in point: consider this very typical definition of a class. It has three attributes: name, rank, and serial number.

class Spartan
  attr_accessor :name
  attr_accessor :rank
  attr_accessor :serial_number

  def initialize(name: "John", rank: "Master Chief", serial_number: "117")
    @name = name
    @rank = rank
    @serial_number = serial_number
  end
end

This class doesn't really do much of anything yet. Let's count the number of times the name of each attribute is repeated in this do-nothing class.

  1. In the attr_accessor definition
  2. In the keyword arguments for the initialize method.
  3. In the name of an instance variable when first assigning the attribute.
  4. In the parameter variable used as the source of the initialization value.

That's a lot of repetition. And we do this, or something like it, over and over again when setting up a new class.

Today I want to show you an alternative. In order to use it, we have to install and require a Rubygem called "fattr", from Ara T. Howard. fattr stands for "fatter accessors". It is one of my favorite gems, and it usually finds its way into the dependencies of most of the apps I build.

Now let's redefine our class using fattr. For the time being, we'll remove the initializer. Then we'll change each use of attr_accessor to fattr.

By itself, this functions a lot like what we expect from a basic attr_accessor declaration. We can instantiate a Spartan object and then query the attributes we've defined. By default they are nil. We can assign to them and get the values back. Nothing very exciting here.

require "fattr"

class Spartan
  fattr :name
  fattr :rank
  fattr :serial_number
end

s = Spartan.new
s.name                          # => nil
s.rank                          # => nil
s.name = "John"
s.name                          # => "John"

One of the features our original class had was a set of default values for the attributes. Normally, we would have to add an initializer to set up default values. But with fattr, we can add default values to the attribute declarations directly. We'll add defaults for name, rank, and serial number. Now when we create a new object, we can see that the attributes have default values.

require "fattr"

class Spartan
  fattr :name => "John"
  fattr :rank => "Master Chief"
  fattr :serial_number => "117"
end

s = Spartan.new
s.name                          # => "John"
s.rank                          # => "Master Chief"
s.serial_number                 # => "117"

It's not always convenient to embed the defaults as literals in the class definition. For instance, in some cases the default for one attribute might need to be calculated using the value of another attribute. For cases like this, there is another way to define defaults. We can pass a block to the definition. Let's demonstrate this with a new attribute called designation. By default, it will be created by combining the spartan's name and serial number.

(Note that we have to put parentheses around the attribute name in order to comply with Ruby's parsing rules for curly-brace blocks.)

When we create a new object and ask it for its designation, we can see the default value is generated as expected.

Defaults in fattr are lazily assigned. To see what this means, let's create a new object and give it a custom name and serial number. Then let's ask it for its designation. This time, we see a new designation. Fattr waited until we called the designation accessor before calculating the value.

It's worth noting, however, that while defaults are assigned lazily, they are not calculated every time. Once a value is set, it is set. We can see that if we change the name of our object and then check its designation again. Because defaults are only assigned once, this attribute is not recalculated.

require "fattr"

class Spartan
  fattr :name => "John"
  fattr :rank => "Master Chief"
  fattr :serial_number => "117"
  fattr(:designation) { "#{name}-#{serial_number}" }
end

s = Spartan.new
s.designation                   # => "John-117"

s = Spartan.new
s.name = "Linda"
s.serial_number = "058"
s.designation                   # => "Linda-058"
s.name = "Kelly"
s.designation                   # => "Linda-058"

One more interesting property of fattr defaults is that we can reset an attribute to its default any time we want to. fattr defines a bang version of each accessor method. If we send the bang version, it resets the attribute to the default value defined in the class definition.

require "fattr"

class Spartan
  fattr :name => "John"
  fattr :rank => "Master Chief"
  fattr :serial_number => "117"
  fattr(:designation) { "#{name}-#{serial_number}" }
end

s = Spartan.new
s.name = "Linda"
s.serial_number = "058"
s.name                          # => "Linda"
s.serial_number                 # => "058"
s.name!
s.serial_number!
s.name                          # => "John"
s.serial_number                 # => "117"

This is all very interesting, but we haven't quite recreated all of our original class abilities. Originally, we were also able to pass keys and values into the class constructor in order to initialize attribute values.

Before we recreate this functionality with fattr, there is one more feature we need to understand. When setting attributes, we can omit the equals sign. We can use the reader method with an argument, and it doubles as a writer.

s = Spartan.new
s.name "Linda"
s.name                          # => "Linda"

Now lets write the initializer. We'll write it to collect keyword arguments into a single attributes hash. Then, for each key/value pair, it will send the key as a message to itself, with the value as an argument.

Let's instantiate an object, and pass in a full set of attribute values. When we interrogate the resulting Spartan object, we can see that it has taken on all of the attribute values we passed into the initializer.

require "fattr"

class Spartan
  fattr :name => "John"
  fattr :rank => "Master Chief"
  fattr :serial_number => "117"
  fattr(:designation) { "#{name}-#{serial_number}" }

  def initialize(**attributes)
    attributes.each do |k, v|
      public_send k, v
    end
  end
end

s = Spartan.new(name: "Linda", rank: "Petty Officer 2", serial_number: "058")

s.name                          # => "Linda"
s.rank                          # => "Petty Officer 2"
s.serial_number                 # => "058"

We have now completely recreated our original class' functionality. But unlike before, our new definition has no repetition. Every attribute name is referenced exactly once in the class definition. If we need to add a new attribute, we'll be able to add it in just one place. If we decide to remove an attribute, we only have to delete one line.

fattr is not the only Rubygem to offer this kind of declarative, concise syntax for defining attributes. But I like it a lot because most of the time it gives me exactly what I want without a lot of extras I don't need. I've been using it in my projects for years, and it has saved me quite a bit of time and typing.

Happy hacking!

Responses