Remote proxy pattern has to be my favorite design pattern of all time. (people have favorite design patterns... right..?) It allows for a really unique way to develop client/server applications without even considering the client/server aspect until all the functionality is implemented and tested. Not only does it let you effectively mock the entire system and test locally, but it also will give you effectively both a client and server implementation in one go.
I've been advocating for this design pattern ever since seeing it in my undergrad Software Development course. However, after talking to a bunch of people, I've realized that nobody really covers this pattern, so I wanted to write up a quick page explaining why its awesome and how it works.
To do this I'm going to introduce a simple client/server protocol for rolling any number of any-sided dice, walk through the local implementation, then use remote proxy pattern to make it into a proper full network protocol. The result will be both a client and server implementation as well as a fully local implementation. I will be doing this all in a pseudocode which is similar to Java, but glazes over annoying steps like serialization/deserialization and network sockets.
The RTD Protocol Local Implementation
The "Roll the Dice" protocol is as follows:
CLIENT: Roll me two 6-sided dice. SERVER: I got 1 and 5. CLIENT: Roll me one 20-sided die. SERVER: I got 14.
To implement locally, we just make a class called something like
RTDServer and implement it like you'd expect. So it should be
something like:
class RTDServer {
int[] roll(int sides, int rolls) {
int[] result = [];
for (int i = 0; i < rolls; i++) {
int roll = Random.range(1, sides); // Random from [1, sides] inclusive.
result.push(roll);
}
return result;
}
}
Now our client can use it simply like this:
RTDServer s = new RTDServer(); s.roll(6, 2); s.roll(20, 1);
If we were to graph out our protocol right now, it would look something like this:
Method call
[Client] -----------> [Server]
Return value
[Client] <----------- [Server]
This is fully local code with no networking in sight. Its also really trivial to test. But our goal is to expose this over the network, and this doesn't accomplish that. This is where we move to remote proxy pattern.
The Core of Remote Proxy Pattern
To add networking to our protocol (to make it truly client/server), we need to add a layer in between which will translate our method calls into some serialized message for transport over the network, and on the other side turn that message back into a method call and execute it. The flow will effectively look like this once we are all done:
Method call Network Method call
[Client] -----------> [Proxy] ======> [Handler] -----------> [Server]
Return Value Network Return value
[Client] <----------- [Proxy] <====== [Handler] <----------- [Server]
Where Client is the client we defined earlier (with the only change that s is now and instance of RemoteRTDServer rather than RTDServer), and Server is that instance of RTDServer.
So what is this Proxy thing that we introduced on the client side?
This is effectively just a class which implements the same interface as
RTDServer, however, instead of working locally, it packages up our
message and sends it over the network. It then waits for a response from the
remote server, then decodes that and returns it. As far as the client is
concerned, nothing has changed from the local implementation to this remote
implementation. The interface is the same and the return values are the same.
On the opposite side, the Handler waits for any messages from the
Proxy, then decodes the values sent as arguments, then runs them on
the local implementation of RTDServer which we made earlier. It then packages up
the return value there and sends it back to the proxy over the network. Like the
client, RTDServer is totally unaware that it isn't being called locally. Methods
are being invoked in the same way as when we were local, but those parameters
are coming from over the network instead of being supplied directly to it.
The implementation of the Proxy and Handler can be as simple as:
class RTDProxy implements RTDServer {
int[] roll(int sides, int rolls) {
Request req = new Request(sides, rolls);
Network.send(req);
Response res = Network.read();
return res.results;
}
}
class RTDHandler {
// Suppose this is called when a message is received over the network.
void onNetworkMessage(Request req) {
int[] results = this.server.roll(req.sides, req.rolls);
Response res = new Response(results);
Network.send(res);
}
}
Again, notice that we didn't need to change a single line of the existing RTDServer to make it networked. This means any tests we had written to make sure RTDServer works don't need to be rewritten in order to make sure it works over the network. This means if we have a really complicated protocol, we can test locally, then network it, in which case, the only additional tests we need are only concerned with the packaging and calling of methods rather than the methods' functionality. We also technically don't even need to know the implementation of RTDServer (or the client if RTDServer is provided to it rather than created by it). What an awesome little design pattern!