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.
- It knows all of our properties about a Product.
- It has
save,create,delete,destroy, and a whole mess of dynamicfindcapabilities. - It contains all of the Active Record associations that you’ve defined.
- 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.