Caffeine-Powered Life

Ruby Mixins and SRP

Hadi Hariri put together a good post on the Single Responsibility Principle (SRP) earlier this week. Honestly, it’s the one thing that I kinda fret about when working with Ruby on Rails. The ActiveModel pattern. Let’s consider a mundane domain model in Rails.


class Product < ActiveRecord::Base  

  belongs_to :category

  has_many :order_lines

  

  validates_presence_of :name

  validates_presence_of :price

  validates_numericality_of :price, :greater_than_or_equal_to => 0.00

  

  def deactivate    

    raise "Cannot deactivate a product with open orders" if open_orders?

    self.active = false

  end

  

  def open_orders?

    order_lines.where(:status => "open").any?

  end

  

end

We’re working with the Active Record design pattern in Rails. Just check out the enormous capabilities of this class. At a bare minimum, our Product class the following abilities.

  1. It knows all of our properties about a Product.
  2. It has save, create, delete, destroy, and a whole mess of dynamic find capabilities.
  3. It contains all of the Active Record associations that you’ve defined.
  4. It has all of the ability to validate a Product. You can even write your own custom validators.

This works in Ruby. No really, it does. And it works in a way that just fails in C#. It works because Ruby is a dynamic language that supports mixins. Ruby technically only allows for single-class inheritance, but you can also grant the methods from any other class by adding modules. The validators in ActiveRecord are a perfect example of an included module.

Ruby lets you go overboard. Or, as I like to say, “I can program badly in any language.”


validates :validate_shipping_price

  

def heavy_product?

  product.weight > 30

end  



def validate_shipping_price

  if heavy_product?

    # a bunch of validation steps here to set a shipping price...    

  end

end  

How big of a class are we talking about? Shipping stuff is expensive, and we don’t want to get this one wrong. Perhaps you should write a custom validator class. It’s not very difficult to do, there are plenty of examples online, and it puts the code in one place.


class ShippingPriceValidator < ActiveModel::EachValidator

    

  def validate_each(record, attribute, value)

    @record = record

    if value && heavy? && value < min_shipping_price

      record.errors[attribute] << "must have a minimum of #{min_shipping_price}"

    end

  end

      

  def heavy?

    @record.weight && @record.weight > 30

  end

      

  def min_shipping_price    

    @record.weight * 0.20

  end

      

end

We’ve now isolated the code to perform a validation on one of our models. Since our validator inherits from Active Model’s validation library, it’s automatically included in our Product class. We don’t need to do anything else. If we make a change to how product shipping minimums are calculates, it’s right here, not in the Product class itself.

Comments