本篇要回答的问题:到底什么是"接口"?接口的两种含义——模块的使用界面与模块的环境依赖——分别应该遵循什么准则去设计?
上一讲 OCP 强调"模块业务要稳定、变化点用接口/插件开放出去"。本讲承接这一点,把"接口"这个天天挂嘴边的词用科学家的严谨拆开:接口在不同语境下有两层含义,一是模块的使用界面(规格),二是模块对依赖环境的抽象(契约式设计)。两者各有准则。
接口的两种含义
| 含义 | 别称 | 一句话 |
|---|---|---|
| 模块的使用界面 | 规格 | 公开的类/函数原型,应自然体现业务需求 |
| 模块的环境依赖 | 契约 | 模块与模块之间的契约,基于接口而非具体实现(Design by Contract) |
模块的使用界面:KISS 原则
使用界面最重要的是 KISS(Keep it Simple, Stupid)——让傻子也能看懂,简单自然、符合惯例。以七牛 mockhttp(不真启动 HTTP 服务、无端口占用的测试库)两版接口对比:
| 版本 | 接口 | 问题 / 改进 |
|---|---|---|
| mockhttp.v1 | func Bind(host, service interface{});var Client rpc.Client |
违反 SRP(既启服务又做路由分派);Bind 不合 Go 惯例;Client 类型 rpc.Client 带来多余依赖,应叫 DefaultClient |
| mockhttp.v2 | func ListenAndServe(host, service http.Handler);var DefaultClient *http.Client |
Go 程序员一看即明,与标准库 net/http 自然融合 |
关键判断:接口要 KISS,很重要的一点是符合语言和社区的惯例。某类业务若语言里已有约定俗成的接口(如
ListenAndServe),就尽量沿用相同语义。所有的偷懒总会还回来——v1 的随意最终逼出了 v2 重写。
模块的环境依赖:最小依赖原则
环境依赖遵循 最小依赖原则 / 最少知识原则(LKP),尽力发现并去除多余依赖。环境依赖分两种,处置方式不同:
| 依赖类型 | 含义 | 处置重点 |
|---|---|---|
| 使用界面依赖 | 用模块使用界面时自然涉及的 | 对参数泛化与抽象,适应更广场景 |
| 实现依赖 | 当前实现方案涉及的组件,换方案就可能消失 | 默认直接依赖组件,必要时才抽象成接口 |
参数泛化:恰如其分,别过度
- 好的泛化:IO 接口把
*os.File换成io.Reader/io.Writer,顺带支持剪贴板。 - 伪泛化要警惕:
Bind(..., interface{})看似比ListenAndServe(..., http.Handler)更泛化,实则 v1 内部靠反射做路由分派,假设更强、场景反而收紧了。 - 过度泛化是过度设计:不会发生的泛化就别做。让系统足够简单又不失扩展性,平衡全靠对业务的理解。
实现依赖:如无必要,勿增实体
关键判断:大部分情况应直接依赖组件,不必抽象。大量抽象基础组件会提升可配置性,但也抬高学习成本。
什么时候才该把实现依赖抽象成接口:
| 场景 | 例子 | 注意 |
|---|---|---|
| 需要提供多种选择 | Logger 组件,决策权交使用方 | 但抽象数据库以便 MySQL/MongoDB 自由切换多属过度设计——选数据库本应慎重 |
| 解除庞大外部系统依赖 | 测试时 mock 掉重外部依赖 | 降低测试系统依赖 |
| 依赖的是可选组件 | 初始化时把接口设为 mock 组件 | 用户不关心就当它不存在,降低学习门槛 |
对实现依赖做接口抽象,本质是给模块配置化、增加配置项,需谨慎、适可而止。
总结
接口有"使用界面"和"环境依赖"两种理解,各对应一条准则。一句话:使用界面追求简单自然合惯例,环境依赖追求够用就好别多依赖。
先把"是什么"回答清楚
| 概念 | 一句话说明 |
|---|---|
| 使用界面 | 模块的规格/原型,准则是 KISS、符合惯例 |
| 环境依赖 | 模块间契约,准则是最小依赖/最少知识(LKP) |
| 使用界面依赖 | 用接口时自然涉及的依赖,靠参数泛化处理 |
| 实现依赖 | 当前实现引入的依赖,默认直接依赖、必要才抽象 |
| 过度设计 | 为不会发生的泛化或灵活性付出的复杂度成本 |
一句话速记
使用界面用 KISS 跟着社区惯例走,环境依赖用 LKP"如无必要勿增实体"——泛化和抽象都要恰如其分,过犹不及。
几条值得记住的判断
- 符合社区惯例比"自创一个简单接口"更 KISS。
- 看似泛化的
interface{}可能是场景收紧,要看背后假设。 - 抽象实现依赖 = 配置化,只在"多选择 / 解重依赖 / 可选组件"三种场景才做。
思考题
审视你最近写的一个公共接口:它的参数类型是恰如其分,还是为了"灵活"用了过度泛化的 interface{} / 抽象层?如果删掉这层抽象、直接依赖具体组件,会损失什么真实存在的能力吗?
