Skip to main content
โšก Calmops

AOP Programming in Ruby

Overview

Aspect-Oriented Programming (AOP) is a programming paradigm that separates cross-cutting concerns from business logic. In Ruby, AOP is often implemented through metaprogramming techniques.

What Are Cross-Cutting Concerns?

Cross-cutting concerns are functionalities that affect multiple parts of your application:

  • Logging: Recording method calls and results
  • Authentication: Checking user permissions
  • Caching: Storing and retrieving cached data
  • Error Handling: Catching and processing exceptions
  • Performance Timing: Measuring execution time

AOP vs Traditional OOP

Without AOP

class UserService
  def create_user(params)
    logger.info("Creating user: #{params[:email]}")
    
    begin
      start_time = Time.now
      user = User.create(params)
      logger.info("User created in #{Time.now - start_time}s")
      user
    rescue => e
      logger.error("Failed to create user: #{e.message}")
      raise
    end
  end
end

With AOP

class UserService
  def create_user(params)
    User.create(params)
  end
end

# Aspect: Logging
class LoggingAspect
  def self.before(method_name, *args)
    logger.info("Calling #{method_name}")
  end
  
  def self.after(method_name, result)
    logger.info("Completed #{method_name}")
  end
end

Ruby AOP Implementation

Using Method Aliasing

class UserService
  def create_user(params)
    User.create(params)
  end
end

# Wrap method with logging
class UserService
  alias_method :original_create_user, :create_user
  
  def create_user(params)
    puts "Before creating user"
    result = original_create_user(params)
    puts "After creating user"
    result
  end
end

Using Modules and include

module TimingAspect
  def timing
    start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    result = yield
    elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
    puts "Execution time: #{elapsed.round(3)}s"
    result
  end
end

class PaymentProcessor
  include TimingAspect
  
  def process_payment(amount)
    timing do
      # Payment processing logic
      sleep(rand(0.1..0.5))
      { status: :success, amount: amount }
    end
  end
end

Using ActiveSupport::Notifications

class PaymentProcessor
  def process_payment(amount)
    ActiveSupport::Notifications.instrument("payment.process", amount: amount) do
      # Payment logic
    end
  end
end

# Subscribe to events
ActiveSupport::Notifications.subscribe("payment.process") do |*args|
  event = ActiveSupport::Notifications::Event.new(*args)
  puts "Payment of #{event.payload[:amount]} processed"
end

Rails

  • activesupport-notifications: Event-based AOP
  • around_message: Method wrapping

Pure Ruby

  • aspector: Aspect-oriented programming framework
  • aquarium: AOP framework for Ruby
  • ruby-aop: Simple AOP implementation

Use Cases

1. Method Logging

module LogAspect
  def self.included(base)
    base.extend(ClassMethods)
  end
  
  module ClassMethods
    def log_methods(*method_names)
      method_names.each do |method_name|
        alias_method "original_#{method_name}", method_name
        
        define_method(method_name) do |*args, &block|
          puts "Calling #{method_name}"
          result = send("original_#{method_name}", *args, &block)
          puts "Completed #{method_name}"
          result
        end
      end
    end
  end
end

2. Caching

module CacheAspect
  def cache(key, expires_in: 1.hour)
    Rails.cache.fetch(key, expires_in: expires_in) do
      yield
    end
  end
end

class DataService
  include CacheAspect
  
  def fetch_users
    cache("users_list", expires_in: 30.minutes) do
      User.all.to_a
    end
  end
end

3. Retry Logic

module RetryAspect
  def retry_on_failure(times: 3, delay: 1)
    attempt = 0
    begin
      yield
    rescue => e
      attempt += 1
      if attempt < times
        sleep(delay * attempt)
        retry
      else
        raise e
      end
    end
  end
end

class ExternalAPI
  include RetryAspect
  
  def fetch_data
    retry_on_failure(times: 3, delay: 2) do
      HTTParty.get(api_url)
    end
  end
end

Best Practices

  1. Keep Aspects Simple: Don’t overcomplicate aspects with too much logic
  2. Use Descriptive Names: Name aspects by their purpose (LoggingAspect, CachingAspect)
  3. Avoid Monkey Patching: Use proper module inclusion
  4. Consider Performance: Aspects add overhead; use judiciously
  5. Test Thoroughly: Aspects can be hard to debug; comprehensive tests are essential

Conclusion

AOP in Ruby provides powerful tools for separating cross-cutting concerns from business logic. By using modules, method aliasing, and event-based patterns, you can create clean, maintainable code that is easier to refactor and test.

Comments