程序的库设计
最近在Stack Exchange上面看到一个帖子,是问程序库设计的指导原则的,“What guidelines should I follow while designing a library?”,有趣的是,很多人都在谈论面向设计,各路API设计,还有程序语言设计,唯独搜索“程序库设计”,无论中文还是英文,Google还是百度都找不到太多内容。但是我想,没有程序员会否认库设计的重要性吧,我想在这里结合这个帖子谈谈我的想法。
在这个帖子里面,votes最高的回答,提到了这样几类tips,我在下面简要叙述一下,其中基础的部分包括:
1. Pin Map,明确你期望库主要用来做什么,但不要把它定得太死,用户要可以比较方便地做出改变。
2. Working Library,一个工作的库,如果它连这点都达不到,一定要注明。没有人希望浪费时间在一个无法工作的程序库上面。
3. Basic Readme,清晰地描述库是用来做什么的,测试的情况等等。
4. Interfaces,接口必须清晰地定义,这可以帮助库的使用者。
5. Special Functions,特殊的功能,一定要注明,包括在readme文档中注明,以及在注释中注明。
6. Busy Waits,如果有一些场景需要使用busy wait(我不知道怎么翻译),其过程中可能会出现异常,使用interrupt或者其它妥善的方法来处理。
7. Comments,你做的任何的改变都要注释清楚,明确描述接口和其每个参数,方法是做什么的,又返回什么;如果有某个中间方法被调用到,就要注明。
8. 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参考使用文档。