软件接口设计指南Word文档下载推荐.docx
《软件接口设计指南Word文档下载推荐.docx》由会员分享,可在线阅读,更多相关《软件接口设计指南Word文档下载推荐.docx(13页珍藏版)》请在冰豆网上搜索。
5规定
5.1JAVA接口设计方法
我们在设计系统接口时,经常会遇到这样的问题:
我们的接口应该提供多少方法才合适?
我们的接口应该提供"
原子方法"
还是"
复合方法"
?
我们的接口是否应该封装(或者,能否封装)所有的细节?
接口的设计需要考虑用户的使用习惯、使用的方便程度、使用的安全程度,根据我的编程经验,下面会详细讨论接口设计的2个需要权衡的方面:
接口的单一化&
复合化。
接口
接口提供了不同系统之间或者系统不同组件之间的界定。
在软件中,接口提供了一个屏障,从而从实现中分离目标,从具体中分离抽象,从作者中分离用户。
站在用户的角度看,一个接口建立并命名了一个目标对象的使用方法。
一些约束(例如:
编译时的类型系统、运行时的异常机制及返回值)使得类作者的目的得以体现和加强。
供给(affordances)指事物的被感知的真实的属性,这些属性可以决定事物使用的可能方法,供给提供了对事物操作的线索。
类设计者的一个职责便是在接口中减小约束与供给之间的隔阂、匹配目标以及一定程度上的自由度,尽可能减小错误使用目标对象的可能。
封装
对于封装来说,远不止数据私有那么简单。
在设计中,封装往往会涉及到自我包含(self-containment)。
如果一个类需要你知道如何调用它方法(e.g.在一个线程的环境中,在一个方法调用后调用另一个方法,你必须明确地同步对象),那么它的封装性就不如将所有这些全部包含并隐藏的类(e.g.这个类是thread-safe的)好。
前一个设计存在着设计的漏洞,它的许多限定条件是模糊的,而且把部分责任推给了用户,而不是让类提供者做这些工作来完成类的设计。
在空间或者时间上分离方法的执行(例如,线程,远程方法调用,消息队列),能够对设计的正确性和效率产生意义深远的影响。
这种分离带来的结果是不可忽视的:
并发引入了不确定性和环境(context)选择的开销;
组合方法经常在线程和分布环境中使用,来保证正确性并改善效率。
一些接口提供大量的方法,起初,这些方法看来是最小化的,而且相关性强。
然而,在使用的过程中,一些接口显现得过于原始,它们过于简单化,从而迫使类用户用更多的工作来实现普通的任务,并且,方法之间的先后顺序及依赖性比较强(即,暂时耦合)。
这导致了代码重复,而且非常麻烦和容易出错。
一些需要同时执行成功的方法,在多线程、异常、和分布的情况下会遇到麻烦。
如果两个动作需要同时执行,它们由两个独立的方法进行描述,必须都完全成功的执行,否则会导致所有动作的回滚。
线程的引入使这种不确定性大大增加。
一系列方法同时调用一个易变的(mutable)对象,如果这个对象在线程之间共享,即使我们假设单独的方法是线程安全的,也无法确保结果是意料之中的。
看下面对EventSource的接口,它允许安置句柄和对事件的查询:
interfaceEventSource{
HandlergetHandler(Eventevent);
voidinstallHandler(Eventevent,HandlernewHandler);
}
线程之间的交叉调用可能会引起意想不到的结果。
假设source域引用一个线程共享的对象,对象很可能在1、2之间被另一个线程安装了一个新的句柄:
classEventSourceExample{
publicvoidexample(Eventevent,HandlernewHandler){
oldHandler=eventSource.getHandler(event);
//1
//对象很可能在这里被另一个线程安装了一个新的句柄
eventSource.installHandler(event,newHandler);
//2
privateEventSourceeventSource;
privateHandleroldHandler;
为了解决问题,也需要由类的使用者而不是类的设计者来完成:
synchronized(eventSource){
我们假设:
目标对象eventSource是远程的,执行每一个方法体的时间和通讯的延迟相比是很短的。
在这个例子中,eventSource的方法被调用了两次,并可能在其他的实例中重复多次,因而,开销也是至少两倍。
此外还有一个问题是对外部的synchronized同步块的使用需求。
对synchronized块的使用之所以会失败,主要因为我们通过代理对象来完成工作,所以,调用者的synchronized块,同步的是代理对象而不是最终的目标对象,
调用者不可能对其行为做太多的保证。
CombinedMethod必须在分布的环境,或者,线程环境中同时执行。
它反映了用户直接的应用,恢复策略和一些笨拙的方法被封装到CombinedMethod中,并简化了接口,减少了接口中不需要的累赘。
CombinedMethod的效果是支持一种更像事务处理风格的设计。
在一个组合的Command-Query中提供一个单独的Query方法通常是合理的。
提供分离的Command方法是不太常见的,因为CombinedMethod可以完成这一工作,只要调用者简单的忽略返回结果。
如果返回一个结果招致一个开销的话,才可能会提供一个单独的Command方法。
回到前一个例子中,如果installHandlermethod返回上一次安装的句柄,则设计变得更加简单和独立:
HandlerinstallHandler(Eventevent,HandlernewHandler);
客户代码如下:
oldHandler=eventSource.installHandler(event,newHandler);
这样,我们给调用者提供了一个更加安全的接口,并且不再需要他们解决线程的问题。
从而降低了风险和代码量,将类设计的职责全部给了类设计者而不是推给用户,即使有代理对象的出现也不会影响到正确性。
一个CombinedMethod可以是许多Query的集合,许多Command的集合,或者两者兼有。
这样,它可能补充Command、Query方法,也可能与之相抵触。
当冲突发生的时候,优先选择CombinedMethod会产生一个不同的正确性和适用性。
在另一个例子中,我们考虑获得资源的情况。
假设,在下面的接口中,方法acquire在资源可用前阻塞:
interfaceResource{
booleanisAcquired();
voidacquire();
voidrelease();
类似于下面的代码会在一个线程系统中推荐使用:
classResourceExample{
publicvoidexample(){
booleanacquired=false;
synchronized(resource){
if(!
resource.isAcquired())
resource.acquire();
else
acquired=true;
acquired)
...
privateResourceresource;
然而,即使我们放弃可读性和易用性,这样的设计也不是一个Command-Query分离的设计。
如果引入了代理,它就会失败:
classActualResourceimplementsResource{...}
classResourceProxyimplementsResource{...}
如果用户既可以通过ActualResource来完成工作,也可以通过ResourceProxy来完成工作,而且,ActualResource和ResourceProxy都没有处理同步,则synchronized块可能会失败。
因为,既然我们可以通过代理对象ResourceProxy来完成工作,那么,调用者的synchronized块,同步的就是代理对象ResourceProxy而不是最终的目标对象ActualResource。
一个CombinedMethod解决了这个问题,它使并发和间接性更加透明。
booleantryAcquire();
下面的代码清晰、简单并且正确:
resource.tryAcquire())
CombinedMethod带来的一个结果是使一些测试和基于断言的程序设计变得十分笨拙,然而,它适合解决线程和分布问题。
实际应用中,接口应该单一化还是复合化,要视具体情况而定。
5.2C++接口设计方法
在系统中,观察一个class有两个角度,从外部或者用户角度我们看到的是接口,从内部我们看到的是实现。
因为系统肯定要不断修改,因此实现免不了不停的变化,但是接口又被要求尽量保持稳定。
这两者的矛盾必须通过良好的设计尽量避免,基本原则就是将实现细节与接口隔离。
下面列出几条比较具体点的:
·
接口的设计保持最小而完整
精简接口函数个数,使每一个函数有代表性,函数功能恰好覆盖class的职能。
一个最小的接口可以使维护简单,增加潜在的代码重用性,减少客户的迷惑,并且也可以缩小头文件长度和编译时间。
当改进函数时,应该用类似函数名实现改进而保留原函数,代码注释里应该有相应的说明。
可以增加新函数,但不能删除旧函数。
成员变量应该都为私有,显而易见,public变量破坏封装性以及接口和实现的分离;
protected变量也可能使客户编写继承类而依赖于父类的实现细节。
避免函数返回成员变量的指针或引用,这么做也会使客户代码依赖于实现细节。
考虑是否禁用编译器缺省产生的函数,这些函数包括:
复制构造函数,赋值操作符(operator=)。
如果我们不打