Proxy Pattern
- 10 November 2017
- Patterns Ruby
- Proxy Pattern
In this article, we will cover Proxy pattern and its types. We will implement each of them using Ruby.
Intent
Let's begin with the intent of this pattern. In "Design Patterns: Elements of Reusable Object-Oriented Software" book they describe the intent of Proxy as:
Provide a surrogate or placeholder for another object to control access to it
This pattern is also known as Surrogate.
I like the example they provide in that book:
Consider a document editor that can embed graphical objects in a document. Some graphical objects, like large raster images, can be expensive to create. But opening a document should be fast, so we should avoid creating all the expensive objects at once when the document is opened. [...] The solution is to use another object, like image proxy, that acts as a stand-in for the real image. The proxy acts just like the image and takes care of instantiating it when required.
Dependencies would look like this:
TextDocument
would use ImageProxy
to show some placeholder initially, and ImageProxy
would load actual Image
when required.
Applicability
Proxy is useful whenever there is a need for a more sophisticated reference to an object. It's applicable as:
- Virtual proxy
- Protection proxy
- Remote proxy
- Smart reference
Virtual Proxy
Creates expensive objects on demand
Example with ImageProxy
described above is exactly this type of proxy.
Let's implement it with Ruby.
We have TextDocument
class with 2 methods:
class TextDocument
attr_accessor :elements
def load
elements.each { |el| el.load }
end
def render
elements.each { |el| el.render }
end
end
It has just two methods: load
and render
. We assume that we want to load all elements for the document and then we want to render them.
Let's say that we have Image
element that takes too long to load:
class Image
def load
# ... takes too much time
end
def render
# render loaded image
end
end
So if we want to load document with image:
document = TextDocument.new
document.elements.push(Image.new)
document.load # => takes too much time because of image
It will take too long because of the load time of the image.
We can create a virtual proxy for an image that would implement lazy loading:
class LazyLoadImage
attr_reader :image
def initialize(image)
@image = image
end
def load
end
def render
image.load
image.render
end
end
Now if we use LazyLoadImage
proxy, it will not hold document loading. It happens because LazyLoadImage
doesn't load image until render
call.
document = TextDocument.new
image = Image.new
document.elements.push(LazyLoadImage.new(image))
document.load # => fast
document.render # => slow because image is being loaded
We could use SimpleDelegator to implement proper delegation from LazyLoadImage
to Image
as we did for Decorator Pattern.
Protection Proxy
A protection Proxy controls access to the original object
This one is pretty obvious, if you want to apply some protection rules before calling the original object, you can wrap it in a Protection Proxy
class Folder
def self.create(name)
# creating folder
end
def self.delete(name)
# deleting folder
end
end
class FolderProxy
def self.create(user, folder_name)
raise 'Only Admin Can Create Folders' unless user.admin?
Folder.create(folder_name)
end
def self.delete(user, folder_name)
raise 'Only Admin Can Delete Folders' unless user.admin?
Folder.delete(folder_name)
end
end
I must admit that in this example we have a different interface between Proxy and original class. Folder
accepts just one param for create
and delete
, whereas FolderProxy
accepts user
as well. I'm not sure if it's the best implementation of this type of proxy. Let me know in comments if you have better example ;)
Remote Proxy
A remote proxy provides a local representative for an object in different address space
For example, if you use remote procedure calls (RPC), you can easily create Proxy that would handle RPC calls. I'll use xml-rpc gem for example.
To make a remote procedure call we can use this code:
require 'xmlrpc/client'
server = XMLRPC::Client.new2("http://myproject/api/user")
result = server.call("user.Find", id)
Let's create Remote Proxy that would handle it for us:
class UserProxy
def find(id)
server.call("user.Find", id)
end
private
def server
@server ||= XMLRPC::Client.new2("http://myproject/api/user")
end
end
Now we have Proxy that we can use to get access to an object in a different address space.
Smart Reference
A smart reference is a replacement for a bare pointer that performs additional actions when an object is accessed
One of the usages is to load a persistent object into memory when it's first referenced.
Using this type of Proxy we can create Memoization for response.
Let's say that we have a third-party tool that does some heavy calculation for us. Usually, they would provide a gem for us. Let's imagine that it looks like this:
class HeavyCalculator
def calculate
# takes some time to calculate
end
end
Because it's third-party gem, we can not add memoization there. At the same time, we don't want to wait too long for every call to HeavyCalculator
. In this case, we can create a Smart Reference proxy that would add memoization for us:
class MemoizedHeavyCalculator
def calculate
@result ||= calculator.calculate
end
private
def calculator
@calculator ||= HeavyCalculator.new
end
end
Now we can use MemoizedHeavyCalculator
to call calculate
as many times as we need, and it will make the actual call just once. For any further call, it will use memoized value.
Using Proxy as a smart reference, we could add logging as well. For example: if we have third-pary service that provides some quotes for us (we can not change it's code) and we want to add logging for each call to that service. We could implement it this way:
class ExpensiveService
def get_quote(params)
# ... sending request
end
end
class ExpensiveServiceWithLog
def get_quote(params)
puts "Getting quote with params: #{params}"
service.get_quote(params)
end
private
def service
@service ||= ExpensiveServiceWithLog.new
end
end
We can not change the implementation of ExpensiveService
because it's a third-party code. But using ExpensiveServiceWithLog
we can add any sort of logging we need. Just for sake of simplicity, I used puts
there.
Related Patterns
Some implementations of a Proxy pattern are really similar to implementation of Decorator Pattern. But these two patterns have different intent. A Decorator adds responsibilities to an object, whereas proxy controls access to an object.
A Proxy might look similar to Adapter pattern. But adapter provides a different interface to the object it adapts. A Proxy provides the same interface as its subject.
PS: the proxy object should respond to all methods of the original object. In examples above, I've just implemented methods of the original object in the proxy class. The original object could have many methods and it would be a lot of repetitive code in proxy class.
Since implementation of Proxy pattern and Decorator pattern is almost the same, I highly recommend you to read about SimpleDelegator which helps to delegate methods from proxy to original object.
Thanks for reading!