Table of Contents Previous Next
Logo
The Slice Language : 4.10 Interfaces, Operations, and Exceptions
Copyright © 2003-2008 ZeroC, Inc.

4.10 Interfaces, Operations, and Exceptions

The central focus of Slice is on defining interfaces, for example:
struct TimeOfDay {
    short hour;         // 0  23
    short minute;       // 0  59
    short second;       // 0  59
};

interface Clock {
    TimeOfDay getTime();
    void setTime(TimeOfDay time);
};
This definition defines an interface type called Clock. The interface supports two operations: getTime and setTime. Clients access an object supporting the Clock interface by invoking an operation on the proxy for the object: to read the current time, the client invokes the getTime operation; to set the current time, the client invokes the setTime operation, passing an argument of type TimeOfDay.
Invoking an operation on a proxy instructs the Ice run time to send a message to the target object. The target object can be in another address space or can be collocated (in the same process) as the caller—the location of the target object is transparent to the client. If the target object is in another (possibly remote) address space, the Ice run time invokes the operation via a remote procedure call; if the target is collocated with the client, the Ice run time uses an ordinary function call instead, to avoid the overhead of marshaling.
You can think of an interface definition as the equivalent of the public part of a C++ class definition or as the equivalent of a Java interface, and of operation definitions as (virtual) member functions. Note that nothing but operation definitions are allowed to appear inside an interface definition. In particular, you cannot define a type, an exception, or a data member inside an interface. This does not mean that your object implementation cannot contain state—it can, but how that state is implemented (in the form of data members or otherwise) is hidden from the client and, therefore, need not appear in the object’s interface definition.
An Ice object has exactly one (most derived) Slice interface type (or class type—see Section 4.11). Of course, you can create multiple Ice objects that have the same type; to draw the analogy with C++, a Slice interface corresponds to a C++ class definition, whereas an Ice object corresponds to a C++ class instance (but Ice objects can be implemented in multiple different address spaces).
Ice also provides multiple interfaces via a feature called facets. We discuss facets in detail in Chapter 30.
A Slice interface defines the smallest grain of distribution in Ice: each Ice object has a unique identity (encapsulated in its proxy) that distinguishes it from all other Ice objects; for communication to take place, you must invoke operations on an object’s proxy. There is no other notion of an addressable entity in Ice. You cannot, for example, instantiate a Slice structure and have clients manipulate that structure remotely. To make the structure accessible, you must create an interface that allows clients to access the structure.
The partition of an application into interfaces therefore has profound influence on the overall architecture. Distribution boundaries must follow interface (or class) boundaries; you can spread the implementation of interfaces over multiple address spaces (and you can implement multiple interfaces in the same address space), but you cannot implement parts of interfaces in different address spaces.

4.10.1 Parameters and Return Values

An operation definition must contain a return type and zero or more parameter definitions. For example, the getTime operation on page 101 has a return type of TimeOfDay and the setTime operation has a return type of void. You must use void to indicate that an operation returns no value—there is no default return type for Slice operations.
An operation can have one or more input parameters. For example, setTime accepts a single input parameter of type TimeOfDay called time. Of course, you can use multiple input parameters, for example:
interface CircadianRhythm {
    void setSleepPeriod(TimeOfDay startTime, TimeOfDay stopTime);
    // ...
};
Note that the parameter name (as for Java) is mandatory. You cannot omit the parameter name, so the following is in error:
interface CircadianRhythm {
    void setSleepPeriod(TimeOfDay, TimeOfDay);  // Error!
    // ...
};
By default, parameters are sent from the client to the server, that is, they are input parameters. To pass a value from the server to the client, you can use an output parameter, indicated by the out keyword. For example, an alternative way to define the getTime operation on page 101 would be:
void getTime(out TimeOfDay time);
This achieves the same thing but uses an output parameter instead of the return value. As with input parameters, you can use multiple output parameters:
interface CircadianRhythm {
    void setSleepPeriod(TimeOfDay startTime, TimeOfDay stopTime);
    void getSleepPeriod(out TimeOfDay startTime,
                        out TimeOfDay stopTime);
    // ...
};
If you have both input and output parameters for an operation, the output parameters must follow the input parameters:
void changeSleepPeriod(    TimeOfDay startTime,     // OK
                           TimeOfDay stopTime,
                       out TimeOfDay prevStartTime,
                       out TimeOfDay prevStopTime);
void changeSleepPeriod(out TimeOfDay prevStartTime,
                       out TimeOfDay prevStopTime,
                           TimeOfDay startTime,     // Error
                           TimeOfDay stopTime);
Slice does not support parameters that are both input and output parameters (call by reference). The reason is that, for remote calls, reference parameters do not result in the same savings that one can obtain for call by reference in programming languages. (Data still needs to be copied in both directions and any gains in marshaling efficiency are negligible.) Also, reference (or input–output) parameters result in more complex language mappings, with concomitant increases in code size.

Style of Operation Definition

As you would expect, language mappings follow the style of operation definition you use in Slice: Slice return types map to programming language return types, and Slice parameters map to programming language parameters.
For operations that return only a single value, it is common to return the value from the operation instead of using an out-parameter. This style maps naturally into all programming languages. Note that, if you use an out-parameter instead, you impose a different API style on the client: most programming languages permit the return value of a function to be ignored whereas it is typically not possible to ignore an output parameter.
For operations that return multiple values, it is common to return all values as out-parameters and to use a return type of void. However, the rule is not all that clear-cut because operations with multiple output values can have one particular value that is considered more "important" than the remainder. A common example of this is an iterator operation that returns items from a collection one-by-one:
bool next(out RecordType r);
The next operation returns two values: the record that was retrieved and a Boolean to indicate the end-of-collection condition. (If the return value is false, the end of the collection has been reached and the parameter r has an undefined value.) This style of definition can be useful because it naturally fits into the way programmers write control structures. For example:
while (next(record))
    // Process record...

if (next(record))
    // Got a valid record...

Overloading

Slice does not support any form of overloading of operations. For example:
interface CircadianRhythm {
    void modify(TimeOfDay startTime,
                TimeOfDay endTime);
    void modify(    TimeOfDay startTime,        // Error
                    TimeOfDay endTime,
                out timeOfDay prevStartTime,
                out TimeOfDay prevEndTime);
};
Operations in the same interface must have different names, regardless of what type and number of parameters they have. This restriction exists because overloaded functions cannot sensibly be mapped to languages without built‑in support for overloading.1

Idempotent Operations

Some operations, such as getTime on page 101, do not modify the state of the object they operate on. They are the conceptual equivalent of C++ const member functions. Similary, setTime does modify the state of the object, but is idempotent. You can indicate this in Slice as follows:
interface Clock {
    idempotent TimeOfDay getTime();
    idempotent void setTime(TimeOfDay time);
};
This marks the getTime and setTime operations as idempotent. An operation is idempotent if two successive invocations of the operation have the same effect as a single invocation. For example, x = 1; is an idempotent operation because it does not matter whether it is executed once or twice—either way, x ends up with the value 1. On the other hand, x += 1; is not an idempotent operation because executing it twice results in a different value for x than executing it once. Obviously, any read-only operation is idempotent.
The idempotent keyword is useful because it allows the Ice run time to attempt more aggressive error recovery. Specifically, Ice guarantees at-most-once semantics for operation invocations:
• For normal (not idempotent) operations, the Ice run time has to be conservative about how it deals with errors. For example, if a client sends an operation invocation to a server and then loses connectivity, there is no way for the client-side run time to find out whether the request it sent actually made it to the server. This means that the run time cannot attempt to recover from the error by re-establishing a connection and sending the request a second time because that could cause the operation to be invoked a second time and violate at-most-once semantics; the run time has no option but to report the error to the application.
• For idempotent operations, on the other hand, the client-side run time can attempt to re-establish a connection to the server and safely send the failed request a second time. If the server can be reached on the second attempt, everything is fine and the application never notices the (temporary) failure. Only if the second attempt fails need the run time report the error back to the application. (The number of retries can be increased with an Ice configuration parameter.)

4.10.2 User Exceptions

Looking at the setTime operation on page 101, we find a potential problem: given that the TimeOfDay structure uses short as the type of each field, what will happen if a client invokes the setTime operation and passes a TimeOfDay value with meaningless field values, such as 199 for the minute field, or 42 for the hour? Obviously, it would be nice to provide some indication to the caller that this is meaningless. Slice allows you to define user exceptions to indicate error conditions to the client. For example:
exception Error {}; // Empty exceptions are legal

exception RangeError {
    TimeOfDay errorTime;
    TimeOfDay minTime;
    TimeOfDay maxTime;
};
A user exception is much like a structure in that it contains a number of data members. However, unlike structures, exceptions can have zero data members, that is, be empty. Exceptions allow you to return an arbitrary amount of error information to the client if an error condition arises in the implementation of an operation. Operations use an exception specification to indicate the exceptions that may be returned to the client:
interface Clock {
    idempotent TimeOfDay getTime();
    idempotent void setTime(TimeOfDay time)
                        throws RangeError, Error;
};
This definition indicates that the setTime operation may throw either a RangeError or an Error user exception (and no other type of exception). If the client receives a RangeError exception, the exception contains the TimeOfDay value that was passed to setTime and caused the error (in the errorTime member), as well as the minimum and maximum time values that can be used (in the minTime and maxTime members). If setTime failed because of an error not caused by an illegal parameter value, it throws Error. Obviously, because Error does not have data members, the client will have no idea what exactly it was that went wrong—it simply knows that the operation did not work.
An operation can throw only those user exceptions that are listed in its exception specification. If, at run time, the implementation of an operation throws an exception that is not listed in its exception specification, the client receives a run-time exception (see Section 4.10.4) to indicate that the operation did something illegal. To indicate that an operation does not throw any user exception, simply omit the exception specification. (There is no empty exception specification in Slice.)
Exceptions are not first-class data types and first-class data types are not exceptions:
• You cannot pass an exception as a parameter value.
• You cannot use an exception as the type of a data member.
• You cannot use an exception as the element type of a sequence.
• You cannot use an exception as the key or value type of a dictionary.
• You cannot throw a value of non-exception type (such as a value of type int or string).
The reason for these restrictions is that some implementation languages use a specific and separate type for exceptions (in the same way as Slice does). For such languages, it would be difficult to map exceptions if they could be used as an ordinary data type. (C++ is somewhat unusual among programming languages by allowing arbitrary types to be used as exceptions.)

4.10.3 Exception Inheritance

Exceptions support inheritance. For example:
exception ErrorBase {
    string reason;
};

enum RTError {
    DivideByZero, NegativeRoot, IllegalNull /* ... */
};

exception RuntimeError extends ErrorBase {
    RTError err;
};

enum LError { ValueOutOfRange, ValuesInconsistent, /* ... */ };

exception LogicError extends ErrorBase {
    LError err;
};

exception RangeError extends LogicError {
    TimeOfDay errorTime;
    TimeOfDay minTime;
    TimeOfDay maxTime;
};
These definitions set up a simple exception hierarchy:
• ErrorBase is at the root of the tree and contains a string explaining the cause of the error.
• Derived from ErrorBase are RuntimeError and LogicError. Each of these exceptions contains an enumerated value that further categorizes the error.
• Finally, RangeError is derived from LogicError and reports the details of the specific error.
Setting up exception hierarchies such as this not only helps to create a more readable specification because errors are categorized, but also can be used at the language level to good advantage. For example, the Slice C++ mapping preserves the exception hierarchy so you can catch exceptions generically as a base exception, or set up exception handlers to deal with specific exceptions.
Looking at the exception hierarchy on page 108, it is not clear whether, at run time, the application will only throw most derived exceptions, such as RangeError, or if it will also throw base exceptions, such as LogicError, RuntimeError, and ErrorBase. If you want to indicate that a base exception, interface, or class is abstract (will not be instantiated), you can add a comment to that effect.
Note that, if the exception specification of an operation indicates a specific exception type, at run time, the implementation of the operation may also throw more derived exceptions. For example:
exception Base {
    // ...
};

exception Derived extends Base {
    // ...
};

interface Example {
    void op() throws Base;      // May throw Base or Derived
};
In this example, op may throw a Base or a Derived exception, that is, any exception that is compatible with the exception types listed in the exception specification can be thrown at run time.
As a system evolves, it is quite common for new, derived exceptions to be added to an existing hierarchy. Assume that we initially construct clients and server with the following definitions:
exception Error {
    // ...
};

interface Application {
    void doSomething() throws Error;
};
Also assume that a large number of clients are deployed in field, that is, when you upgrade the system, you cannot easily upgrade all the clients. As the application evolves, a new exception is added to the system and the server is redeployed with the new definition:
exception Error {
    // ...
};

exception FatalApplicationError extends Error {
    // ...
};

interface Application {
    void doSomething() throws Error;
};
This raises the question of what should happen if the server throws a FatalApplicationError from doSomething. The answer depends whether the client was built using the old or the updated definition:
• If the client was built using the same definition as the server, it simply receives a FatalApplicationError.
• If the client was built with the original definition, that client has no knowledge that FatalApplicationError even exists. In this case, the Ice run time automatically slices the exception to the most-derived type that is understood by the receiver (Error, in this case) and discards the information that is specific to the derived part of the exception. (This is exactly analogous to catching C++ exceptions by value—the exception is sliced to the type used in the catch-clause.)
Exceptions support single inheritance only. (Multiple inheritance would be difficult to map into many programming languages.)

4.10.4 Ice Run-Time Exceptions

As mentioned in Section 2.2.2, in addition to any user exceptions that are listed in an operation’s exception specification, an operation can also throw Ice run-time exceptions. Run-time exceptions are predefined exceptions that indicate platform-related run-time errors. For example, if a networking error interrupts communication between client and server, the client is informed of this by a run-time exception, such as ConnectTimeoutException or SocketException.
The exception specification of an operation must not list any run-time exceptions. (It is understood that all operations can raise run-time exceptions and you are not allowed to restate that.)

Inheritance Hierarchy for Exceptions

All the Ice run-time and user exceptions are arranged in an inheritance hierarchy, as shown in Figure 4.3.
Figure 4.3. Inheritance structure for exceptions.
Ice::Exception is at the root of the inheritance hierarchy. Derived from that are the (abstract) types Ice::LocalException and Ice::UserException. In turn, all run-time exceptions are derived from Ice::LocalException, and all user exceptions are derived from Ice::UserException.
Figure 4.4 shows the complete hierarchy of the Ice run-time exceptions.2
Figure 4.4. Ice run-time exception hierarchy. (Shaded exceptions can be sent by the server.)
Note that Figure 4.4 groups several exceptions into a single box to save space (which, strictly, is incorrect UML syntax). Also note that some run-time exceptions have data members, which, for brevity, we have omitted in Figure 4.4. These data members provide additional information about the precise cause of an error.
Many of the run-time exceptions have self-explanatory names, such as MemoryLimitException. Others indicate problems in the Ice run time, such as EncapsulationException. Still others can arise only through application programming errors, such as TwowayOnlyException. In practice, you will likely never see most of these exceptions. However, there are a few run-time exceptions you will encounter and whose meaning you should know.

Local Versus Remote Exceptions

Most error conditions are detected on the client side. For example, if an attempt to contact a server fails, the client-side run time raises a ConnectTimeoutException. However, there are three specific error conditions (shaded in Figure 4.4) that are detected by the server and made known explicitly to the client-side run time via the Ice protocol:
• ObjectNotExistException
This exception indicates that a request was delivered to the server but the server could not locate a servant with the identity that is embedded in the proxy. In other words, the server could not find an object to dispatch the request to.
An ObjectNotExistException is a death certificate: it indicates that the target object in the server does not exist.3 Most likely, this is the case because the object existed some time in the past and has since been destroyed, but the same exception is also raised if a client uses a proxy with the identity of an object that has never been created. If you receive this exception, you are expected to clean up whatever resources you might have allocated that relate to the specific object for which you receive this exception.
• FacetNotExistException
The client attempted to contact a non-existent facet of an object, that is, the server has at least one servant with the given identity, but no servant with a matching facet name. (See Chapter 30 for a discussion of facets.)
• OperationNotExistException
This exception is raised if the server could locate an object with the correct identity but, on attempting to dispatch the client’s operation invocation, the server found that the target object does not have such an operation. You will see this exception in only two cases:
You have used an unchecked down-cast on a proxy of the incorrect type. (See page 210 and page 327 for unchecked down-casts.)
Client and server have been built with Slice definitions for an interface that disagree with each other, that is, the client was built with an interface definition for the object that indicates that an operation exists, but the server was built with a different version of the interface definition in which the operation is absent.
Any error condition on the server side that is not described by one of the three preceding exceptions is made known to the client as one of three generic exceptions (shaded in Figure 4.4):
• UnknownUserException
This exception indicates that an operation implementation has thrown a Slice exception that is not declared in the operation’s exception specification (and is not derived from one of the exceptions in the operation’s exception specification).
• UnknownLocalException
If an operation implementation raises a run-time exception other than ObjectNotExistException, FacetNotExistException, or OperationNotExistException (such as a NotRegisteredException), the client receives an UnknownLocalException. In other words, the Ice protocol does not transmit the exact exception that was encountered in the server, but simply returns a bit to the client in the reply to indicate that the server encountered a run-time exception.
A common cause for a client receiving an UnknownLocalException is failure to catch and handle all exceptions in the server. For example, if the implementation of an operation encounters an exception it does not handle, the exception propagates all the way up the call stack until the stack is unwound to the point where the Ice run time invoked the operation. The Ice run time catches all Ice exceptions that "escape" from an operation invocation and returns them to the client as an UnknownLocalException.
• UnknownException
An operation has thrown a non-Ice exception. For example, if the operation in the server throws a C++ exception, such as a char *, or a Java exception, such as a ClassCastException, the client receives an UnknownException.
All other run-time exceptions (not shaded in Figure 4.4) are detected by the client-side run time and are raised locally.
It is possible for the implementation of an operation to throw Ice run-time exceptions (as well as user exceptions). For example, if a client holds a proxy to an object that no longer exists in the server, your server application code is required to throw an ObjectNotExistException. If you do throw run-time exceptions from your application code, you should take care to throw a run-time exception only if appropriate, that is, do not use run-time exceptions to indicate something that really should be a user exception. Doing so can be very confusing to the client: if the application "hijacks" some run-time exceptions for its own purposes, the client can no longer decide whether the exception was thrown by the Ice run time or by the server application code. This can make debugging very difficult.

4.10.5 Interface Semantics and Proxies

Building on the Clock example, we can create definitions for a world-time server:
exception GenericError {
    string reason;
};

struct TimeOfDay {
    short hour;         // 0  23
    short minute;       // 0  59
    short second;       // 0  59
};

exception BadTimeVal extends GenericError {};

interface Clock {
    idempotent TimeOfDay getTime();
    idempotent void setTime(TimeOfDay time) throws BadTimeVal;
};

dictionary<string, Clock*> TimeMap; // Time zone name to clock map

exception BadZoneName extends GenericError {};

interface WorldTime {
    idempotent void addZone(string zoneName, Clock* zoneClock);
    void removeZone(string zoneName) throws BadZoneName;
    idempotent Clock* findZone(string zoneName)
                                    throws BadZoneName;
    idempotent TimeMap listZones();
    idempotent void setZones(TimeMap zones);
};
The WorldTime interface acts as a collection manager for clocks, one for each time zone. In other words, the WorldTime interface manages a collection of pairs. The first member of each pair is a time zone name; the second member of the pair is the clock that provides the time for that zone. The interface contains operations that permit you to add or remove a clock from the map (addZone and removeZone), to search for a particular time zone by name (findZone), and to read or write the entire map (listZones and setZones).
The WorldTime example illustrates an important Slice concept: note that addZone accepts a parameter of type Clock* and findZone returns a parameter of type Clock*. In other words, interfaces are types in their own right and can be passed as parameters. The * operator is known as the proxy operator. Its left-hand argument must be an interface (or class—see Section 4.11) and its return type is a proxy. A proxy is like a pointer that can denote an object. The semantics of proxies are very much like those of C++ class instance pointers:
• A proxy can be null (see page 121).
• A proxy can dangle (point at an object that is no longer there)
• Operations dispatched via a proxy use late binding: if the actual run-time type of the object denoted by the proxy is more derived than the proxy’s type, the implementation of the most-derived interface will be invoked.
When a client passes a Clock proxy to the addZone operation, the proxy denotes an actual Clock object in a server. The Clock Ice object denoted by that proxy may be implemented in the same server process as the WorldTime interface, or in a different server process. Where the Clock object is physically implemented matters neither to the client nor to the server implementing the WorldTime interface; if either invokes an operation on a particular clock, such as getTime, an RPC call is sent to whatever server implements that particular clock. In other words, a proxy acts as a local "ambassador" for the remote object; invoking an operation on the proxy forwards the invocation to the actual object implementation. If the object implementation is in a different address space, this results in a remote procedure call; if the object implementation is collocated in the same address space, the Ice run time uses an ordinary local function call from the proxy to the object implementation.
Note that proxies also act very much like pointers in their sharing semantics: if two clients have a proxy to the same object, a state change made by one client (such as setting the time) will be visible to the other client.
Proxies are strongly typed (at least for statically typed languages, such as C++ and Java). This means that you cannot pass something other than a Clock proxy to the addZone operation; attempts to do so are rejected at compile time.

4.10.6 Interface Inheritance

Interfaces support inheritance. For example, we could extend our world-time server to support the concept of an alarm clock:
interface AlarmClock extends Clock {
    idempotent TimeOfDay getAlarmTime();
    idempotent void       setAlarmTime(TimeOfDay alarmTime)
                                         throws BadTimeVal;
};
The semantics of this are the same as for C++ or Java: AlarmClock is a subtype of Clock and an AlarmClock proxy can be substituted wherever a Clock proxy is expected. Obviously, an AlarmClock supports the same getTime and setTime operations as a Clock but also supports the getAlarmTime and setAlarmTime operations.
Multiple interface inheritance is also possible. For example, we can construct a radio alarm clock as follows:
interface Radio {
    void setFrequency(long hertz) throws GenericError;
    void setVolume(long dB) throws GenericError;
};

enum AlarmMode { RadioAlarm, BeepAlarm };

interface RadioClock extends Radio, AlarmClock {
    void      setMode(AlarmMode mode);
    AlarmMode getMode();
};
RadioClock extends both Radio and AlarmClock and can therefore be passed where a Radio, an AlarmClock, or a Clock is expected. The inheritance diagram for this definition looks as follows:
Figure 4.5. Inheritance diagram for RadioClock.
Interfaces that inherit from more than one base interface may share a common base interface. For example, the following definition is legal:
interface B { /* ... */ };
interface I1 extends B { /* ... */ };
interface I2 extends B { /* ... */ };
interface D extends I1, I2 { /* ... */ };
This definition results in the familiar diamond shape:
Figure 4.6. Diamond-shaped inheritance.

Interface Inheritance Limitations

If an interface uses multiple inheritance, it must not inherit the same operation name from more than one base interface. For example, the following definition is illegal:
interface Clock {
    void set(TimeOfDay time);                   // set time
};

interface Radio {
    void set(long hertz);                       // set frequency
};

interface RadioClock extends Radio, Clock {     // Illegal!
    // ...
};
This definition is illegal because RadioClock inherits two set operations, Radio::set and Clock::set. The Slice compiler makes this illegal because (unlike C++) many programming languages do not have a built‑in facility for disambiguating the different operations. In Slice, the simple rule is that all inherited operations must have unique names. (In practice, this is rarely a problem because inheritance is rarely added to an interface hierarchy "after the fact". To avoid accidental clashes, we suggest that you use descriptive operation names, such as setTime and setFrequency. This makes accidental name clashes less likely.)

Implicit Inheritance from Object

All Slice interfaces are ultimately derived from Object. For example, the inheritance hierarchy from Figure 4.5 would be shown more correctly as in Figure 4.7.
Figure 4.7. Implicit inheritance from Object.
Because all interfaces have a common base interface, we can pass any type of interface as that type. For example:
interface ProxyStore {
    idempotent  void    putProxy(string name, Object* o);
    idempotent Object* getProxy(string name);
};
Object is a Slice keyword (note the capitalization) that denotes the root type of the inheritance hierarchy. The ProxyStore interface is a generic proxy storage facility: the client can call putProxy to add a proxy of any type under a given name and later retrieve that proxy again by calling getProxy and supplying that name. The ability to generically store proxies in this fashion allows us to build general-purpose facilities, such as a naming service that can store proxies and deliver them to clients. Such a service, in turn, allows us to avoid hard-coding proxy details into clients and servers (see Chapter 35).
Inheritance from type Object is always implicit. For example, the following Slice definition is illegal:
interface MyInterface extends Object { /* ... */ }; // Error!
It is understood that all interfaces inherit from type Object; you are not allowed to restate that.
Type Object is mapped to an abstract type by the various language mappings, so you cannot instantiate an Ice object of that type.

Null Proxies

Looking at the ProxyStore interface once more, we notice that getProxy does not have an exception specification. The question then is what should happen if a client calls getProxy with a name under which no proxy is stored? Obviously, we could add an exception to indicate this condition to getProxy. However, another option is to return a null proxy. Ice has the built‑in notion of a null proxy, which is a proxy that "points nowhere". When such a proxy is returned to the client, the client can test the value of the returned proxy to check whether it is null or denotes a valid object.
A more interesting question is: "which approach is more appropriate, throwing an exception or returning a null proxy?" The answer depends on the expected usage pattern of an interface. For example, if, in normal operation, you do not expect clients to call getProxy with a non-existent name, it is better to throw an exception. (This is probably the case for our ProxyStore interface: the fact that there is no list operation makes it clear that clients are expected to know which names are in use.)
On the other hand, if you expect that clients will occasionally try to look up something that is not there, it is better to return a null proxy. The reason is that throwing an exception breaks the normal flow of control in the client and requires special handling code. This means that you should throw exceptions only in exceptional circumstances. For example, throwing an exception if a database lookup returns an empty result set is wrong; it is expected and normal that a result set is occasionally empty.
It is worth paying attention to such design issues: well-designed interfaces that get these details right are easier to use and easier to understand. Not only do such interfaces make life easier for client developers, they also make it less likely that latent bugs cause problems later.

Self-Referential Interfaces

Proxies have pointer semantics, so we can define self-referential interfaces. For example:
interface Link {
    idempotent SomeType getValue();
    idempotent Link*    next();
};
The Link interface contains a next operation that returns a proxy to a Link interface. Obviously, this can be used to create a chain of interfaces; the final link in the chain returns a null proxy from its next operation.

Empty Interfaces

The following Slice definition is legal:
interface Empty {};
The Slice compiler will compile this definition without complaint. An interesting question is: "why would I need an empty interface?" In most cases, empty interfaces are an indication of design errors. Here is one example:
interface ThingBase {};

interface Thing1 extends ThingBase {
    // Operations here...
};

interface Thing2 extends ThingBase {
    // Operations here...
};
Looking at this definition, we can make two observations:
• Thing1 and Thing2 have a common base and are therefore related.
• Whatever is common to Thing1 and Thing2 can be found in interface ThingBase.
Of course, looking at ThingBase, we find that Thing1 and Thing2 do not share any operations at all because ThingBase is empty. Given that we are using an object-oriented paradigm, this is definitely strange: in the object-oriented model, the only way to communicate with an object is to send a message to the object. But, to send a message, we need an operation. Given that ThingBase has no operations, we cannot send a message to it, and it follows that Thing1 and Thing2 are not related because they have no common operations. But of course, seeing that Thing1 and Thing2 have a common base, we conclude that they are related, otherwise the common base would not exist. At this point, most programmers begin to scratch their head and wonder what is going on here.
One common use of the above design is a desire to treat Thing1 and Thing2 polymorphically. For example, we might continue the previous definition as follows:
interface ThingUser {
    void putThing(ThingBase* thing);
};
Now the purpose of having the common base becomes clear: we want to be able to pass both Thing1 and Thing2 proxies to putThing. Does this justify the empty base interface? To answer this question, we need to think about what happens in the implementation of putThing. Obviously, putThing cannot possibly invoke an operation on a ThingBase because there are no operations. This means that putThing can do one of two things:
1. putThing can simply remember the value of thing.
2. putThing can try to down-cast to either Thing1 or Thing2 and then invoke an operation. The pseudo-code for the implementation of putThing would look something like this:
void putThing(ThingBase thing)
{
    if (is_a(Thing1, thing)) {
        // Do something with Thing1...
    } else if (is_a(Thing2, thing)) {
        // Do something with Thing2...
    } else {
        // Might be a ThingBase?
        // ...
    }
}
The implementation tries to down-cast its argument to each possible type in turn until it has found the actual run-time type of the argument. Of course, any object-oriented text book worth its price will tell you that this is an abuse of inheritance and leads to maintenance problems.
If you find yourself writing operations such as putThing that rely on artificial base interfaces, ask yourself whether you really need to do things this way. For example, a more appropriate design might be:
interface Thing1 {
    // Operations here...
};

interface Thing2 {
    // Operations here...
};

interface ThingUser {
    void putThing1(Thing1* thing);
    void putThing2(Thing2* thing);
};
With this design, Thing1 and Thing2 are not related, and ThingUser offers a separate operation for each type of proxy. The implementation of these operations does not need to use any down-casts, and all is well in our object-oriented world.
Another common use of empty base interfaces is the following:
interface PersistentObject {};

interface Thing1 extends PersistentObject {
    // Operations here...
};

interface Thing2 extends PersistentObject {
    // Operations here...
};
Clearly, the intent of this design is to place persistence functionality into the PersistentObject base implementation and require objects that want to have persistent state to inherit from PersistentObject. On the face of things, this is reasonable: after all, using inheritance in this way is a well-established design pattern, so what can possibly be wrong with it? As it turns out, there are a number of things that are wrong with this design:
• The above inheritance hierarchy is used to add behavior to Thing1 and Thing2. However, in a strict OO model, behavior can be invoked only by sending messages. But, because PersistentObject has no operations, no messages can be sent.
This raises the question of how the implementation of PersistentObject actually goes about doing its job; presumably, it knows something about the implementation (that is, the internal state) of Thing1 and Thing2, so it can write that state into a database. But, if so, PersistentObject, Thing1, and Thing2 can no longer be implemented in different address spaces because, in that case, PersistentObject can no longer get at the state of Thing1 and Thing2.
Alternatively, Thing1 and Thing2 use some functionality provided by PersistentObject in order to make their internal state persistent. But PersistentObject does not have any operations, so how would Thing1 and Thing2 actually go about achieving this? Again, the only way that can work is if PersistentObject, Thing1, and Thing2 are implemented in a single address space and share implementation state behind the scenes, meaning that they cannot be implemented in different address spaces.
• The above inheritance hierarchy splits the world into two halves, one containing persistent objects and one containing non-persistent ones. This has far-reaching ramifications:
Suppose you have an existing application with already implemented, non-persistent objects. Requirements change over time and you find that you now would like to make some of your objects persistent. With the above design, you cannot do this unless you change the type of your objects because they now must inherit from PersistentObject. Of course, this is extremely bad news: not only do you have to change the implementation of your objects in the server, you also need to locate and update all the clients that are currently using your objects because they suddenly have a completely new type. What is worse, there is no way to keep things backward compatible: either all clients change with the server, or none of them do. It is impossible for some clients to remain "unupgraded".
The design does not scale to multiple features. Imagine that we have a number of additional behaviors that objects can inherit, such as serialization, fault-tolerance, persistence, and the ability to be searched by a search engine. We quickly end up in a mess of multiple inheritance. What is worse, each possible combination of features creates a completely separate type hierarchy. This means that you can no longer write operations that generically operate on a number of object types. For example, you cannot pass a persistent object to something that expects a non-persistent object, even if the receiver of the object does not care about the persistence aspects of the object. This quickly leads to fragmented and hard-to-maintain type systems. Before long, you will either find yourself rewriting your application or end up with something that is both difficult to use and difficult to maintain.
The foregoing discussion will hopefully serve as a warning: Slice is an interface definition language that has nothing to do with implementation (but empty interfaces almost always indicate that implementation state is shared via mechanisms other than defined interfaces). If you find yourself writing an empty interface definition, at least step back and think about the problem at hand; there may be a more appropriate design that expresses your intent more cleanly. If you do decide to go ahead with an empty interface regardless, be aware that, almost certainly, you will lose the ability to later change the distribution of the object model over physical server process because you cannot place an address space boundary between interfaces that share hidden state.

Interface Versus Implementation Inheritance

Keep in mind that Slice interface inheritance applies only to interfaces. In particular, if two interfaces are in an inheritance relationship, this in no way implies that the implementations of those interfaces must also inherit from each other. You can choose to use implementation inheritance when you implement your interfaces, but you can also make the implementations independent of each other. (To C++ programmers, this often comes as a surprise because C++ uses implementation inheritance by default, and interface inheritance requires extra effort to implement.)
In summary, Slice inheritance simply establishes type compatibility. It says nothing about how interfaces are implemented and, therefore, keeps implementation choices open to whatever is most appropriate for your application.

1
Name mangling is not an option in this case: while it works fine for compilers, it is unacceptable to humans.

2
We use the Unified Modeling Language (UML) for the object model diagrams in this book (see [1] and [13] for details).

3
The Ice run time raises ObjectNotExistException only if there are no facets in existence with a matching identity; otherwise, it raises FacetNotExistException (see Chapter 30).

Table of Contents Previous Next
Logo