下载本文示例代码
本文转自IBM Developerworks中国网站摘要:我们在设计系统接口时,经常会遇到这样的问题: 1、我们的接口应该提供多少方法才合适? 2、我们的接口应该提供"原子方法"还是"复合方法"? 3、我们的接口是否应该封装(或者,能否封装)所有的细节? 接口的设计需要考虑用户的使用习惯、使用的方便程度、使用的安全程度,根据我的编程经验,下面会详细讨论接口设计的2个需要权衡的方面:接口的单一化 & 复合化。 接口 接口提供了不同系统之间或者系统不同组件之间的界定。在软件中,接口提供了一个屏障,从而从实现中分离目标,从具体中分离抽象,从作者中分离用户。 站在用户的角度看,一个接口建立并命名了一个目标对象的使用方法。一些约束(例如:编译时的类型系统、运行时的异常机制及返回值)使得类作者的目的得以体现和加强。供给(affordances)指事物的被感知的真实的属性,这些属性可以决定事物使用的可能方法,供给提供了对事物操作的线索。 类设计者的一个职责便是在接口中减小约束与供给之间的隔阂、匹配目标以及一定程度上的自由度,尽可能减小错误使用目标对象的可能。 封装 对于封装来说,远不止数据私有那么简单。在设计中,封装往往会涉及到自我包含(self-containment)。如果一个类需要你知道如何调用它方法(e.g. 在一个线程的环境中,在一个方法调用后调用另一个方法,你必须明确地同步对象),那么它的封装性就不如将所有这些全部包含并隐藏的类(e.g. 这个类是thread-safe的)好。前一个设计存在着设计的漏洞,它的许多限定条件是模糊的,而且把部分责任推给了用户,而不是让类提供者做这些工作来完成类的设计。 在空间或者时间上分离方法的执行(例如,线程,远程方法调用,消息队列),能够对设计的正确性和效率产生意义深远的影响。这种分离带来的结果是不可忽视的:
并发引入了不确定性和环境(context)选择的开销;
分布引入了回调的开销,这些开销可能不断增加,而且会导致错误。 这些是设计的问题,修改它们可不是象修改bug那样简单。 如果一个接口主要由存取方法(set和get方法)组成,每个方法都相应的直接指向某个私有域,那么它的封装性会很差。接口中的域存取方法通常是不会提供信息的:他们在对象的使用中不能通讯、简单化和抽象化,这通常会导致代码冗长,并且容易出错。 所以,我们首先考虑接口设计的第一个原则: 命令与查询分离(Command-Query Separation) 要求:保证一个方法不是命令(Command)就是查询(Query) 定义: 查询:当一个方法返回一个值来回应一个问题的时候,它就具有查询的性质; 命令:当一个方法要改变对象的状态的时候,它就具有命令的性质; 通常,一个方法可能是纯的Command模式或者是纯的Query模式,或者是两者的混合体。在设计接口时,如果可能,应该尽量使接口单一化,保证方法的行为严格的是命令或者是查询,这样查询方法不会改变对象的状态,没有副作用(side effects),而会改变对象的状态的方法不可能有返回值。也就是说:如果我们要问一个问题,那么就不应该影响到它的答案。实际应用,要视具体情况而定,语义的清晰性和使用的简单性之间需要权衡。 例如,在java.util.Iterator中,hasNext可以被看作一种查询,remove是一种命令,next合并了命令和查询:
public interface Iterator{
boolean hasNext();
Object next();
void remove();
}
这里,如果不将一个Iterator对象的当前值向前到下一个的话,就不能够查询一个Iterator对象。如果没有提供一个复合方法next,我们将需要定义一系列的命令方法,例如:初始化(initialization)、继续(continuation)、访问(access)和前进(advance),它们虽然清晰定义了每个动作,但是,客户代码过于复杂:
for(initialization; continuation condition; advance){
... access for use ...
}
将Command和Query功能合并入一个方法,方便了客户的使用,但是,降低了清晰性,而且,可能不便于基于断言的程序设计并且需要一个变量来保存查询结果:
Iterator iterator = collection.iterator();
while(iterator.hasNext();){
Object current = iterator.next();
... use current...
}
下面,我们考虑接口设计的第二个原则: 组合方法(Combined Method) 组合方法经常在线程和分布环境中使用,来保证正确性并改善效率。 一些接口提供大量的方法,起初,这些方法看来是最小化的,而且相关性强。然而,在使用的过程中,一些接口显现得过于原始,它们过于简单化,从而迫使类用户用更多的工作来实现普通的任务,并且,方法之间的先后顺序及依赖性比较强(即,暂时耦合)。这导致了代码重复,而且非常麻烦和容易出错。 一些需要同时执行成功的方法,在多线程、异常、和分布的情况下会遇到麻烦。如果两个动作需要同时执行,它们由两个独立的方法进行描述,必须都完全成功的执行,否则会导致所有动作的回滚。 线程的引入使这种不确定性大大增加。一系列方法同时调用一个易变的(mutable)对象,如果这个对象在线程之间共享,即使我们假设单独的方法是线程安全的,也无法确保结果是意料之中的。看下面对Event Source的接口,它允许安置句柄和对事件的查询:
interface EventSource{
Handler getHandler(Event event);
void installHandler(Event event, Handler newHandler);
}
共2页。 1 2 :
本文转自IBM Developerworks中国网站摘要:我们在设计系统接口时,经常会遇到这样的问题: 1、我们的接口应该提供多少方法才合适? 2、我们的接口应该提供"原子方法"还是"复合方法"? 3、我们的接口是否应该封装(或者,能否封装)所有的细节? 接口的设计需要考虑用户的使用习惯、使用的方便程度、使用的安全程度,根据我的编程经验,下面会详细讨论接口设计的2个需要权衡的方面:接口的单一化 & 复合化。 接口 接口提供了不同系统之间或者系统不同组件之间的界定。在软件中,接口提供了一个屏障,从而从实现中分离目标,从具体中分离抽象,从作者中分离用户。 站在用户的角度看,一个接口建立并命名了一个目标对象的使用方法。一些约束(例如:编译时的类型系统、运行时的异常机制及返回值)使得类作者的目的得以体现和加强。供给(affordances)指事物的被感知的真实的属性,这些属性可以决定事物使用的可能方法,供给提供了对事物操作的线索。 类设计者的一个职责便是在接口中减小约束与供给之间的隔阂、匹配目标以及一定程度上的自由度,尽可能减小错误使用目标对象的可能。 封装 对于封装来说,远不止数据私有那么简单。在设计中,封装往往会涉及到自我包含(self-containment)。如果一个类需要你知道如何调用它方法(e.g. 在一个线程的环境中,在一个方法调用后调用另一个方法,你必须明确地同步对象),那么它的封装性就不如将所有这些全部包含并隐藏的类(e.g. 这个类是thread-safe的)好。前一个设计存在着设计的漏洞,它的许多限定条件是模糊的,而且把部分责任推给了用户,而不是让类提供者做这些工作来完成类的设计。 在空间或者时间上分离方法的执行(例如,线程,远程方法调用,消息队列),能够对设计的正确性和效率产生意义深远的影响。这种分离带来的结果是不可忽视的:
并发引入了不确定性和环境(context)选择的开销;
分布引入了回调的开销,这些开销可能不断增加,而且会导致错误。 这些是设计的问题,修改它们可不是象修改bug那样简单。 如果一个接口主要由存取方法(set和get方法)组成,每个方法都相应的直接指向某个私有域,那么它的封装性会很差。接口中的域存取方法通常是不会提供信息的:他们在对象的使用中不能通讯、简单化和抽象化,这通常会导致代码冗长,并且容易出错。 所以,我们首先考虑接口设计的第一个原则: 命令与查询分离(Command-Query Separation) 要求:保证一个方法不是命令(Command)就是查询(Query) 定义: 查询:当一个方法返回一个值来回应一个问题的时候,它就具有查询的性质; 命令:当一个方法要改变对象的状态的时候,它就具有命令的性质; 通常,一个方法可能是纯的Command模式或者是纯的Query模式,或者是两者的混合体。在设计接口时,如果可能,应该尽量使接口单一化,保证方法的行为严格的是命令或者是查询,这样查询方法不会改变对象的状态,没有副作用(side effects),而会改变对象的状态的方法不可能有返回值。也就是说:如果我们要问一个问题,那么就不应该影响到它的答案。实际应用,要视具体情况而定,语义的清晰性和使用的简单性之间需要权衡。 例如,在java.util.Iterator中,hasNext可以被看作一种查询,remove是一种命令,next合并了命令和查询:
public interface Iterator{
boolean hasNext();
Object next();
void remove();
}
这里,如果不将一个Iterator对象的当前值向前到下一个的话,就不能够查询一个Iterator对象。如果没有提供一个复合方法next,我们将需要定义一系列的命令方法,例如:初始化(initialization)、继续(continuation)、访问(access)和前进(advance),它们虽然清晰定义了每个动作,但是,客户代码过于复杂:
for(initialization; continuation condition; advance){
... access for use ...
}
将Command和Query功能合并入一个方法,方便了客户的使用,但是,降低了清晰性,而且,可能不便于基于断言的程序设计并且需要一个变量来保存查询结果:
Iterator iterator = collection.iterator();
while(iterator.hasNext();){
Object current = iterator.next();
... use current...
}
下面,我们考虑接口设计的第二个原则: 组合方法(Combined Method) 组合方法经常在线程和分布环境中使用,来保证正确性并改善效率。 一些接口提供大量的方法,起初,这些方法看来是最小化的,而且相关性强。然而,在使用的过程中,一些接口显现得过于原始,它们过于简单化,从而迫使类用户用更多的工作来实现普通的任务,并且,方法之间的先后顺序及依赖性比较强(即,暂时耦合)。这导致了代码重复,而且非常麻烦和容易出错。 一些需要同时执行成功的方法,在多线程、异常、和分布的情况下会遇到麻烦。如果两个动作需要同时执行,它们由两个独立的方法进行描述,必须都完全成功的执行,否则会导致所有动作的回滚。 线程的引入使这种不确定性大大增加。一系列方法同时调用一个易变的(mutable)对象,如果这个对象在线程之间共享,即使我们假设单独的方法是线程安全的,也无法确保结果是意料之中的。看下面对Event Source的接口,它允许安置句柄和对事件的查询:
interface EventSource{
Handler getHandler(Event event);
void installHandler(Event event, Handler newHandler);
}
共2页。 1 2 :
下载本文示例代码
怎样设计合适的接口怎样设计合适的接口怎样设计合适的接口怎样设计合适的接口怎样设计合适的接口怎样设计合适的接口怎样设计合适的接口怎样设计合适的接口怎样设计合适的接口怎样设计合适的接口怎样设计合适的接口怎样设计合适的接口怎样设计合适的接口怎样设计合适的接口怎样设计合适的接口
阅读(134) | 评论(0) | 转发(0) |