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
Popular Ruby AOP Gems
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
- Keep Aspects Simple: Don’t overcomplicate aspects with too much logic
- Use Descriptive Names: Name aspects by their purpose (LoggingAspect, CachingAspect)
- Avoid Monkey Patching: Use proper module inclusion
- Consider Performance: Aspects add overhead; use judiciously
- 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