viphat / til

Today I Learned
http://notes.viphat.work
0 stars 1 forks source link

[Design Pattern in Ruby] - Chapter 10. Proxy Pattern #243

Open viphat opened 6 years ago

viphat commented 6 years ago

The intent of the Proxy Pattern is to:

Provide a surrogate or placeholder for another object to control access to it.

Proxies as defined by the Proxy Pattern, are deceptively simple, very useful, and highly flexible. The central premise of the Proxy Pattern is that whenever you need to control the access to an object, a Proxy object will do the trick. Sitting between a caller (a Client) and a receiver (the Subject), the Proxy class of the Proxy Pattern can provide protection, hide complexity, or delay expensive actions.

Protection proxies: Sometimes, in the interest of security, it is prudent to place a proxy object in front of an object in order to add additional security. Such proxies are known as protection proxies.

Virtual proxies: Other times, it is wise to delay the creation of expensive objects until they are absolutely needed. Especially in cases where such instantiation may not needed at all. A great example of this type of proxy is the ActiveRecord::Associations::CollectionProxy in Rails. Such proxies are known as virtual proxies.

Remote proxies - Occasionally, it is necessary to locally represent an object that exists on a remote system, hiding the complexity of traversing the network from the local client. Such proxies are known as remote proxies.

#################################################################################
#################################################################################
# The 'Subject' for all of the proxy examples
#################################################################################
#################################################################################

# The 'BankAccount' class will be the 'Subject'
# of this particular demonstration of the Proxy
# pattern. It will be proxied in each of the
# following examples.
class BankAccount
  def balance
    # pretend like this method has been fully implemented.
    puts "check balance"
  end

  def deposit(amount)
    # pretend like this method has been fully implemented.
    puts "deposit #{amount}"
  end

  def withdraw(amount)
    # pretend like this method has been fully implemented.
    puts "withdraw #{amount}"
  end
end

#################################################################################
#################################################################################
# 'Remote Proxy' Example
#################################################################################
#################################################################################

# The 'RemoteBankAccountProxy' class will act as
# a local representative of a remote
# 'BankAccount' object. It will hide and
# manage the querying and securing of
# information across the network to a
# remote server.
#
# From the perspective of the local client,
# the 'RemoteBankAccountProxy' is identical to the
# 'BankAccount' object on the remote server.
class RemoteBankAccountProxy
  # Here, we are setting a base uri for the
  # 'BankAccount' object on the remote server.
  # Of course, in real life we would incorporate
  # security and grapple with potential network
  # problems. We would also pass in some kind
  # of UID, in order to specify a specific
  # bank account.
  def initialize
    @base_uri = "localhost:3000/bank_account"
  end

  # The 'balance' method returns the
  # account balance from the remote account.
  def balance
    rest_service.get("/balance")
  end

  # The 'deposit' method posts the
  # deposited money to the remote account.
  def deposit(amount)
    rest_service.post("/deposit", {amount: amount})
  end

  # The 'withdraw' method withdraws
  # money from the remote account.
  def withdraw(amount)
    rest_service.delete("/withdraw", {amount: amount})
  end

  #######
  private
  #######

  # The 'rest_service' method is responsible for
  # lazily creating and returning a REST client
  # which can be used to communicate with the
  # remote server. I've used the class name
  # 'RestClient' as an example, but I haven't
  # included a real library here.
  def rest_service
    # We've opted to send and receive data in a JSON
    # format.
    @rest_client ||= RestClient.new(base_uri, :json)
  end

  # The rest_service attribute is private, as we
  # don't want the rest_service to be accessible
  # outside of this remote proxy.
  attr_reader :rest_service
end

#################################################################################
#################################################################################
# 'Virtual Proxy' Example
#################################################################################
#################################################################################

class VirtualBankAccountProxy

  def balance
    # When 'balance' is called on this proxy,
    # the 'subject is lazily instantiated
    # on demand. In other words, if the
    # bank account object has not yet been
    # instantiated, it will now be instantiated
    # and set.
    subject.balance
  end

  def deposit(amount)
    # When 'deposit' is called, it either invokes
    # the bank account, or instantiates it, then
    # calls 'deposit' on the bank account. This
    # ensures that the BankAccount object is not
    # instantiated until it actually needs to
    # be used.
    subject.deposit(amount)
  end

  def withdraw(amount)
    # ditto the above two methods.
    subject.withdraw(amount)
  end

  #######
  private
  #######

  def subject
    # Key to the virtual proxy pattern:
    # the lazy initialization of the proxied
    # 'Subject'.
    @subject ||= BankAccount.new
  end
end

#################################################################################
#################################################################################
# 'Protection Proxy' Example
#################################################################################
#################################################################################

# The 'ProtectionBankAccountProxy' class, being
# a 'protection proxy', is responsible for
# protecting the 'BankAccount' subject object
# from unwanted access. It acts as a security
# buffer to the 'BankAccount' object, allowing
# that object to concern itself with the behavior
# and responsibilities of its own domain, and not
# with security concerns.
class ProtectionBankAccountProxy
  attr_reader :user_credentials

  # 'user_credentials' containing the security level
  # of the given user are passed in.
  def initialize(user_credentials)
    @subject = BankAccount.new
    @user_credentials = user_credentials
  end

  # When 'balance' is called, security permissions
  # are first checked. If the user in question has
  # an appropriate level of security, then the
  # subject of this proxy will be called. Otherwise,
  # an error will be raised.
  #
  # In this case, we are specifically checking for
  # 'read' permissions.
  def balance
    check_permissions(:read)
    subject.balance
  end

  # Ditto the above method.
  #
  # It's possible that a user who is authorized to
  # check the balance of an account might not be
  # authorized to deposit money to the account.
  #
  # In this case, we are specifically checking for
  # 'write' permissions.
  def deposit(amount)
    check_permissions(:write)
    subject.deposit(amount)
  end

  # Ditto the above methods.
  def withdraw(amount)
    check_permissions(:write)
    subject.withdraw(amount)
  end

  #######
  private
  #######

  def check_permissions(permission_type)
    # We are calling a 'CredentialValidator' (undefined in this example code)
    # in order to verify whether or not a user has the requisite security
    # credentials to perform a certain action.
    #
    # In this example, a user might have 'read' permissions but not
    # 'write' permissions on the account.
    unless CredentialValidator.validate(@user_credentials, permission_type)
      # If the user does not have the proper credentials, an error is raised.
      raise "Unauthorized #{permission_type} action from: #{@user_credentials}. Account action denied."
    end
  end
end

Using method_missing for proxied methods

In Ruby, it can sometimes be quite helpful to leverage the method_missing method to forward all methods not defined on the proxy to the subject being proxied. In cases where methods are treated uniformly across the proxy interface, doing so can save quite a great deal of time and effort. It can also protect the proxy from future additions to the proxied subject.

Proxies vs Decorators

You may have noticed that a proxy sometimes uses the same implementation pattern as a decorator. In such cases, the difference between a decorator and a proxy lies in the intent. A decorator is intent on adding additional responsibilities to an object, whereas a proxy controls access to an object.

Proxies vs Adapter

An adapter provides a different interface to the object that it adapts, whereas a proxy provides either the same interface as its subject, or a subset of the interface of its subject.

Source

viphat commented 6 years ago

The intent of the proxy-pattern is to provide a placeholder for another object to control access to it. It introduces an additional level of indirection. There are several reasons why you would want to do this, hence there are several uses for this pattern.

One reason for controlling access to an object is to defer the full cost of its creation and initialization until we actually need to use it. Consider a document editor that can embed graphical objects in a document. It isn't necessary to load all pictures when the document is opened, because not all of these objects will be visible at the same time

So we could think about loading each picture on demand, which occurs when an image becomes visible in the document. But what do we put in the document instead of the image? And how can we hide that the pictures are created on demand?

The solution is to use another object, a proxy, in place of an image. The proxy acts like an image and loads the picture when it's required.

The proxy loads and creates the real image just when the document editor requests for showing that image by invoking its draw-method. The proxy then loads the image and forwards the request to it.

This was one usage of a proxy, but there are many more. Some have their own names, depending on their responsibility:

viphat commented 6 years ago

Superficially, the proxy is very similar to the adapter: One object stands in for another. But the proxy does not change the interface; the interface of the proxy is exactly the same as the interface of its subject. Instead of trying to transform the interface of that inner object in the same way that an adapter does, the proxy tries to control access to it.