When diving deep into Ruby, understanding how to manage object access and behavior is crucial. There are two standout design patterns that come to the rescue: the Proxy pattern and Delegation. These patterns let you control object interactions by adding layers of functionality without tweaking the original objects.
The Proxy Pattern Unraveled
The Proxy pattern is a key player in structural design. Think of it as a stand-in (or substitute) for a real service object. When a client makes a request, the proxy steps in, does some pre or post-processing (like logging, caching, or access control), and then forwards the request to the actual service object. The clever part? The proxy matches the interface of the service, making it completely interchangeable.
Why Bother with Proxies?
Proxies become a game-changer when you need to add behaviors to an object without touching the client code. Imagine needing access control where only authorized users can call specific methods. Or perhaps you want to cache results from resource-heavy method calls and boost performance.
Different Flavors of Proxies
- Protection Proxy: Shields the object from unauthorized access by verifying user permissions before passing requests.
- Remote Proxy: Facilitates access to objects on remote machines, handling all the networking complexities behind the scenes.
- Virtual Proxy: Delays object creation until absolutely necessary, which is handy when the creation is resource-intensive.
Crafting a Proxy in Ruby
Picture a scenario where you want to keep a log of all interactions with a BankAccount
object. Instead of altering BankAccount
, create a BankAccountLogger
class to act as its proxy.
class BankAccount
def initialize(amount)
@amount = amount
end
def deposit(amount)
@amount += amount
end
def withdraw(amount)
@amount -= amount if @amount >= amount
end
def balance
@amount
end
end
class BankAccountLogger
def initialize(account)
@account = account
end
def method_missing(name, *args, &block)
puts "Calling method #{name} with arguments #{args}"
result = @account.send(name, *args, &block)
puts "Method #{name} returned #{result}"
result
end
end
# Usage
account = BankAccount.new(100)
logged_account = BankAccountLogger.new(account)
logged_account.deposit(50)
logged_account.withdraw(20)
puts logged_account.balance
Here, BankAccountLogger
logs method calls and then forwards them to the BankAccount
object. Simple and effective!
The Charm of Delegation
Delegation is all about an object passing off some of its responsibilities to another object, quite different from inheritance where a class derives behavior from a parent class. This method bolsters flexibility and modularity.
Why Lean on Delegation?
Delegation helps when you want to keep objects loosely coupled. Changing an object’s behavior becomes effortless without disturbing other parts of the system. A classic example? Offloading logging duties to a separate logger object, allowing easy swapping of logging mechanisms.
Delegation in Ruby: The How-to
Ruby offers a couple of slick ways to pull off delegation: using the method_missing
method or the forwardable
module.
Employing method_missing
The method_missing
gem in Ruby catches and handles unimplemented method calls. Here’s a demonstration:
class Logger
def log(message)
puts message
end
end
class BankAccount
def initialize(amount, logger)
@amount = amount
@logger = logger
end
def method_missing(name, *args, &block)
if @logger.respond_to?(name)
@logger.send(name, *args, &block)
else
super
end
end
def deposit(amount)
@amount += amount
end
def withdraw(amount)
@amount -= amount if @amount >= amount
end
def balance
@amount
end
end
# Usage
logger = Logger.new
account = BankAccount.new(100, logger)
account.log("Transaction started")
account.deposit(50)
account.log("Transaction completed")
Here, BankAccount
uses method_missing
to delegate logging to the Logger
object.
Leveraging forwardable
Ruby’s forwardable
module simplifies delegation. Here’s how:
require 'forwardable'
class Logger
def log(message)
puts message
end
end
class BankAccount
extend Forwardable
def initialize(amount, logger)
@amount = amount
@logger = logger
end
def_delegators :@logger, :log
def deposit(amount)
@amount += amount
end
def withdraw(amount)
@amount -= amount if @amount >= amount
end
def balance
@amount
end
end
# Usage
logger = Logger.new
account = BankAccount.new(100, logger)
account.log("Transaction started")
account.deposit(50)
account.log("Transaction completed")
Here, def_delegators
in BankAccount
delegates logging to the Logger
object.
Going Beyond: Advanced Cases
Dynamic Delegation at Play
Ruby’s dynamic capabilities allow adding or removing delegations on the fly, useful for changing an object’s behavior based on conditions.
class DynamicBankAccount
def initialize(amount)
@amount = amount
end
def method_missing(name, *args, &block)
if @delegate && @delegate.respond_to?(name)
@delegate.send(name, *args, &block)
else
super
end
end
def set_delegate(delegate)
@delegate = delegate
end
def deposit(amount)
@amount += amount
end
def withdraw(amount)
@amount -= amount if @amount >= amount
end
def balance
@amount
end
end
# Usage
account = DynamicBankAccount.new(100)
logger = Logger.new
account.set_delegate(logger)
account.log("Transaction started")
account.deposit(50)
account.log("Transaction completed")
Here, DynamicBankAccount
can dynamically assign a delegate, demonstrating the power of flexibility.
Using BasicObject
for Proxies
Ruby’s BasicObject
, introduced from version 1.9, aids in crafting proxies efficiently. It’s a stripped-down version of Object
with minimal methods, reducing conflicts.
class AccountLogger < BasicObject
def initialize(account)
@account = account
end
def method_missing(name, *args, &block)
puts "Calling method #{name} on #{@account.class}"
result = @account.send(name, *args, &block)
puts "Method #{name} returned #{result}"
result
end
end
# Usage
account = BankAccount.new(100)
logged_account = AccountLogger.new(account)
logged_account.deposit(50)
logged_account.withdraw(20)
puts logged_account.balance
In this snippet, AccountLogger
uses BasicObject
to keep method conflicts at bay.
Wrapping Up
Understanding and implementing the Proxy and Delegation patterns in Ruby brings tremendous power to your design toolkit. These patterns enable adding functionality layers without tweaking original objects. Whether it’s managing cache, logging, or controlling access, utilizing these patterns makes your code more modular, maintainable, and efficient. Embrace these flexible solutions to tackle common software design challenges seamlessly.