Custom Ruby Class Macros with Class Instance Variables
Tags: ruby, macro, classinstancevar
Published: 2018-04-11 15:29:48

---
title: Custom Ruby Class Macros with Class Instance Variables
published: true
description: Class macros are class methods that are only used when a class is defined. They allow us to dry up shared code at across classes. In this post, we'll build a custom class macro that leverages class instance variables to define class-specific attributes. 
tags: Ruby, Macro, Class Instance Var
---

Class macros are class methods that are only used when a class is defined. They allow us to dry up shared code at across classes. In this post, we'll build a custom class macro that leverages class instance variables to define class-specific attributes. 

## Shared Code at the Class Level
We'll revisit a domain model from an [earlier post on my personal blog](http://www.thegreatcodeadventure.com/roll-your-own-xml-templater-in-ruby-2/) in which we built our very own XML templating engine. We built our engine with the help of a module, `XmlFormattable`, that we mixed in to any classes that need to respond to `#to_xml` to generate strings of XML. 

Previously, we included our module in our `Album`, `Track` and `Artist` classes to help us write XML describing audio release metadata. Now our app has grown and we have a variety of sub-classed `Album` classes for the specific online stores that we are sending this metadata to. 

We have three album sub-classes:

* `Itunes::Album`
* `Spotify::Album`
* `DDEX::Album`

Where the `Itunes::Album` and `Spotify::Album` classes describe albums specifically formatted for those stores, and the `DDEX::Album` class defines the default album that can be sent to a variety of different stores. *(DDEX is the industry standard for defining audio release XML)*

Our shared `XmlFormattable` module needs to know the XML format of a given album, i.e. the iTunes, Spotify, or DDEX format, in order to write the correct XML and validate the written XML against the correct schema. 

Our `XmlFormattable` module looks something like this:

``` ruby
module XmlFormattable  
  def to_xml
    formatter.format
  end

  def render(file_name, object, options)
    formatter.render(file_name, object, options)
  end

  def formatter
    @formatter ||= XmlFormatter.new(self, "format type goes here!!")
  end
end  
```

### The Wrong Way to DRY Up our Classes

How can we give awareness of the format type to the `XmlFormattable` module? 

Well, this is information that will be shared across every instance of a given `::Album` class. One option would be to define class constants in each class:

```ruby
class Itunes::Album
  include XMlFormattable
  XML_FORMAT = "itunes_version_9"
end
```

```ruby
class Spotify::Album
  include XmlFormattable
  XML_FORMAT = "spotify_version_7"
end
```

```ruby
class DDEX::Album
  include XmlFormattable
  XML_FORMAT = "ddex_version_38"
end
```

Then, we could reference our class constant in the `XmlFormattable` module like this:

``` ruby
module XmlFormattable  
  def to_xml
    formatter.format
  end

  def render(file_name, object, options)
    formatter.render(file_name, object, options)
  end

  def formatter
    @formatter ||= XmlFormatter.new(self, XML_FORMAT)
  end
end  
```

**This approach violates the Single Responsibility Principle.** The whole point of creating the `XmlFormattable` module in the first place was to take the responsibility of writing XML _out_ of the individual album classes. This approach introduced an attribute related to writing and validating XML back into the album class. And it made it the sole responsibility of each `::Album` class to define and manage that attribute.

At the same time, we've made our `XmlFormattable` module reliant on a piece of info, `XML_FORMAT`, that is not defined or controlled by this module at all. Instead it is managed by the class that this module is mixed in to. This forces whoever uses this module in the future to know and remember to define the `XML_FORMAT` class constant in the class in which they are including the module. This split brain state makes it hard to scale the use of our module. 

How can we make our module responsible for defining a class attribute _and_ make sure that the class attribute is _different_ for the different classes? I thought you'd never ask!

### Class Macros
Class macros are class methods that are evaluated only once, when a class is defined. We'll define a class macro in our `XmlFormattable` module and we'll call on this class macro in each `::Album` class to specify the XML format. 

Here's how we'll use our macro:

```ruby
class Itunes::Album
  include XmlFormattable
  xml_format "itunes_version_9"
end
```

```ruby
class Spotify::Album
  include XmlFormattable
  xml_format "spotify_version_7"
end
```

```ruby
class DDEX::Album
  include XmlFormattable
  xml_format "ddex_version_38"
end
```

#### Defining the Class Macro

Defining a class macro is easy--it's really just a plain old class method. We'll define our `xml_format` macro in a sub-module, `XmlFormattable::ClassMethods` so that we can make it available as a class method. 

```ruby
module XmlFormattable
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def xml_format(name)
      # coming soon!
    end
  end
```

We have a class method `.xml_format`, that takes in an argument of the XML format name. We call this class method as a macro in each of the `::Album` sub-classes. 

Now that we have a macro that takes in an argument of the XML format, how will we store that format for each `::Album` class so that it can be retrieved by instances of that class later on?

One option that comes to mind for storing class-specific information is a class variable. Let's see what happens when we try to use class variables here. 

### (Spoiler) Class Variables Won't Work!

We'll set a class variable `@@xml_format` equal to a default value of `"ddex"` in our `XmlFormattable` module. *We'll give it this default value since DDEX is the industry standard for audio release XML.* 

```ruby
module XmlFormattable
 module ClassMethods
    @@xml_format = "ddex"
  end
end
```
Then, we'll use our `.xml_format` class method to assign the `@@xml_format` variable to the given XML format type.

```ruby
module XmlFormattable
 module ClassMethods
    @@xml_format = "ddex"

    def xml_format(format)
      @@xml_format = format
    end
  end
end
```

Lastly, we'll give our module a way to read the `@@xml_format` variable to expose it to the world.

```ruby
module XmlFormattable
 module ClassMethods
    @@xml_format = "ddex"

    def xml_format(format)
      @@xml_format = format
    end

    def format
      @@xml_format
    end
  end
end
```

Alright, let's see what happens when we define our `::Album` sub-classes. What will the `xml_format` macro set `@@xml_format` equal to for each sub-class?

```ruby
class Itunes::Album
  include XmlFormattable
  xml_format "itunes_version_9"
end

class Spotify::Album
  include XmlFormattable
  xml_format "spotify_version_7"
end

class DDEX::Album
  include XmlFormattable
  xml_format "ddex_version_38"
end

itunes_album = Itunes::Album.new
itunes_album.class.format
  => "ddex_version_38"

spotify_album = Spotify::Album.new
spotify_album.class.format
  => "ddex_version_38"

ddex_album = DDEX::Album.new
ddex_album.class.format
  => "ddex_version_38"f
```
What?? `.format` evaluates to `"ddex_version_38"` for instances of _all_ of our `::Album` classes!

This is because the `xml_format` class macro is updating the value of the `@@xml_format` class variable that is _shared by all of the classes_ that have our module mixed in. Remember that mixing a module into a class adds that module to the class's inheritance chain.

What we've done is bind our `@@xml_format` class variable to the `XmlFormattable` module, which is _shared_ by all three of our album classes. This means that each time we call our `xml_format` macro, we are _re-assigning_ that same shared class variable. 

Our class macro gets called when a class is defined, so when we defined the `Itunes::Album` class, `@@xml_format` got set to `"itunes_version_9`, and when we subsequently defined our `Spotify::Album` class we reset that same variable to `"spotify_version_7"`. Lastly, when we defined `DDEX::Album`, it overwrote that class variable one last time, setting it equal to `"ddex_version_38"`. 

If only there was a way to store a class attribute using shared code _and_ have the value of that attribute be specific to each class...

### Class Instance Variables to the Rescue!

Class instance variables will let us do exactly that.(You're so surprised, I know.) In Ruby, _any object_ can assign an instance variable. And what is a class but a plain old object?

> Classes in Ruby are first-class objects—each is an instance of class `Class` –– [Ruby Docs](https://ruby-doc.org/core-2.2.0/Class.html)

This means that a _class can have an instance variable too_, same as any other Ruby object. 

Unlike a class variable which is _shared_ by all of a class (or module)'s descendants, a class instance variable is specific to the given class. 

Let's replace our class variable, `@@xml_format` with a class instance variable to resolve our problem. 

```ruby
module XmlFormattable
 module ClassMethods
    @xml_format = "ddex"

    def xml_format(format)
      @xml_format = format
    end

    def format
      @xml_format
    end
  end
end
```
We can see that all we did here was give our `XmlFormattable` module an instance variable on the class level:

```ruby
XmlFormattable::ClassMethods.instance_variables
  => [:@xml_format]
```

Now, when we define our three album classes, the `@xml_format` instance variable gets set for each individual class. It doesn't overwrite a shared class variable. 

```ruby
class Itunes::Album
  include XmlFormattable
  xml_format "itunes_version_9"
end

class Spotify::Album
  include XmlFormattable
  xml_format "spotify_version_7"
end

class DDEX::Album
  include XmlFormattable
  xml_format "ddex_version_38"
end

itunes_album = Itunes::Album.new
itunes_album.class.format
  => "itunes_version_9"

spotify_album = Spotify::Album.new
spotify_album.class.format
  => "spotify_version_7"

ddex_album = DDEX::Album.new
ddex_album.class.format
  => "ddex_version_38"
```

And that's it!

## Conclusion
Class instance variables aren't magic. Using an instance variable on the class level is no different than using an instance variable within a specific instance of a class. This is because classes are objects too. 

By leveraging class instance variables, we were able to write a macro that defines a class-level and class-specific attribute for each of our `::Album` sub-classes. This allowed us to move the responsibility of defining and managing the `xml_format` attribute _out_ of our individual album classes and _into_ our `XmlFormattable` module, keeping our code DRY and adherent to SRP. 

Happy coding!