如何设计一个好的api

来源:互联网 发布:java转义字符\\ 编辑:程序博客网 时间:2024/04/28 12:50

原文:How to design a good API

翻译时间:31-08-2007

译者:周林

( 欢迎转载,转载请注明出处^_^)

 

摘要:

    本文以NetBeans架构下API的设计为背景,描述了如何设计优秀的API(Application Programming Interface)。

 

 

 

目录:

1. 为何要有API?

2. 什么是API?

3. 面向用例(Use Case Oriented)的重要性

4. API的生命周期

5. 投资保值(Preversation of Investments)

6. 设计实践:

   1. 不要暴露过度(Do not expose more than you want)

       . 方法(Method)优于字段(Field)

       . 工厂(Factory)优于构造器(Constructor)

       . 所有的API应该定义为Final属性

       . 只赋予友元代码(friend code)访问权限

    2. 将Client API 与 Provider API(SPI) 分离

    3. 组件间的查找与通信

    4. 接口(Interface) vs. 抽象类(Abstract Class)

    5. 将Client API 与 SPI 分离的学习示例

    6. 玩NetBeans核心开发团队开发的游戏来提高API的设计水平

 

 

 

为何要有API

    API的全称是应用程序编程接口(Application Programming Interface).在描述和建议如何实现API之前,没有理由不分析一下这个名字的意义。

    “接口”(interface)这个词表明API介于至少两个客体之间。举个例子来说,某个应用程序内部的数据结构对该应用程序是透明的,而其他的应用程序只能通过外部调用间接使用该数据结构。再举个例子,某个程序员或者开发团队开发了一个应用程序以及相应的API,而其他的程序员使用这些API。我们可以看出,在这两个例子中都存在彼此独立的双方——一方独立地进行编译,一方由完全不同的开发团队,根据他们自己的进度安排,目标和需要,进行开发。

    正是“分离”(separation)一词精确地暗示了设计和维护API的准则。如果不采用“分离”的思想,整个产品由一个高度耦合的团队进行开发,一次build, 那么就没有必要引入API和撰写本文了。但是在现实世界中,产品是由彼此独立的工程(Project)组合起来的,每个工程由不同的团队来开发,他们没有必要彼此认识。虽然他们有完全不同的进度安排,独立地build各自的工程,但是他们可以相互交流。Stable Contract就是用来达到这种交流的一种手段。

  例子:虽然Mandrake和Redhat是Linux版本的生产商,但是这些Linux版本实际上是由成千上万个的独立的开源工程组成的。这些版本的生产商并不干预这些开源工程的开发者的开发工作,仅仅在给定的时间,提取这些工程中稳定可用的部分,整合后生成发行版本。

 

 

 

什么是API

    由于API允许在开发团队和应用程序之间进行交互,它使开发过程变成了分离的,分布式的活动,所以要解释什么是API就要涵盖影响这种活动的方方面面。

 

   . 方法和字段的签名(method and field signatures)

应用程序之间的相互通常是通过如下的方式展现的:函数调用以及数据结构的传递。如果方法的名字,参数,或者交互用的数据结构改变了,那么整个项目通常不能够链接成功,正确运行。

 

   . 字段及其内容(fields and their content)

许多应用程序都要读取各种各样的文件。它们的内容会影响它们的行为。设想一个应用程序,在调用它之前,需要有另一个程序来读取它的配置文件并以此来修改它的内容。如果文件格式改变了或者文件被完全忽略了,那么这两个应用程序之间的交互就断开了。

 

   . 环境变量(enviroment varibals)

例如,CVS会受变量CVSEDITOR的影响。

 

   . 协议(protocols)

为如下的操作建立API给其他应用程序使用:打开一个Socket用来解析数据流;把读取的数据放入剪贴板中;拖放操作……

 

   . 行为(behaviour)

有点难掌握但是对于“分离”非常重要的一点是动态行为:程序流如何,执行序列是怎样的,哪些锁在调用期间要保持,在哪些线程里调用可以发生,等等。

 

   . L10N 消息(L10N message)

     通常进行本地化成某种语言工作的人并不是代码的作者,所以他们必须使用同样的关键词(NbBundle.getMessage ("CTL_SomeKey"))。代码的作者和翻译者之间应该达成契约(对API进行排序)。

  

特别要注意某些API,它们和分布式开发活动有关,其他代码可能依赖它们。只有认识了自己应用程序的这些方面,开发才不会影响到参与合作的其他的应用程序。

 

 

 

面向用例的重要性

    很多时候不难评判一个程序是好程序还是坏程序——如果它没有做任何有用的事情就崩溃了,那么它就是一个坏程序。如果程序不能编译,那么更糟。但是如果它可以运行,也可以完成预定的工作,只是有时候会崩溃,那么不能说它是一个好程序,而只能说它算不上是一个坏程序,最终好坏与否这取决于评估者的感觉。和主观感受有关系。对于设计的评判同样如此,无论是UI的设计还是API的设计。

    另一方面,工程(至少应该)由工程师来完成,工程中很重要的一个方面是可度量性(measurability)。所以设计的最终目标是使其可度量,排除主观上的东西,定义出可以度量设计质量的需求集合来。当然,定义需求集合的时候需要主观意见,但是一旦需求被文档化,工程师就是纯粹的工程师了,用纯粹的科学方法来度量哪些需求可以被满足。

    正如上面好程序/坏程序的例子所展示的那样,用户的主观感受是很重要的。对于设计也是如此。但是对于API来说,由于它是应用程序内部实现与该应用程序功能使用者之间的接口,所以这种主观感受来自使用API的程序员。他会评判设计好坏与否。当然,这种评判因人而异,这取决于学习设计与使用API期间获得的经验。

越能让API的使用者减少所需要的工作量,这样的设计越能得到高的评价。程序员更多关注的是学习API的时间,完成工作所需要的代码量以及API的稳定性。要设计好的API就要平衡这些相互矛盾的需求。

    通常为了赢得更多的使用者,更好地提高使用者的开发效率,要对API的设计进行优化。一般说来,API使用者的数目远远大于API实现者的数目,如果能简化API使用者开发的话,即便是API的实现复杂一点也可以接受的。为了更好地表达使用者的需要,理解使用者的需求是很有必要的。如果设计出来的API能简化普通任务的实现,那么它就是一个好的API。

    这就是为什么在API设计的初期阶段要调查和收集用例的原因。一旦这些用例文档化了,就可以对API的每个方面进行评估,确认设计。虽然用例在实际中不可能用来评判设计质量,但是至少可以很容易地检查设计有没有满足这些用例。

   一个用例一旦被支持,就应该一直被支持下去。

 

 

API的生命周期

   API的形成似乎有两种方式:一种是自然形成的(spontaneously),另一种是人为设计的(by design)。

 

. 自然形成的(spontaneously) —— 某人开发了一种功能,另一个人发觉这种功能很有用并且开始使用它。之后他们开始交流,共享他们的经验,而且很有可能发现该功能之前的设计并不是十分通用,或者说还不至于形成真正的API。为了让该功能的设计向API演进,他们开始讨论如何把该功能做得更好。几次改进之后,便会形成一种有用的,稳定的版本。

 

 

. 人为设计的(by design) —— 在系统的两个组件之间存在某种已知的契约。经过需求采集,问题域调研,用例理解之后,某人开始着手设计和实现API。最终该API会形成一种有用的,稳定的版本。

       尽管上述两种情况的出发点不同,但是它们有共同的一个特性:在API正式开始被用户使用之前,它们都需要一段时间接受反馈和评估。并不是所有的努力都会以诞生稳定的API为回报;有时最终不得不放弃之前所作的所有努力。

      为了清楚地知道API的设计处于哪个阶段,它是否还在发展,它是否可以最终成为一个真正的API,以及它是否很稳定可被使用,让我们引入一个稳定性分级系统 (a system of stability classification)。该系统的目标是:提供一个让API实现的代码作者与需要该API功能的用户之间进行交流的途径。

 

 

. 私有性(Private) —— 私有性是一种在其组件外部不可访问的属性。在新版本中对这些属性进行修改是有一定风险的,应该尽量避免。

 

 

. 友元(Friend) API —— 这种API是为系统中某些指定组件之间的访问服务的。它可以用来解决缺乏真正稳定的API的问题。但是它仅仅只可以用在那些互为友元的组件之间。友元组件常常由同一个开发团队的人来开发。虽然每个发布版本中组件组件之间的友元关系可以改变,但是必须提前通知这些友元组件的宿主(owners of those friend components)。系统中的其他非友元组件并不依赖该API —— 该API的开发者并没有打算让它成为一个全局通用的API。

 

 

. 开发(Under development) —— 这里“开发”的意思是:正在为实现一个稳定的API而努力,但是还没有完成。当前状态是已经有了大体概念,大家开始着手进行开发工作,并通过邮件列表(mail list)进行联系。版本更迭允许非兼容的更改,但是应该尽量少,并且这样的更改应该是非基础性的,除此之外,还应该通过邮件进行公开声明。

 

 

. 稳定的(Stable)API —— 是指那些已经完成而且维护人员打算永久支持,决不进行非兼容性更改的API。“永久”和“决不”不是绝对意义上的;这些API可能会有更改,但是只会在某些主要版本上进行,并且这些更改必须是经过深思熟虑,不得不改的。

 

 

. 官方的(Official)API ——是指那些已经稳定的,并且被包装进NetBeans的一个官方命名空间(如:org.netbeans.api, org.netbeans.spi 或者 org.openide)的API。把一个API放进一个包中,必须向其他包声明该API是稳定的 —— 随后也是稳定的(不包括对早期的7个模块的有条件支持,这7个模块对应的代码库的名字都以/0结尾)。此外,尽量减少对官方API非兼容性的更改,即便是源代码级不兼容了,也要保持二进制级别上的兼容。

 

 

. 第三方(Third party)接口 —— 它们是由不遵循NetBeans规则的其他组织开发的,因此很难对它们进行分类。为了不让NetBeans API的用户受到这些接口的改变带来的非预期的影响,最好不要把它们作为NetBeans标准API的一部分。

 

 

. 标准(Standard) —— 是和上面“第三方”相似的一个概念。也是由NetBeans之外的人提供的。但是它与NetBeans相兼容(例如JSRs)。人们不希望“标准”经常性地被更改。

 

 

. 过时的(Deprecate


设计实践 (Design Practices)

    现在我们来谈谈Java的设计实践与设计模式,这两者有助于开发者和维护者的工作符合前几个章节所提到的准则,用户体验佳。

 

 

不要暴露过度 (Do not expose more than we want)

    显而易见,API暴露的内部实现越少,将来的弹性就更好。有不少窍门可以用来隐藏内部实现,但是不影响到其API的功能。这一节我们就来谈谈这些窍门。

 

方法优于字段 (Method is better than Field)

    最好用方法(getters 和 setters)来访问字段,而不要直接暴露字段。这样做的原因之一是:调用方法可以做很多额外的事情,比如限制字段为只读或者只写。使用getters,可以进行例行的初始化,同步访问,以及利用某种算法对数值进行组织。另一方面,setters可以对字段的赋值正确与否进行检查,还可以在字段的数值改变时通知相应的监听器。

    使用方法的另一个原因在于Java虚拟机规范。该规范允许将一个方法从子类移到父类中而不破坏二进制级别上的兼容性。因此,一个最初像如下形式引入的方法在新版本中可以被删除:

 

    Dimension javax.swing.JComponent.getPreferredSize (Dimension d)

 

在新版本中,它被移到

      

    Dimension java.awt.Component.getPreferredSize (Dimension d)

 

JComponent 是 Component 的子类(以上真实发生在JDK 1.2版本中)。但是类似的操作对字段是禁止的。一旦在一个类中定义了某个字段,该字段就永远不应该被挪动位置,以保证二进制级别上的兼容性。这也是最好把字段定义为私有属性的原因。

 

工厂优于构造器 (Factory is better than Constructor)

    导出工厂方法比导出构造器更有弹性。一旦构造器作为API的一部分,那么它可以保证生成的实例是而且仅仅是对应类的实例,而不是其子类的实例。另外,每次调用构造器的时候都会生成一个新的实例。与之相对应的工厂方法 (通常工厂方法实现成一个静态方法,该方法的参数与构造器的一模一样,也返回构造器所在的类的实例) 有诸多不同:首先,工厂方法并不是简单返回指定的类的实例,而是使用了多态 (polymophism),另一个优势在于工厂方法可以缓存实例。构造器每次都生成新的实例,而工厂方法可以缓存之前生成的实例来进行重用,这样可以节省内存。另一个原因是:调用工厂方法可以进行合适的同步,而构造器不能。以上这些便是选择工厂方法要优于构造器的原因。

 

所有的API都应该定义为Final属性 (Make Everything Final)

    很多情况下,人们都没有考虑过子类化 (subclassing) 的问题,在设计时也没有进行保护。如果你在开发一个API,但是你不希望别人进行子类化你的接口 (可以参考 API vs. SPI 一节),那么最好显式禁止子类化。

    最简单的办法是把你的类声明成Final类型的。其他的办法包括:把构造器声明为非公有类型的 (对应的工厂方法也应该这样处理),或者把所有 (至少大多数的) 方法声明为Final或者私有类型的。

    当然这样做只在类级别上有效,如果你开发的是接口,那么就不能阻止在虚拟机级别上对该接口进行外部实现,你只能要求制定Java规范的人不要这样做。

 

只赋予友元代码(friend code)访问权限

    另一个可以防止“暴露过度”的很有用的技术是:只给友元代码以访问特定功能的权限 (例如,实例化某个类或者调用某个方法)。

默认情况下,Java要求互为友元的类必须在同一个包中。如果你想把某个功能共享给同一个包中的其他类,马么你可以给构造器,字段或者方法加上package-private修饰符,这样的话,只有友元可以进行访问。

但是有的时候,更有用的方法是将友元集合扩展到更广的类范围中 —— 比如,有人把API的纯定义放在一个包中,而其实现放在另一个包中。这种情况下,下面的方法非常有用。假设有一个类item (顺便说一下,你可以直接从CVS上check out源代码):

 

public final class api.Item {

       /** Friend only constructor */

       Item (int value) {

              this.value = value;

       }

 

       /** API methods (s) */

       public int getValue () {

          return value;

       }

 

       /** Friend only method */

       final void addListener (Listener l) {

             // some impl

       }

}

 

以上只是item的部分代码,但是已经可以防止友元(这些友元类不仅仅只在 api 包中)之外的类对其进行实例化或者监听事件了。接下来的代码在非api包中定义了一个Accessor:

 

public abstract class impl.Accessor {

   public static Accessor DEFAULT;            

            

   static {

      // invokes static initializer of Item.class

      // that will assign value to the DEFAULT field above

      Class c =  api.Item.class;

      try {

         Class.forName (c.getName (), true, c.getClassLoader ());

      } catch (ClassNotFoundException ex) {

               assert false : ex;

      }

     

      assert DEFAULT != null : “The DEFAULT field must be initialized”;

}

 

/**  Accessor to constructor Item */

public abstract Item newItem (int value);

/** Accessor to listener */

public abstract void addListener (Item item, Listener l);

}

 

上面的抽象方法用来访问Item类的友元功能,静态字段用来得到Accessor的实例。Accessor的具体实现是通过api包中的一个非公有的类来实现的:

 

final class api.AccessorImpl extends impl.Accessor {

     public Item newItem (int value) {

       return new Item (value);

     }

 

     public void addListener (Item item, Listener l) {

        item.addListener (l);

     }

}

 

    为Item类添加一个静态的初始化器 (initializer),这个初始化器为首次接触api.Item的人的注册了一个默认的实例:

public final class Item {

       static {

                impl.Accessor.DEFAULT = new api.AccessorImpl ();

       }

 

      // the rest of the Item class as shown above

}

 

    现在友元代码就可以从任意一个包,利用Accessor来调用隐藏的功能了:

 

Api.Item item = impl.Accessor.DEFAULT.newItem (10);

Impl.Accessor.DEFAULT.addListener (item, this);

 

    请注意:在NetBeans中有一个很有用的做法:把指定的具有公有访问权限的包全部列在模块清单 (module manifest) 里 (OpenIDE-Module-Public-Packages: api.**)。这样做的话,可以在类加载的级别上,阻止来自impl.Accessor之外的访问。

 

 

将Client API 与 Provider API(SPI) 分离 (Separate API for clients from support API)

    API的种类是否不止一种?如果是这样的话,如何对它进行分类?是否也要对API的使用者进行分类?他们是不是有不同的目标?本章的第一节将回答以上这些问题。然后我们将定义进化不同类型的API的时候所要遵循的约束,除此之外,我们还会介绍一些帮助用户遵循这些约束的窍门和知识。

 

Client API vs. Provider API

    在正式开始之前,我们应该问一个问题:谁是客户(Client),谁是服务提供者(provider)?让我们用XMMS的例子来说明。XMMS是Unix平台上的一款多媒体播放器(在其它平台上叫做Winamp)。

该播放器可以播放音频文件,在前后歌曲之间快进,还提供了一个可以增加,删除和录制歌曲的播放列表。不光普通用户可以直接使用该播放器的功能,其他的程序也可以对其功能进行访问。所以一个外部程序可以调用xmms.pause()或者xmms.addToPlaylist(filename)。在这种情况下,交互是由调用播放器API的外部程序发起的,该程序调用这些API来完成某些操作。调用结束后,控制权返回给调用者。我们把调用者称为“客户”, 而被调用的API称为“客户API”(Client API)。

另一方面,XMMS API支持第三方的插件(output plugins)。通过这种方式,可以提供一个方法对播放器的功能进行扩展:把播放过的数据写进磁盘,网络广播,等等。在这种情况下,交互是由播放器自身发起的。在收集到了足够用来回放的数据之后,程序将定位对应的插件,把数据发送给它进行处理:plugin.playback(data)。插件在完成了回放操作之后,把控制权返回给播放器,播放器继续收集数据,进行后续的操作。那么插件是个“客户”吗?它完全不同于上一段中提到的“客户”的概念。它并没有指示XMMS做任何事情,而是增强了XMMS的功能。所以插件并不是一个“客户”。XMMS支持插件的功能称为“服务提供者接口”(Service Provider Interface, SPI)。


文章出处:飞诺网(http://www.diybl.com/course/3_program/java/javaxl/20071018/77730.html)

原创粉丝点击