前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何设计优秀的API(三)

如何设计优秀的API(三)

作者头像
用户5224393
发布2019-06-05 14:35:01
5940
发布2019-06-05 14:35:01
举报
文章被收录于专栏:Java研发军团Java研发军团

阅读本文需要5分钟

引言

此文章翻译来自国外的一本叫做《How to design API》的书籍,如果还没有没有看过前两张的朋友可以先看看前两章文章,如何设计优秀的API(一)如何设计优秀的API(二)

接口 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)给调用者:

代码语言:javascript
复制
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的用户都必须如下编码:

代码语言:javascript
复制
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增加新方法的时候是多么简单啊:

代码语言:javascript
复制
Boolean isInstanceOf(Class c) {
   return c.isAssighnableFrom(instanceClass());
}

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

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

代码语言:javascript
复制
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)

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

代码语言:javascript
复制
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接口:

代码语言:javascript
复制
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的签名。通常该方法用如下的简单方式来实现:

代码语言:javascript
复制
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再做一次修改:

代码语言:javascript
复制
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类型,所以我们可以给它增加一个新方法,例如:

代码语言:javascript
复制
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里有一个缺省实现,但是增加一个新方法通常是很危险的(可能会使之前的程序崩溃)。

在这个例子中,我们定义:

代码语言:javascript
复制
Interface CloneableEditorProvider2 extends CloneableEditorProvider {
    /** Will be called when the document is about to be closed by user */
    public Boolean canClose();
}

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

代码语言:javascript
复制
EditorCookie EditorFactory.createEditor(CloneableEditorProvider2 p);

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

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

代码语言:javascript
复制
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的设计水平。

END

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-03-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java研发军团 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 接口 vs. 抽象类(Interfaces vs. Abstract Classes)
  • 使用接口的好处(The Advantages of Interfaces)
  • 使用抽象类的好处(The Advantages of Abstract Classes)
  • 用例(Use cases)
  • TopManager
  • Cookies
  • 文件对象(FileObject)
  • CloneableEditorSupport
  • 接口还是抽象类?(Interface or Classes)
  • 将Client API 与 SPI 分离的学习示例(Case Study of client API and SPI seperation)
  • 发通知给实现方(Passing Notifications to Implementation)
  • 实现方的回调方法(Callbacks to Implementation)
  • 可扩展的客户行为(Extensible Client Behaviour)
  • 可扩展的服务提供者的进化(Extensible Provider Evolution)
  • 玩NetBeans核心开发团队开发的游戏来提高API的设计水平(Using games to Improve API Design Skills)
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档