fast_forwardProxy

Description

The Proxy design pattern is a structural pattern that involves the use of a surrogate or placeholder object to control access to another object. It acts as an intermediary, allowing or restricting access to the real object.

Overview

In today’s example, we will build an application that will allow different users to manage one bank account. First, let’s build our class, which will create our account with the initial balance and allow us to:
  • check account balance
  • deposit money
  • withdraw money
So let’s create Account class.

class Account
  def initialize
    @balance = 20_000
  end

  def balance(user:)
    @balance
  end
  
  def deposit(amount:, user:)
    @balance += amount
  end
  
  def withdraw(amount:, user:)
    @balance -= amount
  end
end
Let’s also create a User class that will store only a user name.

class User
  attr_reader :name
  
  def initialize(name)
    @name = name
  end
end
With the code prepared in this way, each User will be able to easily withdraw and deposit money into the account.

account = Account.new
john = User.new('John Doe')
jane = User.new('Jane Doe')

account.balance(user: john)                # => 20000
account.deposit(amount: 1350, user: john)  # => 21350
account.withdraw(amount: 2000, user: jane) # => 19350
To use the Proxy pattern, we need to create a new class that will have the same interface as the base class, but to make it working we will pass our base class to its constructor.

class ProxyAccount
  attr_reader :account

  def initialize(account)
    @account = account
  end

  def balance(user:)
    account.balance(user:)
  end
  
  def deposit(amount:, user:)
    account.deposit(amount:, user:)
  end
  
  def withdraw(amount:, user:)
    account.withdraw(amount:, user:)
  end
end
With the class prepared in this way, we can add a new method that will add some information and place it before each operation.

class ProxyAccount
  attr_reader :account

  def initialize(account)
    @account = account
  end

  def balance(user:)
    log(user, "Check account balance")

    account.balance(user:)
  end
  
  def deposit(amount:, user:)
    log(user, "send #{amount}$ to account")

    account.deposit(amount:, user:)
  end
  
  def withdraw(amount:, user:)
    log(user, "Take #{amount}$ from account")

    account.withdraw(amount:, user:)
  end

  private
  
  # Display time of the action and who did it
  def log(user, text)
    puts "#{Time.now} | #{user.name} | #{text}"
  end
end
Thanks to the fact that our proxy class has the same interface as the base class, we can use it interchangeably and receive logs before starting each operation.

proxy_account = ProxyAccount.new(Account.new)
proxy_account.balance(user: john)
# 2024-01-25 18:55:06 +0100 | John Doe | Check account balance
# => 20000

proxy_account.withdraw(amount: 2000, user: jane)
# 2024-01-25 18:55:43 +0100 | Jane Doe | Take 2000$ from account
# => 18000
Another very popular adoption of this pattern is access control. Therefore, let’s add a method that will raise an error when the withdrawal limit exceeds 5000 $.

class ProxyAccount
  attr_reader :account

  def initialize(account)
    @account = account
  end

  def balance(user:)
    log(user, "Check account balance")

    account.balance(user:)
  end
  
  def deposit(amount:, user:)
    log(user, "send #{amount}$ to account")

    account.deposit(amount:, user:)
  end
  
  def withdraw(amount:, user:)
    log(user, "Take #{amount}$ from account")
    check_limit(amount)

    account.withdraw(amount:, user:)
  end

  private
  
  # Display time of the action and who did it
  def log(user, text)
    puts "#{Time.now} | #{user.name} | #{text}"
  end

  def check_limit(amount)
    return if amount < 5000

    raise("Blocked! Transaction value too high.")
  end
end
Now let’s use the new implementation to check whether it works properly.

proxy_account = ProxyAccount.new(Account.new)
proxy_account.withdraw(amount: 7500, user: jane) # =>
# Blocked! Transaction value too high. (RuntimeError)
In summary, the Proxy Pattern is useful in situations where you need to add an additional layer of control, functionality, or optimization to the access of an object, without directly modifying the object’s code.