Slice的主要焦点就是定义接口,例如:
struct TimeOfDay { short hour; short minute; short second; }; interface Clock { TimeOfDay getTime(); void setTime(TimeOfDay time); }
上面的代码定义了一个名为Clock的接口。该接口支持两个操作:getTime和setTime。客户端通过调用代理上的操作访问支持这个接口的对象:要读取当前的时间,客户端调用getTime;要设置当前时间,客户端通过调用setTime操作,并传递一个TimeOfDay类型的参数。
要调用一个代理上的操作就会让Ice运行时发送一个消息给目标对象。目标对象是在另一个地址空间或者是与调用者配置在一起,总之,目标对象的位置对于客户端来说是透明的。如果目标对象在另一个地址空间(有可能是一个远程的机器),Ice运行时就会通过过一个远程调用过程调用客户端要调用的操作;如果目标对象与客户端配置在一起,那么Ice运行时使用一个常规的方法来代理远程调用从而避免列集开销。
你可以认为接口就是等同于C plus plus的类定义的公共部分,也可以认为是Java的接口,以及操作定义是一个虚成员函数。不过,只有接口内部才能有操作定义。 而且你不能在接口定义中定义类型和异常,以及数据成员。但是,这并非意味着你的对象实现时不能包含状态。包含状态是可以的,但是状态的实现方式对于客户端来说是隐藏的,因此在对象的接口定于众不需要出现。
一个Ice对象只能由一个Slice接口。当然,你可以创建多个使用同一个接口的Ice对象。用C plus plus来做对比,一个Slice接口对应于C plus plus的类定义,一个Ice对象对应于C plus plus的类实例(不过,Ice的对象可以在多个不同的地址空间中实现)。
Ice也通过名为facets的特征提供了多重接口。
一个Slice接口定义了Ice中的最小分布粒度:每一个Ice对象有一个唯一区分于其它对象的标示。当开始通讯时,你必须在对象的代理上调用操作。在Ice中没有可寻址的实体的概念。你不能创建一个结构,然后让客户端远程来调用它。为了能够调用这个结构,你必须创建一个接口允许客户端通过这个接口访问这个结构。
因此,将应用划分为不同的接口在总体架构上有着深远的影响。分布边界必须遵从接口或类边界;你可以将接口的实现分布到多个地址空间中(你也可以在一个地址空间中实现多个接口),但是,你不能在不同的地址空间中实现接口的不同的部分。
参数和返回值
一个操作的定义必须包含一个返回类型和零个或多个参数的定义。例如,前面的代码汇总的getTime操作有一个TimeOfDay的返回类型以及setTime操作有一个void的返回类型。你必须使用void来指出操作不返回任何值。
一个操作可以有一个或多个输入参数,例如,setTime方法接受一个TimeOfDay类型的输入参数。当然,你可以使用多个输入参数,例如:
interface CircadianRhythm { void setSleepPeriod(TimeOfDay startTime, TimeOfDay stopTime); //... };
注意,参数名是必须的。
默认的,参事从客户端发送到服务器端,也就是说,他们是输入参数。如果要从服务器端传输到客户端,你可以使用输出参数,输出参数使用out关键字标示。例如,另一个获取当前的时间的方法可以如下:
void getTime(out TimeOfDay time);
与输入参数一样,你可以使用多个输出参数:
interface CircadianThythm { void setSleepPeriod(TimeOfDay startTime, TimeOfDay stopTime); void getSleepPeriod(out TimeOfDay startTime, out TimeOfDay stopTime); //... };
如果你既有输入参数又有输出参数,那么输出参数必须跟在输入参数的后面:
void changeSleepPeriod(TimeOfDay startTime, TimeOfDay stopTime, out TimeOfDay prevStartTime, out TimeOfDay preStopTime);
Slice不支持既做输出又作输入的参数。
操作定义的风格'
你可能会期望,语言映射可以遵循你在Slice定义中的风格:Slice的返回类型映射到编程语言的返回类型,以及Slice的参数映射到编程语言的参数。
对于只有一个返回值的操作,它一般都是由操作返回值而不是使用输出参数。这个风格自然地映射到所有编程语言。注意,如果你使用输出参数而不是返回类型,那么你就是把一个不同的API风格强加给客户:大部分的编程语言允许函数的返回值被忽略而不允许输出参数被忽略。
如果操作返回多个值,它一般使用多个输出参数并返回一个void类型。实际上,这条规则并非都适用,因为有些有多个返回值的操作中,可能有一个值比其他返回值更重要。一个典型的例子就是从一个Collection中逐步的获取其中的项:
bool next(out RecordType r);
next操作有两个返回值:一个RecordType的值,一个bool值指出是否到了集合的最后一项。这样的定义风格是非常有用的,因为这很自然的让程序员编写控制结构。例如:
while(next(record)) //处理record... if(next(record)) //获取一个有效的record...
重载
Slice不支持任何形式的操作重载。在同一个接口中的操作必须有不同的名字,与这些操作的类型和参数的数量无关。
Nonmutating 操作
有一些操作,例如上面代码中的getTime操作,这个操作不会修改所操作的对象的值。它们在概念上等效于C plus plus的const 成员函数。你可以如下的指出这样的操作:
interface Clock { nonmutating TimeOfDay getTime(); void setTime(TimeOfDay time); };
nonmutating关键字指出了getTime操作不会修改它所操作的对象的状态。这样使用有两个原因:
- 语言映射可以关于操作行为的附加知识的好处。例如:对于C plus plus来说,nonmutating操作映射到const成员函数。
- 当得知一个操作不会修改对象的状态,则允许Ice运行时更积极的进行错误恢复。特别的,Ice会保证操作调用的最多一次语义。
对于普通的操作,Ice运行时对于如何处理错误是保守的。例如,如果一个客户端发送一个操作调用到服务器,然后丢失了连接,对于客户端的Ice运行时来说,没有办法知道调用是否成功。这就意味着,运行时不能通过尝试重新连接和再次发送请求来恢复错误,因为这可能第二次引发操作以及违背了最多一次语义。运行时没有选择,只能把错误报告给应用。对于nonmutating操作,换句话说,客户端的运行时可以尝试再次连接和安全的二次送出失败的请求。如果第二次发送能够到达服务器,那么万事OK。只有第二次再次失败,错误才会报告给应用(错误重试的次数可以在Ice的配置文件中配置)。
Idempotent操作
我们可以更进一步去修改上面的Clock接口的定义,从而可以让setTime操作是idempotent的:
interface Clock { nonmutating TimeOfDay getTime(); idempotent void setTime(TimeOfDay time); };
对某一个操作进行两次成功的操作,其结果都一样,就像只调用了一次一样,那么这个操作就是idempotent操作。例如,x = 1; 是一个idempotent操作因为不管执行了一次还是两次,x的值都是1。换句话说,x += 1;就不是一个idempotent操作,因为它执行了两次后,结果不同了。
idempotent关键字指出了一个操作能够安全的执行多次。同nonmutating操作一样,Ice运行时使用idempotent来达到更积极地错误恢复。
一个操作只能是nonmutating或idempotent,不能两个都是。(nonmutating隐含了idempotent)
用户异常
查看前面的setTime操作的代码,我们发现一个潜在的问题:TimeOfDay结构中的每一个成员都是short类型,如果一个客户端调用setTime操作并且传入一个毫无意义的值,例如-199作为分钟,或者42作为小时,那么会发生什么事呢?很显然,应该提供一些提示给调用者,这个值是无意义的。Slice允许你定义用户异常来给客户端指出错误的情况。例如:
exception Error {}; //空的错误是有效的 exception RangeError { TimeOfDay errorTime; TimeOfDay minTime; TimeOfDay maxTime; };
一个用户异常很象一个结构一样包含了一些数据成员。实际上,与结构不同的是,一场能够有零数据成员,也就是说,一个空的异常。当客户端的操作的实现出现错误的条件时,异常允许你返回任意数量的错误信息。操作使用一个异常规范来说明可能会传递给客户端的异常:
interface Clock { nonmutating TimeOfDay getTime(); idempotent void setTime(TimeOfDay time) throws RangeError,Error; };
上面的定义说明了setTime操作可能会抛出一个RangeError或者一个Error用户异常。如果客户端接收到了一个RangeError异常,这个异常包含有传递给setTime的TimeOfDay值以及被引起的错误。如果setTime因为非RangeError定义的错误而调用失败,操作将爆出Error异常。很显然,因为错误没有数据成员,所以客户端将无法知道发生了什么错误,客户端只知道操作没有成功。
一个操作只可以抛出那些列在异常规范中的用户异常。如果在运行时操作的实现抛出的异常没有列在异常规范中,那么客户端将收到一个运行时异常来表示操作失败。为了说明一个操作没有抛出任何用户异常,只要简单的忽略异常规范就可以了。
异常不是第一类数据类型,第一类数据类型也不是异常:
- 你不能将异常作为参数值传递
- 你不能使用异常作为数据成员类型
- 你不能使用异常作为序列的元素类型
- 你不能使用异常作为字典的键值或值值
- 你不能抛出一个非异常类型的值
异常继承
异常支持继承:
exception ErrorBase { string reason; }; enum RTError { DivideByZerp, NegativeRoot, IllegalNull /*...*/ }; exception RuntimeError extends ErrorBase { RTError err; }; enum LError { ValueOutRange, ValuesInconsistent, /*...*/ }; exception LogicError extends ErrorBase { LError err; } exception RangeError extend LogicError { TimeOfDay errorTime; TimeOfDay minTime; TimeOfDay maxTime; }
上面的定义建造了一个层次的异常定义:
- ErrorBase是继承树的根并且包含了一个字符串,用来存放引发错误的原因。
- RuntimeError和LogicError继承于ErrorBase,每一种异常包含一个分类错误的枚举值。
- 最后,RangeError继承于LogicError,并且报告了指定的错误的细节。
建立这样一个异常层次结构不仅仅是有助于创建一个更易读的规范,还能够在语言层次上带来好处。例如C plus plus映射会保持异常的层次结构,这样你就可以用基类俘获异常,或者建立异常句柄来处理指定的异常。
查看上面的异常层次,这还不是很清楚,在运行时,应用将会抛出继承的异常,例如RangeError,还是基类异常,例如LogicError,RuntimeError和ErrorBase。如果你指明一个积累异常,接口或类是抽象的,你可以添加注释达到效果。
注意,如果一个操作的异常规范指明了一个异常规范类型,在运行时,操作的实现可能抛出多重继承异常。例如:
exception Base { //... }; exception Derived extends Base { //... }; interface Exsample { void op() throw Base; //可能抛出基类也可能是继承类. }
随着系统的演变,系统中可能会加入新的,继承的异常。假设我们开始的系统中是如下定义的:
exception Error { //.. }; interface Application { void doSomthing() throw Error; };
再假设已经部署了大量的客户端,也就说,当你升级系统时,你不能轻松的升级所有的客户端。随着这个系统的演变,一个新的异常被加入到系统中,并且服务器端要重新使用新的定义部署:
exception Error { //... }; exception FatalApplicationError extend Error { //... }; interface Application { void doSomething() throws Error; };
如果服务器端抛出了一个FatalApplicationError,那么会发生什么事情?这就需要看客户端是否是使用新的或还是使用旧的定义了:
- 如果客户端使用和服务器端相同的定义,那么客户端就会收到FaltalApplicationError。
- 如果客户端使用的是旧的定义,那么客户端不知道FatalApplicationError错误的存在,这样,Ice运行时会自动将错误切成继承层次中最深的、能被客户端理解的异常类型(在这个例子中是Error),并且抛弃与派生的异常相关的信息。
异常只支持单继承。