Sonntag, 20. April 2014

Share your Ruby logic with modules!

One of the widespread design patterns is composition. The Gang of Four already defined: "Favor composition over inheritance". Ruby offers a great way to achieve composition by modules.
The original class from the "Soft delete your ActiveRecord" example:
class Person < ActiveRecord::Base
  attr_accessible :name
  validates :active, :inclusion => { :in => [true, false] }
  before_validation :activate, 
    :on => :create,
    :if => Proc.new {|r| r.active.nil? }

  def self.active
    where :active => true
  end 

  def activate
    self.active = true
    self
  end 

  def deactivate
    self.active = false
    self
  end 
end
can be refactored to include the soft deletion feature by composition. Starting with the module structure in the new created file lib/activerecord_extentions.rb:
module ActiveRecordExtensions
  module Deactivateable
    extend ActiveSupport::Concern
    self.included do
    end   
  end 
end
The module Deactivateable was scoped by ActiveRecordExtensions to be more clear about its intention.
Using the Ruby on Rails ActiveSupport::Concern module helps to handle module dependencies gracefully.
The self.included block contains all the stuff, that has to be done immediately when the module was included. Until now it contains not a bit logic, but can already be included:
class Person < ActiveRecord::Base
  include ActiveRecordExtensions::Deactivateable
  attr_accessible :name
  validates :active, :inclusion => { :in => [true, false] }
  before_validation :activate, 
    :on => :create,
    :if => Proc.new {|r| r.active.nil? }

  def self.active
    where :active => true
  end 

  def activate
    self.active = true
    self
  end 

  def deactivate
    self.active = false
    self
  end 
end
Extracting the instance methods and the putting them into the module:
module ActiveRecordExtensions
  module Deactivateable
    extend ActiveSupport::Concern
    self.included do
    end

    def activate
      self.active = true
      self
    end 

    def deactivate
      self.active = false
      self
    end 
  end 
end
The second step is to move the validator and the default setter on creation into the self.included block:
module ActiveRecordExtensions
  module Deactivateable
    extend ActiveSupport::Concern
    self.included do
      validates :active, :inclusion => { :in => [true, false] }
      before_validation :activate, 
        :on => :create,
        :if => Proc.new {|r| r.active.nil? }
    end

    def activate
      self.active = true
      self
    end 

    def deactivate
      self.active = false
      self
    end 
  end 
end
Both the validator and the callback are processed right at including time on the target.
At least the class method is moved into the class scope of the prospectively enriched class:
module ActiveRecordExtensions
  module Deactivateable
    extend ActiveSupport::Concern
    self.included do
      validates :active, :inclusion => { :in => [true, false] }
      before_validation :activate, 
        :on => :create,
        :if => Proc.new {|r| r.active.nil? }
    end

    def activate
      self.active = true
      self
    end 

    def deactivate
      self.active = false
      self
    end 

    module ClassMethods
      def active
        where :active => true  
      end
    end
  end 
end
Finally, let's take a look at the target class Person after moving the soft delete feature into the module:
class Person < ActiveRecord::Base
  include ActiveRecordExtensions::Deactivateable
  attr_accessible :name
end
and benefit from it by sharing it with other classes:
class Task < ActiveRecord::Base
  include ActiveRecordExtensions::Deactivateable
end
The benefits are:
  1. Modularized features (means concise code blocks)
  2. Maintainable code
  3. Avoids the downsides of inheritance

Supported by Ruby 2.1.1 and Ruby on Rails 3.2.17