如何设计优秀的API(3)

翻译 2007年10月10日 15:40:00

 

 

翻译时间:31-08-2007

译者:周林

( 版权所有,未经译者同意谢绝转载 )

 

 

接口 vs. 抽象类(Interfaces vs. Abstract Classes)

    喜欢使用纯接口的人与喜欢使用抽象类的人似乎永远都会相互争论。像这样的争论每隔几个月就会发生,无休无止。因为人们都趋向于固执己见。通常像这样的争论是在背景都不一样的情况下发生的 —— 用例或者需求都不相同。下面我们从用例的角度来看这个问题。

 

使用接口的好处(The Advantages of Interfaces)

    最显而易见的一点是类型的使用。如果是用抽象类来实现的话,是不允许多继承的。这个问题仅仅在以下的情况中才会显得尖锐突出:当类型很巨大,或者为了提高开发者的工作效率,在子类中重用父类的实现的时候。我们可以称这样的它们为“支撑类(support class)”,在支撑类中有一个子类,它重用了某个父类的实现。

    使用接口的第二个好处是:可以将API与它的实现有效地分离。但是抽象类也有这个功能,但是必须由抽象类自己来保证这种分离,而用接口的话,这种分离是由编译器来保证的。

 

使用抽象类的好处(The Advantages of Abstract Classes)

    人们喜欢使用抽象类最主要的原因是它的进化能力 —— 它可以增加一个有缺省实现的新方法而不影响已有的客户和实现方(在这里我们谈的是运行期的兼容性,而不是编译期的兼容性)。接口不具备这种能力,所以必须引入另一个接口来提供扩展功能,如:interface BuildTargetDependencyEx extends BuildTargetDependency。这种情况下,原始的接口仍然有效,新的接口也可用。

    抽象类另一个很有用的特性在于它的限制访问权限的能力。公共接口中的方法都是公有类型的,所有人都可以实现该接口。但是在现实情况中,通常应该进行限制。接口缺少这种限制能力。

    其次,抽象类可以有静态工厂方法。当然,对于接口,可以使用工厂方法来创建不同的类,但是,类其实才是容纳返回实例对象的工厂方法最合理也是最自然的地方。

 

用例(Use cases)

    现在让我们举一些现实世界中的例子,来谈谈接口和抽象类哪个好一些,并且阐明其原因。

 

TopManager

 

    TopManager可以说是NetBeans开源API中的老资格了。它被设计成连接org.openide.* 包和这些包在org.netbeans.core里的实现的纽带。该manager(由core提供)只有一个实例,并且该API的客户不应该扩展或者实现它。

分析表明:TopManager是为客户提供一系列有用的方法,但是对这些方的实现有完全控制权的典型案例。客户应该把精力放在对该API的使用上面,动态地去发现其实现(该API在编译单元openide里,而其实现在另一个编译单元core里)。

在这种情况下,和抽象类相比,使用接口没有任何优势。抽象类可以有工厂方法,可以增加新方法,可以有效地将API与其实现相分离,可以防止除默认实例之外的实例化的发生。如果你也面临类似的情况,最好使用抽象类。

    再举一个例子来说明使用接口的后果:让我们把目光放在和TopManager处于同一个包中的Places接口上面。实际上,该接口和TopManager一样,也只允许一个实例存在。该实例可以通过工厂方法TopManager.getDefault().getPlaces()进行访问。而且,该接口的所有方法都可以是TopManager的一部分。我们仅仅想在逻辑上将该接口与其实现分开,而且我们是使用接口来达到这个目的。结果,新版本的应该很有用的“places”被创建以后,我们将不敢为它添加新方法。一旦我们创建了这样的Places2接口之后会产生严重的后果,所以使用Places接口的用户越来越少,现在几乎被丢弃不用了。

 

Cookies

    Cookie是一种编码模式,它允许任何对象提供某种特性(这种特性称为cookie)给调用者:

 

OpenCookie opencookie = (OpenCookie)anObject.getCookie(OpenCookie.class);

 

if(openCookie != null) {

   opneCookie.open();

}

 

    那么OpenCookie应该被设计成接口还是抽象类呢?简单的分析表明:存在很多的客户,API的用户以及很多经常想同时提供多个Cookie的服务提供者。此外,cookie自身只有一个open方法。以上者所有的一切都表明Cookie应该被设计成接口。这样的话,我们就有多继承能力,而且不用害怕接口的功能扩展问题 —— 因为该接口只有一个方法。除此之外,也没有必要提供工厂方法,没有必要担心子类化问题。综上所述,设计成接口是正确的选择。

    类似的,还有何多其它cookie的例子 —— InstanceCookie。它也是一个接口,在以前的老版本里有三个方法。但是在发布了几个版本之后,我们意识到有必要改善该接口的性能,所以我们不得不引入一个子类InstanceCookie.Of extending InstanceCookie,并且为它增加了一个instanceOf方法。当然,这样的更改没有问题,但是给使用该接口的用户带来了不少麻烦。每个使用该API的用户都必须如下编码:

 

Boolean doIAccept;

InstanceCookie ic = (InstanceCookie)obj.getCookie(InstanceCookie.class);

if(ic instanceOf InstanceCookie.Of) {

   doIAccept = ((InstanceCookie.Of)ic).instanceOf(myRequiredClass);

} else {

   doIAccept = ic != null &&

      myRequiredClass.isAssighnableFrom(ic.instanceClass());

}

 

    以上的代码看起来并不简单,而且这样的代码遍布了整个代码库。但是我们给这个cookie增加新方法的时候是多么简单啊:

Boolean isInstanceOf(Class c) {

   return c.isAssighnableFrom(instanceClass());

}

 

    但是Java并不允许接口中存在方法的缺省实现。我们应该换用抽象类吗?不,我们不应该这样做,当前的用例和OpenCookie类似,但是得用到一个技巧:

我们并不把那三个方法放进该接口,取而代之的是仅仅增加一个返回包含所有必要信息的类的方法:

 

Interface InstanceCookie {

   public Info instanceInfo();

  

   public static class Info extends Object {

      public String instanceName();

      public Class instanceClass();

      public Object instanceCreate();

   }

}

 

    以上的解决方案似乎是完美的。客户有简单的API可以使用,服务提供者可以实现而不是扩展这个接口。instanceInfo方法可以实例化info,实例化方式可以是:使用构造器,使用工厂方法,或者是使用子类化。这样的话,在InstanceCookie中增加instanceOf方法就一点问题也没有了。InstanceCookie.Info是一个类,它可以由一个有缺省实现的方法来进行扩展。

当然为了使这样增加方法的处理是安全的,最好把这个类声明成final类型,并且为InstanceCookie的实现方提供工厂方法。这样的工厂方法可以有两种:一种很简单,比方说给instanceName,instanceClass和instanceCreate方法准备好返回值;另一种会使用另一个接口,该接口中的方法会来处理像info.instanceCreate这样的方法调用。具体采用哪一种取决于API用户的需求。

    请注意:Java监听器采用了类似的模式。每个监听器都是一个接口,它有固定数目的方法。但是每个方法都对应一个EventObject,EventObject是一个类。如果有必要的话,可以为该类增加一个新方法。

 

文件对象(FileObject)

    另一个来自NetBeans的例子是FileObject(filesystem API的一部分)。它的用法似乎和TopManager的例子很相似(其实不然):很少有人直接子类化FileObject(Java规范中的HttpFileSystem,Kyley和Niclas),但是使用该客户API的人却很多。直接子类化FileSystem的人也很少。由此看来,似乎应该把FileObjct和FileSystme作为抽象类,但是事实上是作为接口的。此外,有一个支撑类AbstractFileSystem,它是FileSystem的子类,用来实现FileSystem类。因为它是一个支撑类,所以它必须是一个具体的类或者至少有一个工厂方法,但是实际上它提供了五个接口(Info, Change,List,Transfer)。这五个接口并没有在FileSystem这个客户API中暴露出来。FileSystem API的用户可以自己实现FileSystem。事实上很多时候都是这样做的,而且还可以使用多继承。因为AbstractFileSystem实现了FileSystem这个客户API,所以任何子类化了FileSystem的用户都可以放心:他们不光实现了FileSystem,也实现了FileSystem。

 

CloneableEditorSupport

   支撑类可以作为接口吗?很难。如果实现了支撑类的所有的方法,那它将会变成怎么样啊!所以,抽象类经常作为支撑类的父类。

   但是应该小心地把支撑类和真正的API(比如CloneableEditorSupport类就和它所实现的EditorCookie类不在同一个包中)。这样的隔离可以保证基本的设计质量,而且可以防止欺诈 —— 即便是在实现代码中也只能使用API的方法,而不能hook非公有类型的方法。

 

接口还是抽象类?(Interface or Classes)

    接口和抽象类哪个更好一些?很难给出一个绝对的答案。但是如果回溯到这个问题的根源上,我们会得到比较好的答案。

    首先,只有那些在设计API的人才会考虑这个问题,那些只是纯粹做开发的人没有必要考虑这个问题,他们可以根据他们的喜好来决定选择哪一个。

    其次,如果你不关心API用户的话,那就没有必要在这个问题上伤脑筋。

从以上两个方面可以看出:对于客户API用抽象类要好一些;而对于服务提供者API来说,用接口要好一些。如果使用该API的用户仅仅只是调用它的话,那么最好就用抽象类;如果仅仅只想让用户调用它的子类的话,那么最好用接口,这样当子类化的时候,使用该API起来比较安全,简单。如果你面临的情况介于以上两者之间的话(根据“将Client API 与 Provider API(SPI) 分离”那个章节所说的,这种情况是禁止的),那么最后的抉择取决于你,但是你在下最后的决定之前,要仔细判断考量哪些是用户经常会用到的 —— 仅仅只是调用一下还是需要子类化。这样的话,你的选择才是恰当的。

 

 

 

将Client API 与 SPI 分离的学习示例(Case Study of client API and SPI seperation)

    前面CloneableEditorSupport的例子表明:如果不用抽象类的话,很难实现支撑类。但是事实上并不是很复杂,而且可以把SPI与客户API分离,即便是将来进行扩展也会很安全,很容易。重写了CloneableEditorSupport的开发团队就是使用接口来实现的:

    CloneableEditorSupport的主要目标是实现像OpenCookie,EditCookie和EditorCookie这样的接口,而让子类去实现像String messageName(),String messageModified()和String messageOpen()这样的抽象方法。为了实现这些抽象方法,子类可以调用一些像protected final UndoRedo.Manager getUndoRedo()这样的支撑方法,并且可以使用像protected Task reloadDocument()这样的方法来与父类的实现进行交互。以上整个过程已经很复杂了,但是以下的事实会让其变得更加复杂:几乎所有的方法都可以在子类中被覆盖(overriden)。这使得局面变得很混乱,而且将来几乎没有办法再对其进行扩展了。

 

把protected类型的方法移到接口里面(Move Protected Methods Into Interface)

    如果把所有会在子类中被覆盖的方法隔离出来,放到一个单独的接口里面的话,情况会变得简单一些:

 

public interface CloneableEditorProvider {

   // methods that have to be overridden

   // in order for the functionality to work

   public String messageName();

   public String messageSave();

  

  

   // additional stuff described below

}

 

    再提供一个工厂方法EditorCookie EditorFactory.createEditor(CloneableEditorProvider p);

该工厂方法可以把服务提供者接口转换成所想要的客户API(这种处理很简单,不然的话,真正的API必须通过一个参数Class[]来支持多种Cookie的创建,如:OpneCookie,EditorCookie等等,这个Class[]参数用来为不同的Cookie指定不同的返回值)。从功能上讲,这相当于提供了一个包含所有应该在子类中实现的方法的类,而且它还确保任何人都不能通过把EditorCookie转换成CloneableEditorProvider来调用一些特殊的方法,因为createEditor方法必须返回一个新的对象,来提供它的功能。

 

发通知给实现方(Passing Notifications to Implementation)

    但是目前还不能完全模拟老版本的CloneableEditorSupport的功能 —— 不能调用reloadDocument或者任何相似功能的方法。为了说明这一点,我们增强了CloneableEditorProvider接口:

 

public interface CloneableEditorProvider {

   // the getter methods as in previous example

   public String messageSave();

 

   // the support for listeners

   public void addChangeListener(ChangeListener l) throws TooManyListenersException;

       public void removeChangeListener(ChangeListener l);

}

    现在,工厂方法不仅可以创建EditorCookie对象,还提供了监听器。因为最多只能有一个监听器,所以addChangeListener方法有抛出TooManyListenersException的签名。通常该方法用如下的简单方式来实现:

 

private ChangeListener listener;

public void addChangeListener(ChangeListener l)

       throws TooManyListenersException {

   if(listener != null) throw new ToomanyListenersException();

   listener = l;

}

 

    如果遵循JavaBeans规范的话,就没有必要为多个监听器的支持伤脑筋。无论什么时候需要重新加载文档,都可以激活listener.startChanged(ev),这样的话,监听的实现方就会知道有文档重新加载的请求来了。

 

实现方的回调方法(Callbacks to Implementation)

    监听器方法支持服务提供者到其实现的单向通信,但是仍然不够完美 —— 不能通过CloneableEditorSupport.getUndoRedo来得到UndoRedo。为了支持这种功能,我们不得不对CloneableEditorProvider再做一次修改:

 

public interface CloneableEditorProvider {

   // the getter methods as in previous example

   public String messageSave();

  

   // the support callbacks

   public void attach(Impl impl) throws ToomanyListenersException;

 

   // the class with methods for communication with the implementation

   public static final class Impl extends Object {

      public void reloadDocument();

      public UndoRedo getUndoRedo();

   }

}

 

    我们用一个专门的Impl类代替了之前的监听器。该Impl类包含了服务提供者可以调用的所有方法,此外新增加的attach方法用来注册Impl。

请注意:Impl类是声明为final类型的,任何从CloneableEditorProvider接口的实现方调用的方法都是CloneableEditorProvider接口里面的方法。从服务提供者到工厂的反向通信被独立出来放在CloneableEditorProvider.Impl类中。现在的CloneableEditorSupport,乍眼看来比之前的CloneableEditorSupport复杂很多,但是代码关系显得清晰多了。

 

可扩展的客户行为(Extensible Client Behaviour)

    可以给EditorCookie增加新的方法或者功能吗?当然可以,扩展EditorFactory就可以了。可以给客户请求做日志吗?可以, EditorFactory是实现这种功能的好地方。可以提供一些同步访问和死锁等等保护吗?在EditorFactory里实现这些功能是最佳选择。

 

服务提供者与其实现之间的可扩展性交互(Extensible Communication between provider and implementation)

   因为CloneableEditorProvider声明为final类型,所以我们可以给它增加一个新方法,例如:

  

   public static final class CloneableEditorProvider.Impl extends Object {

   public void reloadDocument();

   public UndoRedo getUndoRedo();

   public void closeDocument();

}

 

    事实上,Impl类可以看作是CloneableEditorProvider的客户API,这也是为什么最好把Impl设计成类的原因。

 

可扩展的服务提供者的进化(Extensible Provider Evolution)

    一般说来,如果CloneableEditorProvider升级了的话,EditorCookie的功能也会相应得到扩展。在最早的CloneableEditorSupport的例子里,可以增加一个新方法(protected类型的方法),该方法在CloneableEditorSupport里有一个缺省实现,但是增加一个新方法通常是很危险的(可能会使之前的程序崩溃)。在这个例子中,我们定义:

 

Interface CloneableEditorProvider2 extends CloneableEditorProvider {

    /** Will be called when the document is about to be closed by user */

    public Boolean canClose();

}

 

    此外,有可能再定义一个新的工厂方法(之所以说“有可能”是因为之前的工厂方法有可能已经够用了):

   

EditorCookie EditorFactory.createEditor(CloneableEditorProvider2 p);

 

    以上的这些做法可以提供一个新的接口来更好地实现Editor,同时可以为客户API保持相同的接口。

    再举一个这种类型的进化的经典例子:如果老版本的服务提供者接口完全错了,在新版本中修正了它,或者完全写了一个新接口:

 

Interface PaintProvider {

   public void piantImage(Image image);

}

 

/** Based on a ability to paint creates new EditorCookie */

EditorCookie EditorFactory.createEditor(PaintProvider p);

 

    尽管服务提供者API完全改变了,但是这些改变在工厂方法外不可见。工厂方法在客户API与新的服务提供者接口之间充当了翻译的角色。这样的做法使得进化的时候不会产生老程序崩溃的情况。真正想提供CloneableEditorProvider功能的服务提供者,可以通过直接实现CloneableEditorProvider接口来达到目的;想处理closeDocument调用的服务提供者,可以通过实现CloneableEditorProvider2接口来达到目的;而那些依赖全新绘图风格的服务提供者,可以通过实现PaintProvider来达到目的。每个上述这样的服务提供者都要显式指定它想实现哪个SPI接口,这比直接在CloneableEditorSupport里添加新方法要显得清晰得多。

 

 

玩NetBeans核心开发团队开发的游戏来提高API的设计水平(Using games to Improve API Design Skills)

    具备优秀的API设计素质对于那些致力于开发像NetBeans这样的开源框架的开发者非常重要。阅读和学习一些API设计大纲是很有帮助的,但是比起单纯学习,在模拟情景中进行设计实践要有效的多。情阅读一下有关API Fest的文章,来了解一下API Fest游戏。该游戏是由NetBeans核心开发团队开发出来的,玩该游戏可以提高API的设计水平。

 

如何设计一款优秀的API

随着近来软件规模的日益庞大,API编程接口的设计变的越来越重要。良好的接口设计可以降低系统各部分之间的相互依赖,提高组成单元的内聚性,降低组成单元间的耦合度,从而提高系统的维护性和稳定性。 Jo...
  • xtjsxtj
  • xtjsxtj
  • 2014年03月03日 14:28
  • 1087

10款优秀的产品包装设计欣赏!

何为优秀,第一要与众不同,第二历久弥新、百看不腻。下面就一同来欣赏下10款优秀的产品包装设计吧!...
  • haha_asia
  • haha_asia
  • 2017年07月28日 05:01
  • 680

逐步改善,设计优秀API

判断一个API是否优秀,并不是简单地根据第一个版本给出判断的,而是要看多年后,该API是否还能 存在,是否仍旧保持得不错。 判断一个API是否优秀,并不是简单地根据第一个版本给出判断的,而是要...
  • jeson1224
  • jeson1224
  • 2013年01月23日 15:56
  • 383

如何设计优秀的 API

API的设计是编程中最困难的事情。甚至有人认为,哪怕你已经有着十年的相关经验,也仅仅只能接触尝试API的设计。我们也曾经或多或少的为了那些缺乏经验的程序员所设计的一些API吃了苦头。然而,如果你能在这...
  • coderabbit
  • coderabbit
  • 2013年04月04日 16:23
  • 303

如何设计一个优秀的API

到目前为止,已经负责API接近两年了,这两年中发现现有的API存在的问题越来越多,但很多API一旦发布后就不再能修改了,即时升级和维护是必须的。一旦API发生变化,就可能对相关的调用者带来巨大的代价,...
  • Enbiting
  • Enbiting
  • 2013年07月05日 13:25
  • 305

如何设计一个优秀的API

到目前为止,已经负责API接近两年了,这两年中发现现有的API存在的问题越来越多,但很多API一旦发布后就不再能修改了,即时升级和维护是必须的。一旦API发生变化,就可能对相关的调用者带来巨大的代价,...
  • qingxinyeren
  • qingxinyeren
  • 2016年08月16日 08:14
  • 143

逐步改善,设计优秀的API

文/ Jaroslav Tulach   判断一个API是否优秀,并不是简单地根据第一个版本给出判断的,而是要看多年后,该API是否还能存在,是否仍旧保持得不错。   第一个版本远非完美   第...
  • coderabbit
  • coderabbit
  • 2013年04月04日 16:29
  • 447

什么是优秀的用户体验:解读40个优秀界面设计

1 尽量使用单列而不是多列布局   单列布局能够让对全局有更好的掌控。同时用户也可以一目了然内容。而多列而已则会有分散用户注意力的风险使你的主旨无法很好表达。最好的做法是用一个有逻辑的叙述来...
  • ifyourmm
  • ifyourmm
  • 2016年08月03日 15:13
  • 909

《触动人心—设计优秀的iPhone应用》读书笔记(一)

图书简介:《触动人心—设计优秀的iPhone应用》是一本诠释如何进行iPhone应用设计的书。书中既介绍了设计中需要注意的细节,也包含用户心理、人类工程学等多方面的知识。非常适合计划做iPhone应用...
  • circlepig
  • circlepig
  • 2013年02月03日 16:13
  • 3761

找不到交互设计实例?看这里!

现在很多优秀的网站、移动应用中都广泛的使用到交互设计。网络上也有很多相关的文章,例如什么是交互设计,交互设计的流程是怎样的?但是设计从来不是空谈,理论和实际的碰撞,才能激发出更多的灵感。以下是我自己总...
  • jongde1
  • jongde1
  • 2017年04月19日 14:42
  • 646
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:如何设计优秀的API(3)
举报原因:
原因补充:

(最多只允许输入30个字)