In addition to interfaces, Slice permits the definition of classes. Classes are like interfaces in that they can have operations and are like structures in that they can have data members. This leads to hybrid objects that can be treated as interfaces and passed by reference, or can be treated as values and passed by value. Classes provide much architectural flexibility. For example, classes allow behavior to be implemented on the client side, whereas interfaces allow behavior to be implemented only on the server side.
Classes support inheritance and are therefore polymorphic: at run time, you can pass a class instance to an operation as long as the actual class type is derived from the formal parameter type in the operation’s signature. This also permits classes to be used as type-safe unions, similarly to Pascal’s discriminated variant records.
class TimeOfDay {
short hour; // 0 ‑ 23
short minute; // 0 ‑ 59
short second; // 0 ‑ 59
};
Apart from the keyword class, this definition is identical to the structure definition we saw on
page 95. You can use a Slice class wherever you can use a Slice structure (but, as we will see shortly, for performance reasons, you should not use a class where a structure is sufficient). Unlike structures, classes can be empty:
class EmptyClass {}; // OK
struct EmptyStruct {}; // Error
Much the same design considerations as for empty interfaces (see page 122) apply to empty classes: you should at least stop and rethink your approach before committing yourself to an empty class.
class TimeOfDay {
short hour; // 0 ‑ 23
short minute; // 0 ‑ 59
short second; // 0 ‑ 59
};
class DateTime extends TimeOfDay {
short day; // 1 ‑ 31
short month; // 1 ‑ 12
short year; // 1753 onwards
};
This example illustrates one major reason for using a class: a class can be extended by inheritance, whereas a structure is not extensible. The previous example defines
DateTime to extend the
TimeOfDay class with a date.
1
class TimeOfDay {
short hour; // 0 ‑ 23
short minute; // 0 ‑ 59
short second; // 0 ‑ 59
};
class Date {
short day;
short month;
short year;
};
class DateTime extends TimeOfDay, Date { // Error!
// ...
};
class Base {
int integer;
};
class Derived extends Base {
int integer; // Error, integer redefined
};
Classes use the same pass-by-value semantics as structures. If you pass a class instance to an operation, the class and all its members are passed. The usual type compatibility rules apply: you can pass a derived instance where a base instance is expected. If the receiver has static type knowledge of the actual derived run-time type, it receives the derived instance; otherwise, if the receiver does not have static type knowledge of the derived type, the instance is sliced to the base type. For an example, suppose we have the following definitions:
// In file Clock.ice:
class TimeOfDay {
short hour; // 0 ‑ 23
short minute; // 0 ‑ 59
short second; // 0 ‑ 59
};
interface Clock {
TimeOfDay getTime();
void setTime(TimeOfDay time);
};
// In file DateTime.ice:
#include <Clock.ice>
class DateTime extends TimeOfDay {
short day; // 1 ‑ 31
short month; // 1 ‑ 12
short year; // 1753 onwards
};
Because DateTime is a sub-class of
TimeOfDay, the server can return a
DateTime instance from
getTime, and the client can pass a
DateTime instance to
setTime. In this case, if both client and server are linked to include the code generated for both
Clock.ice and
DateTime.ice, they each receive the actual derived
DateTime instance, that is, the actual run-time type of the instance is preserved.
Contrast this with the case where the server is linked to include the code generated for both
Clock.ice and
DateTime.ice, but the client is linked only with the code generated for
Clock.ice. In other words, the server understands the type
DateTime and can return a
DateTime instance from
getTime, but the client only understands
TimeOfDay. In this case, the derived
DateTime instance returned by the server is sliced to its
TimeOfDay base type in the client. (The information in the derived part of the instance is simply lost to the client.)
Class hierarchies are useful if you need polymorphic values (instead of polymorphic
interfaces). For example:
class Shape {
// Definitions for shapes, such as size, center, etc.
};
class Circle extends Shape {
// Definitions for circles, such as radius...
};
class Rectangle extends Shape {
// Definitions for rectangles, such as width and length...
};
sequence<Shape> ShapeSeq;
interface ShapeProcessor {
void processShapes(ShapeSeq ss);
};
Note the definition of ShapeSeq and its use as a parameter to the
processShapes operation: the class hierarchy allows us to pass a polymorphic sequence of shapes (instead of having to define a separate operation for each type of shape).
The receiver of a ShapeSeq can iterate over the elements of the sequence and down-cast each element to its actual run-time type. (The receiver can also ask each element for its type ID to determine its type—see
Section 6.14.1 and
Section 10.11.2.)
Slice does not offer a dedicated union construct because it is redundant. By deriving classes from a common base class, you can create the same effect as with a union:
interface ShapeShifter {
Shape translate(Shape s, long xDistance, long yDistance);
};
The parameter s of the
translate operation can be viewed as a union of two members: a
Circle and a
Rectangle. The receiver of a
Shape instance can use the type ID (see
Section 4.13) of the instance to decide whether it received a
Circle or a
Rectangle. Alternatively, if you want something more along the lines of a conventional discriminated union, you can use the following approach:
class UnionDiscriminator {
int d;
};
class Member1 extends UnionDiscriminator {
// d == 1
string s;
float f;
};
class Member2 extends UnionDiscriminator {
// d == 2
byte b;
int i;
};
With this approach, the UnionDiscriminator class provides a discriminator value. The "members" of the union are the classes that are derived from
UnionDiscriminator. For each derived class, the discriminator takes on a distinct value. The receiver of such a union uses the discriminator value in a
switch statement to select the active union member.
class Link {
SomeType value;
Link next;
};
This looks very similar to the self-referential interface example on page 122, but the semantics are very different. Note that
value and
next are data members, not operations, and that the type of
next is
Link (
not Link*). As you would expect, this forms the same linked list arrangement as the
Link interface on
page 122: each instance of a
Link class contains a
next member that points at the next link in the chain; the final link’s
next member contains a null value. So, what looks like a class including itself really expresses pointer semantics: the
next data member contains a pointer to the next link in the chain.
You may be wondering at this point what the difference is then between the Link interface on
page 122 and the
Link class on
page 131. The difference is that classes have
value semantics, whereas proxies have
reference semantics. To illustrate this, consider the
Link interface from
page 122 once more:
interface Link {
idempotent SomeType getValue();
idempotent Link* next();
};
Here, getValue and
next are both operations and the return value of
next is
Link*, that is, next returns a
proxy. A proxy has
reference semantics, that is, it denotes an object somewhere. If you invoke the
getValue operation on a
Link proxy, a message is sent to the (possibly remote) servant for that proxy. In other words, for proxies, the object stays put in its server process and we access the state of the object via remote procedure calls. Compare this with the definition of our
Link class:
class Link {
SomeType value;
Link next;
};
Here, value and
next are data members and the type of next is
Link, which has
value semantics. In particular, while
next looks and feels like a pointer,
it cannot denote an instance in a different address space. This means that if we have a chain of
Link instances, all of the instances are in our local address space and, when we read or write a value data member, we are performing local address space operations. This means that an operation that returns a
Link instance, such as
getHead, does not just return the head of the chain,
but the entire chain, as shown in
Figure 4.8.
On the other hand, for the interface version of Link, we do not know where all the links are physically implemented. For example, a chain of four links could have each object instance in its own physical server process; those server processes could be each in a different continent. If you have a proxy to the head of this four-link chain and traverse the chain by invoking the
next operation on each link, you will be sending four remote procedure calls, one to each object
enum UnaryOp { UnaryPlus, UnaryMinus, Not };
enum BinaryOp { Plus, Minus, Multiply, Divide, And, Or };
class Node {};
class UnaryOperator extends Node {
UnaryOp operator;
Node operand;
};
class BinaryOperator extends Node {
BinaryOp op;
Node operand1;
Node operand2;
};
class Operand extends Node {
long val;
};
The expression tree consists of leaf nodes of type Operand, and interior nodes of type
UnaryOperator and
BinaryOperator, with one or two descendants, respectively. All three of these classes are derived from a common base class
Node. Note that
Node is an empty class. This is one of the few cases where an empty base class is justified. (See the discussion on
page 122; once we add operations to this class hierarchy (see
Section 4.11.7), the base class is no longer empty.)
If we write an operation that, for example, accepts a Node parameter, passing that parameter results in transmission of the entire tree to the server:
interface Evaluator {
long eval(Node expression); // Send entire tree for evaluation
};
Self-referential classes are not limited to acyclic graphs; the Ice run time permits loops: it ensures that no resources are leaked and that infinite loops are avoided during marshaling.
One obvious question to ask is: why does Ice provide structures as well as classes, when classes obviously can be used to model structures? The answer has to do with the cost of implementation: classes provide a number of features that are absent for structures:
Obviously, an implementation cost is associated with the additional features of classes, both in terms of the size of the generated code and the amount of memory and CPU cycles consumed at run time. On the other hand, structures are simple collections of values ("plain old structs") and are implemented using very efficient mechanisms. This means that, if you use structures, you can expect better performance and smaller memory footprint than if you would use classes (especially for languages with direct support for "plain old structures", such as C++ and C#). Use a class only if you need at least one of its more powerful features.
Classes, in addition to data members, can have operations. The syntax for operation definitions in classes is identical to the syntax for operations in interfaces. For example, we can modify the expression tree from
Section 4.11.5 as follows:
enum UnaryOp { UnaryPlus, UnaryMinus, Not };
enum BinaryOp { Plus, Minus, Multiply, Divide, And, Or };
class Node {
idempotent long eval();
};
class UnaryOperator extends Node {
UnaryOp operator;
Node operand;
};
class BinaryOperator extends Node {
BinaryOp op;
Node operand1;
Node operand2;
};
class Operand {
long val;
};
The only change compared to the version in Section 4.11.5 is that the
Node class now has an
eval operation. The semantics of this are as for a virtual member function in C++: each derived class inherits the operation from its base class and can choose to override the operation’s definition. For our expression tree, the
Operand class provides an implementation that simply returns the value of its
val member, and the
UnaryOperator and
BinaryOperator classes provide implementations that compute the value of their respective subtrees. If we call
eval on the root node of an expression tree, it returns the value of that tree, regardless of whether we have a complex expression or a tree that consists of only a single
Operand node.
Operations on classes are normally executed in the caller’s address space, that is, operations on classes are
local operations that do not result in a remote procedure call.
2 Of course, this immediately raises an interesting question: what happens if a client receives a class instance with operations from a server, but client and server are implemented in different languages? Classes with operations require the receiver to supply a factory for instances of the class. The Ice run time only marshals the data members of the class. If a class has operations, the receiver of the class must provide a class factory that can instantiate the class in the receiver’s address space, and the receiver is responsible for providing an implementation of the class’s operations.
Therefore, if you use classes with operations, it is understood that client and server each have access to an implementation of the class’s operations. No code is shipped over wire (which, in an environment of heterogeneous nodes using different operating systems and languages is infeasible).
Classes that do not use inheritance and only have data members (whether self-referential or not) pose no architectural problems: they simply are values that are marshaled like any other value, such as a sequence, structure, or dictionary. Classes using derivation also pose no problems: if the receiver of a derived instance has knowledge of the derived type, it simply receives the derived type; otherwise, the instance is sliced to the most-derived type that is understood by the receiver. This makes class inheritance useful as a system is extended over time: you can create derived class without having to upgrade all parts of the system at once.
Classes with operations require additional thought. Here is an example: suppose that you are creating an Ice application. Also assume that the Slice definitions use quite a few classes with operations. You sell your clients and servers (both written in Java) and end up with thousands of deployed systems.
As time passes and requirements change, you notice a demand for clients written in C++. For commercial reasons, you would like to leave the development of C++ clients to customers or a third party but, at this point, you discover a glitch: your application has lots of classes with operations along the following lines:
class ComplexThingForExpertsOnly {
// Lots of arcane data members here...
MysteriousThing mysteriousOperation(/* parameters */);
ArcaneThing arcaneOperation(/* parameters */);
ComplexThing complexOperation(/* parameters */);
// etc...
};
It does not matter what exactly these operations do. (Presumably, you decided to off-load some of the processing for your application onto the client side for performance reasons.) Now that you would like other developers to write C++ clients, it turns out that your application will work only if these developers provide implementations of all the client-side operations and, moreover, if the semantics of these operations exactly match the semantics of your Java implementations. Depending on what these operations do, providing exact semantic equivalents in a different language may not be trivial, so you decide to supply the C++ implementations yourself. But now, you discover another problem: the C++ clients need to be supported for a variety of operating systems that use a variety of different C++ compilers. Suddenly, your task has become quite daunting: you really need to supply implementations for all the combinations of operating systems and compiler versions that are used by clients. Given the different state of compliance with the ISO C++ standard of the various compilers, and the idiosyncrasies of different operating systems, you may find yourself facing a development task that is much larger than anticipated. And, of course, the same scenario will arise again should you need client implementations in yet another language.
The moral of this story is not that classes with operations should be avoided; they can provide significant performance gains and are not necessarily bad. But, keep in mind that, once you use classes with operations, you are, in effect, using client-side native code and, therefore, you can no longer enjoy the implementation transparencies that are provided by interfaces. This means that classes with operations should be used only if you can tightly control the deployment environment of clients. If not, you are better off using interfaces and classes without operations. That way, all the processing stays on the server and the contract between client and server is provided solely by the Slice definitions, not by the semantics of the additional client-side code that is required for classes with operations.
Ice also provides a built‑in persistence mechanism that allows you to store the state of a class in a database with very little implementation effort. To get access to these persistence features, you must define a Slice class whose members store the state of the class. We discuss the persistence features of Slice in detail in
Chapter 36.
interface Time {
idempotent TimeOfDay getTime();
idempotent void setTime(TimeOfDay time);
};
class Clock implements Time {
TimeOfDay time;
};
The implements keyword indicates that the class
Clock provides an
implementation of the
Time interface. The class can provide data members and operations of its own; in the preceding example, the
Clock class stores the current time that is accessed via the
Time interface. A class can implement several interfaces, for example:
interface Time {
idempotent TimeOfDay getTime();
idempotent void setTime(TimeOfDay time);
};
interface Radio {
idempotent void setFrequency(long hertz);
idempotent void setVolume(long dB);
};
class RadioClock implements Time, Radio {
TimeOfDay time;
long hertz;
};
The class RadioClock implements both
Time and
Radio interfaces.
interface Time {
idempotent TimeOfDay getTime();
idempotent void setTime(TimeOfDay time);
};
class Clock implements Time {
TimeOfDay time;
};
interface AlarmClock extends Time {
idempotent TimeOfDay getAlarmTime();
idempotent void setAlarmTime(TimeOfDay alarmTime);
};
interface Radio {
idempotent void setFrequency(long hertz);
idempotent void setVolume(long dB);
};
class RadioAlarmClock extends Clock
implements AlarmClock, Radio {
TimeOfDay alarmTime;
long hertz;
};
For this definition, Radio and
AlarmClock are abstract interfaces, and
Clock and
RadioAlarmClock are concrete classes. As for Java, a class can implement multiple interfaces, but can extend at most one class.
interface BaseInterface {
void op();
};
class BaseClass {
int member;
};
class DerivedClass extends BaseClass implements BaseInterface {
void someOperation(); // OK
int op(); // Error!
int someMember; // OK
long member; // Error!
};
As we saw in Section 4.11.5, classes naturally support pass-by-value semantics: passing a class transmits the data members of the class to the receiver. Any changes made to these data members by the receiver affect only the receiver’s copy of the class; the data members of the sender’s class are not affected by the changes made by the receiver.
class TimeOfDay {
short hour;
short minute;
short second;
string format();
};
interface Example {
TimeOfDay* get(); // Note: returns a proxy!
};
Note that the get operation returns a
proxy to a
TimeOfDay class and not a
TimeOfDay instance itself. The semantics of this are as follows:
•
When the client receives a TimeOfDay proxy from the
get call, it holds a proxy that differs in no way from an ordinary proxy for an interface.
•
The client can invoke operations via the proxy, but cannot access the data members. This is because proxies do not have the concept of data members, but represent interfaces: even though the
TimeOfDay class has data members, only its
operations can be accessed via a the proxy.
The net effect is that, in the preceding example, the server holds an instance of the TimeOfDay class. A proxy for that instance was passed to the client. The only thing the client can do with this proxy is to invoke the
format operation. The implementation of that operation is provided by the server and, when the client invokes
format, it sends an RPC message to the server just as it does when it invokes an operation on an interface. The implementation of the
format operation is entirely up to the server. (Presumably, the server will use the data members of the
TimeOfDay instance it holds to return a string containing the time to the client.)
The preceding example looks somewhat contrived for classes only. However, it makes perfect sense if classes implement interfaces: parts of your application can exchange class instances (and, therefore, state) by value, whereas other parts of the system can treat these instances as remote interfaces. For example:
interface Time {
string format();
// ...
};
class TimeOfDay implements Time {
short hour;
short minute;
short second;
};
interface I1 {
TimeOfDay get(); // Pass by value
void put(TimeOfDay time); // Pass by value
};
interface I2 {
Time* get(); // Pass by reference
};
In this example, clients dealing with interface I1 are aware of the
TimeOfDay class and pass it by value whereas clients dealing with interface
I2 deal only with the
Time interface. However, the actual implementation of the
Time interface in the server uses
TimeOfDay instances.
Be careful when designing systems that use such mixed pass-by-value and pass-by-reference semantics. Unless you are clear about what parts of the system deal with the interface (pass by reference) aspects and the class (pass by value) aspects, you can end up with something that is more confusing than helpful.
A good example of putting this feature to use can be found in Freeze (see Chapter 36), which allows you to add classes to an existing interface to implement persistence.
interface Time {
idempotent TimeOfDay getTime();
// ...
};
interface Record {
void addTimeStamp(Time t); // Note: Time t, not Time* t
// ...
};
Note that addTimeStamp accepts a parameter of type
Time, not of type
Time*. The question is, what does it mean to pass an interface
by value? Obviously, at run time, we cannot pass an an actual interface to this operation because interfaces are abstract and cannot be instantiated. Neither can we pass a proxy to a
Time object to
addTimeStamp because a proxy cannot be passed where an interface is expected.
However, what we can pass to
addTimeStamp is something that is not abstract and derives from the
Time interface. For example, at run time, we could pass an instance of our
TimeOfDay class from the previous section. Because the
TimeOfDay class derives from the
Time interface, the class type is compatible with the formal parameter type
Time and, at run time, what is sent over the wire to the server is the
TimeOfDay class instance.