最近在 Stack Exchange 上面看到一个帖子,是问程序库设计的指导原则的,“What guidelines should I follow while designing a library?”,有趣的是,很多人都在谈论面向设计,各路 API 设计,还有程序语言设计,唯独搜索“程序库设计”,无论中文还是英文,Google 还是百度都找不到太多内容。但是我想,没有程序员会否认库设计的重要性吧,我想在这里结合这个帖子谈谈我的想法。
在这个帖子里面,votes 最高的回答,提到了这样几类 tips,我在下面简要叙述一下,其中基础的部分包括:
其中的高级部分又包括 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 参考使用文档。