Monday, April 14, 2014

DRYer Ruby Class Definitions w/ Struct

Many web developers subscribe to a principle know as the DRY principle. It translates to Don't Repeat Yourself. I try my best to adhere to the DRY principle, but sometimes I repeat snippets of code here and there, especially if the footprint is small. Well, today, a coworker showed me a clever way to DRY up some of my "small footprint" repeated code. It's probably some age-old Ruby technique, but I just discovered it today, and I'm really excited about it! So, I'll share it.

A Myriad of Class Definitions

Let's say we need to implement a Pizza application for a friend. After much white-boarding, we finally decide on a code architecture and a series of fancy design patterns we're gonna use to make our Pizza site come to life! Inevitably, we are going to have several class definitions in our codebase:
class Pizza
  attr_accessor :cheese, :sauce, :toppings

  def initialize(cheese, sauce, toppings)
    @cheese = cheese
    @sauce = sauce
    @toppings = toppings
  end
  
  # ... more implementation
end
class Soda
  attr_accessor :type, :is_diet

  def initialize(type, is_diet)
    @type = type
    @is_diet = is_diet 
  end

  # ... yes, more implementation
end
class Topping
  attr_accessor :name, :cost

  def initialize(name, cost)
    @name = name
    @cost = cost
  end

  # ... you get it
end

The Boilerplate

It's pretty obvious there are repeated elements of code in each of our classes. It's small, but repetitive nonetheless! Any DRY refactors we can do to reduce the code-smell would be an improvement.

When I'm writing class definitions, I frequently find myself writing two elements:
  • getters/setters for public attributes
  • a constructor with parameterss for initial attribute values

The DRY way to get getters and setters is to use attr_accessor.
attr_accessor :cheese, :sauce, :toppings
To set initial attribute values upon construction we use an initialize method with parameters. This is basic Ruby function. Within the initialize method, we explicitly define which attribute gets which parameter.
def initialize(cheese, sauce, toppings)
  @cheese = cheese
  @sauce = sauce
  @toppings = toppings
end

Reduce the Repetition with Struct

We can get rid of BOTH of these snippets of code by inheriting from a Struct instance! Well, kind of. A Struct generates an instance of Class. The new Class instance will have predefined attributes, along with accessor methods for them. By inheriting from the generated class, we essentially get all of the goodies we want in a single line of code!

Our Pizza class from above now becomes:
class Pizza < Struct.new(:cheese, :sauce, :toppings)  
  # no more initialize, just our implementation!
end
Subsequently, our entire codebase (however tiny) becomes:
class Pizza < Struct.new(:cheese, :sauce, :toppings)  
end
class Soda < Struct.new(:type, :is_diet)
end
class Topping < Struct.new(:name, :cost)
end

Mind. Blown. Anyway, I hope this helps someone as much as it has helped me. Comments or questions? Let me know!

No comments:

Post a Comment