Notes from lecture 5 – The proxy pattern
The two design patterns we discuss in lecture 4 belong to the so-called behavioral group. Such patterns implement communication protocols between a collection of objects, e.g., the visitor objects and the shape objects in the visitor pattern.
how to create new objects or collections of new objects that meet certain constraints (creational patterns);
how to manage objects in a multi-threading world (concurrency patterns); and
how to organize objects in structures that realize relations between objects (structural patterns).
The proxy pattern is a very powerful structural pattern that solves the problem of modifying the behavior of an object without changing the implementation of the class of the object nor the way other code interacts with the object.
(define player<%> (interface () register receive-token make-a-move))
(define player% (class* object% (player<%>) (super-new) (init-field name) (define my-game-state '()) (define/public (register) name) (define/public (receive-token token) (set! my-game-state (cons token my-game-state))) (define/public (make-a-move board) board)))
The proxy pattern offers an alternative that is scalable, composable and requires no changes to the player% class and minimum changes to the code that uses the player% object. The trick is quite simple: we define another class that implements the player<%> interface, has a field that stores an object of a class that implements player<%>, and methods that after some processing invoke the corresponding methods of the stored object and process the result of these invocations before returning themselves.
(define player-contract-proxy% (class* object% (player<%>) (super-new) (init-field the-real-player) (define/public (register) (define name (send the-real-player register)) (if (string? name) name "something is wrong with the real player")) (define/public (receive-token token) (if (string? token) (send the-real-player receive-token token) "something is wrong with the game admin")) (define/public (make-a-move board) (send the-real-player make-a-move board))))
> (define christos (new player% [name 'christos])) > (define christos-with-contract (new player-contract-proxy% [the-real-player christos])) > (send christos-with-contract register) "something is wrong with the real player"
> (send christos-with-contract receive-token 'VoidWarden) "something is wrong with the game admin"
(define player-logging-proxy% (class* object% (player<%>) (super-new) (init-field the-real-player) (define/public (register) (displayln "register called") (send the-real-player register)) (define/public (receive-token token) (displayln "receive-taken called") (send the-real-player receive-token token)) (define/public (make-a-move board) (displayln "make-a-move called") (send the-real-player make-a-move board))))
> (define christos-with-contract+logging (new player-logging-proxy% [the-real-player christos-with-contract])) > (send christos-with-contract+logging register) register called
"something is wrong with the real player"
> (define christos-with-contract+logging (new player-logging-proxy% [the-real-player christos])) > (send christos-with-contract+logging register) register called
'christos
The following class diagram depicts the static aspects of the relationships between objects as captured by the proxy pattern in our running example:
Specifically, both the proxy and the actual player% class implement the player<%>. Moreover, the player proxy encapsulates an instance of the player% class. As we have seen above with the logging proxy, the latter can be generalized so that the proxy encapsulates an instance of any class that implements the player<%> interface. One important bit of information from the diagram is that code that interacts with player% objects can keep doing so even if they are proxied as both player% objects and their proxies implement the same interface. This diagram gives an alternative view of the proxy pattern:
Instead of the static relationships between the objects involved, this so-called sequence diagram shows how they interact at runtime. Vertical lines correspond to the lifetimes of objects, horizontal lines from left-to-right to method calls, horizontal dashed lines from right-to-left to method returns, and white rectangles to method body computation. In a succinct manner the diagram communicates that the proxy object intercepts all method invocations and returns of the player%