程序的库设计

jopen 10年前

  最近在 Stack Exchange 上面看到一个帖子,是问程序库设计的指导原则的,“What guidelines should I follow while designing a library?”,有趣的是,很多人都在谈论面向设计,各路 API 设计,还有程序语言设计,唯独搜索“程序库设计”,无论中文还是英文,Google 还是百度都找不到太多内容。但是我想,没有程序员会否认库设计的重要性吧,我想在这里结合这个帖子谈谈我的想法。

  在这个帖子里面,votes 最高的回答,提到了这样几类 tips,我在下面简要叙述一下,其中基础的部分包括:

  • Pin Map,明确你期望库主要用来做什么,但不要把它定得太死,用户要可以比较方便地做出改变。
  • Working Library,一个工作的库,如果它连这点都达不到,一定要注明。没有人希望浪费时间在一个无法工作的程序库上面。
  • Basic Readme,清晰地描述库是用来做什么的,测试的情况等等。
  • Interfaces,接口必须清晰地定义,这可以帮助库的使用者。
  • Special Functions,特殊的功能,一定要注明,包括在 readme 文档中注明,以及在注释中注明。
  • Busy Waits,如果有一些场景需要使用 busy wait(我不知道怎么翻译),其过程中可能会出现异常,使用 interrupt 或者其它妥善的方法来处理。
  • Comments,你做的任何的改变都要注释清楚,明确描述接口和其每个参数,方法是做什么的,又返回什么;如果有某个中间方法被调用到,就要注明。
  • Consistency,一致性,所有东西,包括注释。相关的方法要放在一个简单的代码文件里面,小但是逻辑一致。

  其中的高级部分又包括 Detailed Readme,Directory Structure,Licensing 和 Version Control。

  这些都是需要注意的内容,并且大部分对于程序的库设计来说是基础要求,但是这些从重要性来说,并没有说到点上。《C++沉思录》里面有这样一句 话:“库设计就是语言设计,语言设计就是库设计”,二者从先定义问题域到后解决问题的思路是类似的。我觉得比较重要的需要考虑的事情包括:

  考虑库的目标用户。这听起来扯得有点泛,但实际上这是确切的问题,这是开源库还是你只是在小组内部使用的库,或者是公司内部使用的?用户的能力和需求是不一样的,要求当然不同。

  要解决的核心问题。这是上文中 Pin Map 的一部分,不要重复发明轮子,那么每一个新库都有其存在的价值,这个问题既要通用又要具体,“通用”指的是库总有一个普适性,解决的实际问题对于不同的用 户来说是不一样的;而“具体”是指库解决的问题对于程序员来说是非常清晰和直接的。例如设计一个库,根据某种规则把不同的数据类型(XML,BSON 或者某种基于行的文本等等)都转换成 JSON。

  统一的编程风格。很多库都有自己精心设计的一套 DSL,比如链式调用等等几种方式,当然,这也和使用的语言有关系。定义一种用起来舒服的编程风格对于程序库的推广是很有好处的。这也是一致性的一个体现。

  内聚的调用入口。这和面向对象的“最少知识原则”有类似的地方,把那些不该暴露出去的库内部实现信息隐藏起来,在很多情况下,程序库不得不暴露和要求用户了解一些知识的时候,比如:

MappingConfig config = new MappingConfig ();  config.put (MappingConfigConstants.ENCODE, "UTF-8");  FileBuilder fileBuilder = new StandardFileBuilder (mapping);  InputStream stream = fileBuilder.build () .getInputStream ();  DataTransformer<XMLNode> transformer = new XMLDataTransformer (...);  ...  transformer.transform (stream);

  这里引入了太多的概念,MappingConfig、FileBuilder、DataTransformer 等等,整个过程大致是构造了一个数据源,还有一个数据转换器,然后这个数据转换器接受这个数据源来转换出最后结果的过程。那么:

  这些象征着概念的接口和类最好以某种易于理解的形式组织起来,比如放在同一层比较浅的包里面,便于寻找;

  也可以建立一个 facade 类,提供几种常用的组合,避免这些繁琐的对象构建和概念理解,例如:

XXFacade.buildCommonXMLTransformer ();

  向后兼容。当然,这一点也可以归纳到前文提到的一致性里面去。我曾经拿 JDKHashTable 举了一个例子,它的 containsValue 和 contains 方法其实是一样的,造成这种情况的一个原因就是为了保持向后兼容。

  依赖管理。依赖管理很多情况下是一个脏活累活,但是却不得不考虑到。通常来说,任何一个库考虑自己的依赖库时都必须慎重,尤其是面对依赖的库需要升级的时候。如果依赖的库出了问题,自己设计的程序库也可能因此连累。

  完善的测试用例。通常来说,程序库都配套有单元测试保证,无论是什么语言写的。

  健全的文档组织。通常包括教程(tutorial)、开发者文档(developer guide)和接口 API 文档(API doc)。前者是帮助上手和建议使用的,中间的这个具备详尽的特性介绍,后者则是传统的 API 参考使用文档。