1. 概述
Java是开源世界的基石之一。几乎每个Java项目都会使用其他开源项目——毕竟没人喜欢重复造轮子。但现实是:我们经常需要某个库的功能,却完全不知道怎么用。常见踩坑场景包括:
- 这些满篇的“*Service”类到底是个什么鬼?
- 这玩意儿怎么实例化?依赖项多到爆炸,"latch"又是什么玩意儿?
- 好不容易拼凑跑起来了,却突然开始抛IllegalStateException,我到底哪里搞错了?
根本问题在于:很多库设计者根本不考虑用户体验。大多数人只堆功能,却很少思考API在实际场景中怎么用,用户代码怎么写、怎么测试。
本文提供几点建议,帮用户避开这些坑——注意,不是靠写文档(当然,这个主题值得写整本书,我也确实见过几本)。这些是我在开发多个库时总结的关键经验。
我会用两个库举例说明:charles 和 jcabi-github
2. 边界设计
这应该是常识,但很多人就是做不到。写代码前先明确几个问题:需要哪些输入?用户首先接触哪个类?需要用户提供哪些实现?输出是什么?这些问题想清楚,库的骨架就立起来了。
2.1. 输入设计
这是最关键的一环。必须让用户清楚知道:库需要什么才能干活?有时很简单(比如API的auth token字符串),但有时可能是接口或抽象类的实现。
✅ 最佳实践:通过构造函数注入依赖,保持参数精简(超过3-4个就该重构了)。
❌ 避坑:别用setter注入强制依赖,否则用户迟早会遇到概述里的第三种崩溃。
多提供几种构造函数,给用户选择权。比如同时支持String和Integer,或者别死绑FileInputStream,改用InputStream——这样单元测试时用户就能传ByteArrayInputStream。
看jcabi-github的Github API入口点实例化方式:
Github noauth = new RtGithub();
Github basicauth = new RtGithub("username", "password");
Github oauth = new RtGithub("token");
简单粗暴,不用初始化那些鬼配置对象。这三个构造函数很合理:对应Github的未登录、密码登录、OAuth三种场景。当然未登录时某些功能不可用,但用户一开始就清楚。
再看charles爬虫库的用法:
WebDriver driver = new FirefoxDriver();
Repository repo = new InMemoryRepository();
String indexPage = "http://www.amihaiemil.com/index.html";
WebCrawl graph = new GraphCrawl(
indexPage, driver, new IgnoredPatterns(), repo
);
graph.crawl();
这段代码还算直观,但有个设计缺陷:所有构造函数都强制要求IgnoredPatterns实例。默认本该不忽略任何模式,用户却必须显式传参——这就是反面教材。用户看到这肯定懵:“IgnoredPatterns又是什么鬼?”
变量说明:
indexPage
:爬虫起始URLdriver
:浏览器驱动(无法默认,不知道用户装了什么浏览器)repo
:存储实现(下节详述)
核心原则:保持构造函数简单直观,把复杂逻辑封装好。别让用户对着你的构造函数挠头。
⚠️ 反面教材:试试用aws-sdk-java调AWS API——你得先搞懂AmazonHttpClient、ClientConfiguration、ExecutionContext这些玩意儿,最后可能还不知道ExecutionContext到底干嘛的。
2.2. 输出设计
主要针对与外部交互的库。核心问题:输出怎么处理? 看似简单,但容易翻车。
再看charles的例子:为什么必须传Repository实现?为什么不让*WebCrawl.crawl()直接返回List
如果这么设计:
WebCrawl graph = new GraphCrawl(...);
List<WebPage> pages = graph.crawl();
灾难!爬取1000个页面?直接OOM。两种解决方案:
- 返回分页数据,用户传起止页码
- 要求用户实现*export(List
)*接口,库在达到阈值时回调
方案2完胜:双方都简单,且更易测试。想想方案1用户得写多少逻辑?现在用户只需指定存储方式(存DB或写磁盘),调用*crawl()*后就不用管了。
核心原则:别把库和用户职责完全割裂。始终考虑输出数据的去向——就像卡车驱动到站后该帮忙卸货,而不是直接把货扔地上。
3. 接口设计
永远用接口。用户只能通过严格契约与你的代码交互。
看jcabi-github的例子:用户实际只接触RtGithub类:
Repo repo = new RtGithub("oauth_token").repos().get(
new Coordinates.Simple("eugenp/tutorials"));
Issue issue = repo.issues()
.create("Example issue", "Created with jcabi-github");
这段代码在eugenp/tutorials仓库创建了工单。虽然用了Repo和Issue实例,但实际类型被隐藏了。用户不能直接这么写:
Repo repo = new RtRepo(...)
这有逻辑原因:你能直接在Github仓库创建工单吗?得先登录→找仓库→才能创建。当然技术上允许,但用户代码会充斥样板:RtRepo得传授权对象→认证→定位仓库...
接口优势:
- ✅ 易扩展:用户能装饰接口或写替代实现
- ✅ 向后兼容:开发者必须遵守已发布的契约
- ✅ 架构控制:强制规则的同时给用户改造空间
核心原则:尽可能抽象封装。用接口优雅地实现这点——让用户既能扩展行为,又不会破坏设计。
记住:你的库你做主。必须清楚用户代码长什么样、怎么测试。如果连你都不知道,别人更懵,最终只会催生一堆难以维护的代码。
4. 第三方依赖
好库必须是轻量库。你的代码再牛,如果jar包让用户构建增加10MB,说明项目早就跑偏了。依赖太多?可能功能太杂,该拆成多个小项目。
尽可能透明,别绑定具体实现。经典案例:用SLF4J(日志API)而非直接用log4j——用户可能想用其他日志框架。
⚠️ 注意传递依赖,尤其别引入xalan或xml-apis这类危险依赖(为什么危险?本文不展开)。
底线:保持构建轻量透明,清楚每个依赖的用途。这能帮用户避免想象不到的麻烦。
5. 总结
本文提供几个关键点,帮项目保持可用性。库作为大系统中的组件,既要功能强大,又要提供流畅精良的接口。
设计越界很容易把事情搞砸。贡献者当然会用,但新手可能一脸懵逼。生产力至上——遵循这个原则,用户应该能在几分钟内上手你的库。