How should I pass proxies?

We occasionally see Slice definitions such as the following:

Slice
interface ThingManager
{
    string get(/* params */);
    // ...
}
interface Thing
{
    // ...
}

The client code then looks something like this:

C++
shared_ptr<ThingManagerPrx> tm = ...;
string thingStr = tm->get(/* params */);

shared_ptr<ObjectPrx> obj = communicator->stringToProxy(thingStr);
shared_ptr<ThingPrx> thing = Ice::uncheckedCast<ThingPrx>(obj);
// Use thing...

While this works, it has a number of problems. For one, returning a stringified proxy to the client and then converting the string back into a proxy is somewhat inefficient. (Not seriously so, but why waste CPU cycles when there is no need to?)

Second, and more seriously, this design suffers from three flaws:

  1. A reader looking at the ThingManager interface in isolation sees that the get operation returns a string. But, as the code demonstrates, what is really returned is a proxy — it's just that the proxy is returned as a string and converted back to a proxy in the client. In other words, the design obscures the intent, which is that the get operation returns a Thing proxy.
  2. The design is vulnerable to programming error. For example, the server could accidentally return a string that is not a stringified proxy. In that case, the client's call to stringToProxy will throw an exception because it does not parse correctly.
  3. The design is not type safe. For example, the server could mistakenly return a stringified proxy to an object other than a Thing. In that case, the client will successfully convert the string into to a proxy and, because the client has chosen to use an uncheckedCast, will even be able to down-cast that proxy into a Thing proxy. But of course, when the client makes its first invocation via that proxy, things will go wrong: the client will most likely get an exception but, per chance, an invocation may even work, but do something entirely unexpected.

    Replacing the uncheckedCast with a checkedCast detects the problem earlier, but still does not fix the underlying issue, namely, that it is possible for the server to return an object that is-not-a Thing.

Compare the preceding Slice definitions to the following:

Slice
interface Thing
{
    // ...
}
interface ThingManager
{
    Thing* get(/* params */);
}

With this design, the client code looks as follows:

C++
shared_ptr<ThingManagerPrx> tm = ...;
shared_ptr<ThingPrx> thing = tm->get(/* params */);
// Use thing...

For one, this is a lot simpler: the code neither calls stringToProxy, nor does it use a down-cast. Secondly, this design clearly expresses the intent: one look at the Slice definitions tells us that the get operation returns a Thing proxy. And, most importantly, this design is type safe: the server can return only a proxy to an object of type Thing (or a proxy to an object that is derived from Thing), and the client can receive only a proxy of type Thing (or a proxy to a base type of Thing). Passing a proxy of the wrong type in the server causes a compile-time error, as does treating the return value as the wrong type in the client.

Given that the second design is obviously superior, why do we need stringified proxies at all? The answer is that we need them for bootstrapping. The client must have a proxy to at least one object before it can invoke an operation, but to invoke an operation, the client must have a proxy. In other words, stringified proxies solve the chicken-and-egg problem of how to get an initial proxy (or a small number of initial proxies), so a client can start making invocations.

The upshot of all this is that the only time you should use a stringified proxy in your application is during startup. (Most likely, you will get a stringified proxy from a configuration file or as a command-line argument.) Thereafter, you should never see a stringified proxy again. Instead, pass proxies as a first-class data type just as you would pass integers or structures. This is not only more efficient, but documents the intent of your design much better. And, if you make a mistake in your code, you get to find out at compile time instead of at run time.

See Also