奈何设计符合的接口
副标题#e#
摘要:我们在设计系统接口时,常常会碰着这样的问题:
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...
}
#p#副标题#e#
下面,我们思量接口设计的第二个原则:
组合要领(Combined Method)
组合要领常常在线程和漫衍情况中利用,来担保正确性并改进效率。
#p#分页标题#e#
一些接口提供大量的要领,起初,这些要领看来是最小化的,并且相关性强。然而,在利用的进程中,一些接口显现得过于原始,它们过于简朴化,从而迫使类用户用更多的事情来实现普通的任务,而且,要领之间的先后顺序及依赖性较量强(即,临时耦合)。这导致了代码反复,并且很是贫苦和容易堕落。
一些需要同时执行乐成的要领,在多线程、异常、和漫衍的环境下会碰着贫苦。假如两个行动需要同时执行,它们由两个独立的要领举办描写,必需都完全乐成的执行,不然会导致所有行动的回滚。
线程的引入使这种不确定性大大增加。一系列要领同时挪用一个易变的(mutable)工具,假如这个工具在线程之间共享,纵然我们假设单独的要领是线程安详的,也无法确保功效是料想之中的。看下面临Event Source的接口,它答允安放句柄和对事件的查询:
interface EventSource{
Handler getHandler(Event event);
void installHandler(Event event, Handler newHandler);
}
线程之间的交错挪用大概会引起意想不到的功效。假设source域引用一个线程共享的工具,工具很大概在1、2之间被另一个线程安装了一个新的句柄:class EventSourceExample{
public void example(Event event, Handler newHandler){
oldHandler = eventSource.getHandler(event); // 1
//工具很大概在这里被另一个线程安装了一个新的句柄
eventSource.installHandler(event, newHandler); // 2
}
private EventSource eventSource;
private Handler oldHandler;
}
为了办理问题,也需要由类的利用者而不是类的设计者来完成:class EventSourceExample{
public void example(Event event, Handler newHandler){
synchronized(eventSource){
oldHandler = eventSource.getHandler(event);
eventSource.installHandler(event, newHandler);
}
}
private EventSource eventSource;
private Handler oldHandler;
}
我们假设:方针工具eventSource是长途的,执行每一个要领体的时间和通讯的延迟对比是很短的。在这个例子中,eventSource的要领被挪用了两次,并大概在其他的实例中反复多次,因而,开销也是至少两倍。
另外尚有一个问题是对外部的synchronized同步块的利用需求。对synchronized块的利用之所以会失败,主要因为我们通过署理工具来完成事情,所以,挪用者的synchronized块,同步的是署理工具而不是最终的方针工具,挪用者不行能对其行为做太多的担保。
Combined Method必需在漫衍的情况,可能,线程情况中同时执行。它反应了用户直接的应用,规复计策和一些鸠拙的要领被封装到Combined Method中,并简化了接口,淘汰了接口中不需要的累赘。Combined Method的结果是支持一种更像事务处理惩罚气势气魄的设计。
在一个组合的Command-Query中提供一个单独的Query要领凡是是公道的。提供疏散的Command要领是不太常见的,因为Combined Method可以完成这一事情,只要挪用者简朴的忽略返回功效。假如返回一个功效招致一个开销的话,才大概会提供一个单独的Command要领。
回到前一个例子中,假如installHandler method返回上一次安装的句柄,则设计变得越发简朴和独立:interface EventSource{
Handler installHandler(Event event, Handler newHandler);
}
客户代码如下:class EventSourceExample{
public void example(Event event, Handler newHandler){
oldHandler = eventSource.installHandler(event, newHandler);
}
private EventSource eventSource;
private Handler oldHandler;
}
这样,我们给挪用者提供了一个越发安详的接口,而且不再需要他们办理线程的问题。从而低落了风险和代码量,将类设计的职责全部给了类设计者而不是推给用户,纵然有署理工具的呈现也不会影响到正确性。
一个Combined Method可以是很多Query的荟萃,很多Command的荟萃,可能两者兼有。这样,它大概增补Command、Query要领,也大概与之相抵触。当斗嘴产生的时候,优先选择Combined Method会发生一个差异的正确性和合用性。
在另一个例子中,我们思量得到资源的环境。假设,在下面的接口中,要领acquire在资源可用前阻塞:interface Resource{
boolean isAcquired();
void acquire();
void release();
}
雷同于下面的代码会在一个线程系统中推荐利用:class ResourceExample{
public void example(){
boolean acquired = false;
synchronized(resource){
if(!resource.isAcquired())
resource.acquire();
else
acquired = true;
}
if(!acquired)
...
}
private Resource resource;
}
然而,纵然我们放弃可读性和易用性,这样的设计也不是一个Command-Query疏散的设计。假如引入了署理,它就会失败:
class ActualResource implements Resource {...}
class ResourceProxy implements Resource {...}
#p#分页标题#e#
假如用户既可以通过ActualResource来完成事情,也可以通过ResourceProxy来完成事情,并且,ActualResource和ResourceProxy都没有处理惩罚同步,则synchronized块大概会失败。因为,既然我们可以通过署理工具ResourceProxy来完成事情,那么,挪用者的synchronized块,同步的就是署理工具ResourceProxy而不是最终的方针工具ActualResource。
一个Combined Method办理了这个问题,它使并发和间接性越发透明。
interface Resource{
boolean tryAcquire();
}
下面的代码清晰、简朴而且正确:class ResourceExample{
public void example(){
if(!resource.tryAcquire())
...
}
private Resource resource;
}
Combined Method带来的一个功效是使一些测试和基于断言的措施设计变得十分鸠拙,然而,它适合办理线程和漫衍问题。
实际应用中,接口应该单一化照旧复合化,要视详细环境而定。