Creating Reusable code in Rails: Plugins, Metaprogramming, and Best Practices

March 25, 2025

Rails developers often face the challenge of writing clean, reusable, and maintainable code. Achieving this requires leveraging plugins, metaprogramming, and Ruby’s dynamic nature. In this article, we’ll explore practical ways to implement these techniques, ensuring your Rails code is DRY (Don’t Repeat Yourself) and efficient.


💡 Looking to improve your application’s code quality or build a scalable new project?

Whether you need cleaner, more efficient, and maintainable code or want to create a robust and scalable application from scratch, I’m here to help. Let’s take your project to the next level!

📩 Get in Touch

1. Extracting Functionality into Plugins

One effective way to keep your Rails application clean is by extracting reusable functionality into plugins. A plugin is a self-contained package of code that can be shared across multiple applications or models. Let’s walk through the creation of a plugin using a Drivable example.

Step 1: Generating the Plugin

Rails provides generators to scaffold a plugin’s structure. Running the following command creates the necessary files:

./script/rails generate plugin drivable 

This will generate a directory structure like this:

vendor/plugins/drivable/
├── init.rb
├── lib/
│   ├── drivable.rb
│   └── drivable/active_record_extensions.rb
├── Rakefile
├── README
└── test/
    ├── drivable_test.rb
    └── test_helper.rb 
  • init.rb: Initializes the plugin.
  • lib/drivable.rb: Hooks the plugin into Rails.
  • lib/drivable/active_record_extensions.rb: Contains the actual logic for extending ActiveRecord.

Step 2: Defining the Behavior

We define behavior in lib/drivable/active_record_extensions.rb:

module Drivable
  module ActiveRecordExtensions
    module ClassMethods
      def drivable
        validates_presence_of :direction, :speed
        include Drivable::ActiveRecordExtensions::InstanceMethods
      end
    end

    module InstanceMethods
      def turn(new_direction)
        self.direction = new_direction
      end

      def brake
        self.speed = 0
      end

      def accelerate
        self.speed = [speed + 10, 100].min
      end
    end
  end
end
  • ClassMethods: Adds a drivable method to models.
  • InstanceMethods: Defines turn, brake, and accelerate behaviors.

Step 3: Hooking into ActiveRecord

To integrate our plugin into ActiveRecord:

require "drivable/active_record_extensions"

class ActiveRecord::Base
  extend Drivable::ActiveRecordExtensions::ClassMethods
end

Finally, we bootstrap the plugin in init.rb:

require File.join(File.dirname(__FILE__), "lib", "drivable")

Now, any model can use drivable behavior:

class Car < ActiveRecord::Base
  drivable
end

By using plugins, we ensure modular, reusable, and easy-to-maintain code.


2. Using Metaprogramming to Write DRY Code

Metaprogramming helps reduce duplication and create flexible, reusable code.

Problem: Repetitive Status Methods

Consider a Purchase model with multiple statuses:

class Purchase < ActiveRecord::Base
  def self.all_in_progress
    where(status: "in_progress")
  end

  def self.all_submitted
    where(status: "submitted")
  end

  def in_progress?
    status == "in_progress"
  end

  def submitted?
    status == "submitted"
  end
end

Adding a new status requires updating multiple methods, leading to duplication.

Solution: Metaprogramming

We can dynamically generate these methods:

class Purchase < ActiveRecord::Base
  STATUSES = %w(in_progress submitted approved shipped received)

  validates :status, presence: true, inclusion: { in: STATUSES }

  class << self
    STATUSES.each do |status_name|
      define_method "all_#{status_name}" do
        where(status: status_name)
      end
    end
  end

  STATUSES.each do |status_name|
    define_method "#{status_name}?" do
      status == status_name
    end
  end
end

This:

  • Centralizes statuses in STATUSES.
  • Dynamically generates finder and accessor methods, reducing duplication.

Improving Readability with Macros

To further improve maintainability, we can extract this logic into a macro:

class ActiveRecord::Base
  def self.has_statuses(*status_names)
    validates :status, presence: true, inclusion: { in: status_names }

    status_names.each do |status_name|
      scope "all_#{status_name}", -> { where(status: status_name) }
    end

    status_names.each do |status_name|
      define_method "#{status_name}?" do
        status == status_name
      end
    end
  end
end

Now, the Purchase model is simplified:

class Purchase < ActiveRecord::Base
  has_statuses :in_progress, :submitted, :approved, :shipped, :received
end

This abstraction improves readability and makes the feature reusable across models.


3. Benefits of DRY Code

Writing DRY code offers several advantages:

  • Maintainability: Changes are localized.
  • Readability: Common patterns are abstracted away.
  • Reusability: Code can be shared across projects.

For example, to add a virtual status not_shipped, we can now do:

class Purchase < ActiveRecord::Base
  has_statuses :in_progress, :submitted, :approved, :partially_shipped, :fully_shipped

  scope :all_not_shipped, -> { where.not(status: ["partially_shipped", "fully_shipped"]) }

  def not_shipped?
    !(partially_shipped? || fully_shipped?)
  end
end

Exceptional cases like not_shipped now stand out clearly.


4. Plugins vs. Gems

While plugins are useful for internal reuse, gems are better suited for distributing code across teams or the open-source community. Gems offer features like versioning and compatibility with non-Rails projects.

For example, the drivable plugin could be converted into a gem for broader use.


Conclusion

By leveraging plugins, metaprogramming, and DRY principles, you can write cleaner, more maintainable Rails applications. These techniques reduce duplication while improving readability and reusability. Whether you’re building an internal tool or an open-source library, these practices will help you create robust and scalable solutions.

Final Thought: Don’t fear metaprogramming—it’s a powerful tool that, when used wisely, can elevate your Ruby and Rails development skills to the next level.

Leave a comment