类别: 原则
类型: 系统设计原则
来源: 罗伯特·C·马丁,C++ Report 工程笔记专栏,1996 年
别名: ISP;胖接口问题;角色接口;Interface Segregation Principle
类型: 系统设计原则
来源: 罗伯特·C·马丁,C++ Report 工程笔记专栏,1996 年
别名: ISP;胖接口问题;角色接口;Interface Segregation Principle
快速回答 — 接口隔离原则(Interface Segregation Principle)是指:调用方不应被迫依赖自己用不到的方法。罗伯特·C·马丁在 1996 年 C++ Report 专栏里把它写下来,针对的是把无关调用方绑在一起的“胖”接口。实务检验很硬:一次你永远不会调用的改动,若仍逼你重编译、重读或重测,你依赖的契约就太宽了。
什么是接口隔离原则?
接口隔离原则是一条设计规则:调用方只应依赖自己真正用到的方法,而不应依赖别人碰巧需要的那一包契约。客户不应被迫依赖他们并不使用的接口。这不是在求你把类写小。同一个对象仍可以干许多活。这条原则管的是可见性:每个调用方应只看见一张瘦、内聚的脸,哪怕脸后面的对象很大。 日常图像是学校出行表。二十分钟的博物馆参观,不需要过夜医疗同意、银行资料或校车驾照。这些字段若印在同一张纸上,每位家长都得跳过——或填错。法务一改过夜条款,短途表也得重印。厨房改了一道这桌根本没点的菜。 在代码里,一个同时要求
work()、eat()、sleep() 的 “Worker” 类型,对机器人做的是同一件事。空方法、UnsupportedOperationException、以及“别在这里调用”的注释,就是伤。单一职责原则问的是:一个模块是否有太多变更理由。这条原则问的是:某一个调用方是否被出示了太多那些理由。
接口隔离原则的三层理解
- 入门:别把打不开的工具箱塞给人。他们只想钉一幅画,开瓶器不是他们的事——直到开瓶器一改,整条工具腰带都得换发。
- 实践:按谁来调用拆接口,而不是按谁来实现。空方法和“不支持”就是气味。胖对象可以同时实现几张瘦脸。
- 进阶:调用方会把变更反向推到自己看见的契约上。胖接口因此把调用方彼此耦合起来。在静态语言里,这是一次重编译。在架构尺度上,仍是马丁后来那句话:依赖一个装了你用不到的东西的模块,是有害的。
起源
接口隔离原则起于静态类型面向对象代码里的编译与耦合问题,不是“接口要小”的口号。 罗伯特·C·马丁在 The Interface Segregation Principle(C++ Report 工程笔记专栏的第四篇,1996 年)里把它写下来。上一篇(1996 年 5 月)是依赖倒置原则。这篇专栏的工作定义,就是上面那句引文。马丁针对的是“胖”或“被污染”的接口:一个类的方法可以分成几组,每组服务不同的调用方。有些对象确实需要一张不内聚的表面。他主张:调用方不必把那张表面当成一个类型来认识。 文中第一处伤是一扇能上锁、解锁、报告开关的安全门Door。TimedDoor 还需要超时报警,于是设计者把 TimerClient 推到 Door 上。不需要计时的门,从此继承一个必须写成空操作的 TimeOut。更糟的是,定时器的一处修正——加上超时编号,免得门关上再打开后误报——会迫使所有 Door 的调用方重编译。马丁的诊断:分开的调用方,就该有分开的接口。父类型上的空实现虚函数,也违反里氏替换原则。
文中更大的例子是 ATM 用户界面。存款、取款、转账各自调用同一个 UI 类型的不同切片。一种交易强加给 UI 的改动,会打到其他所有交易。修法是拆成 DepositUI、WithdrawalUI、TransferUI,再由具体 UI 多重继承。若为了“一处就能拿到全部”又把这些脸包回一个头文件,拆掉的耦合会重新焊上。
马丁在《敏捷软件开发:原则、模式与实践》(Prentice Hall,2003 年)里用配套口号重述:多个面向客户的接口,好过一个通用接口。大约 2004 年,Michael Feathers 把马丁的“前五条”排成 SOLID 口诀。接口隔离原则是其中的 “I”。在《架构整洁之道》(Prentice Hall,2017 年,第 10 章)里,马丁把同一警告扩到类文件之外:依赖一个装了你用不到的东西的模块,是有害的。Martin Fowler 的角色接口从调用方一侧说出同一刀:接口应描述调用方需要的角色,而不是它所交谈的整个对象。
后来的讲授常把最初的咨询洞察放在施乐:一台打印机的 Job 类把打印、装订、传真混在一起。1996 年那篇论文本身并未点名这位客户。公开的展品是计时门和 ATM 界面。
核心要点
接口隔离原则在几种调用方共用一个对象、却并不共用同一批方法时,才真正值钱。它不是禁止丰富的对象。它是一条规则:哪些方法,你不许逼一个陌生人去编译。1
调用方会把变更反向推到自己看见的东西上
我们通常担心接口一改会弄坏用户。马丁多指出一股反向力:用户可以逼接口改,胖类型的其他用户就得付钱。若薪酬和实习生名录共用一个“员工门户”类型,税务字段就能让实习生应用重编。关注点分离拆的是工作。这条原则拆的是看见这些工作的窗口。
2
空方法是招认,不是设计
机器人用抛异常实现
eat(),或鸟把 fly() 写成空操作,并不是“特化的工人”。那是过于庞大的父类型生出的孩子。这些窟窿也破坏可替换性:胖类型的调用方不能信任这个方法。把脸切开,让每个实现者都能兑现自己公布的每一条方法。3
对象可以胖,脸必须瘦
TimedDoor 仍然既要跟门说话,也要跟定时器说话。马丁的答法是适配器对象,或对两个瘦基类型多重继承。同一个对象可以戴几顶帽子。调用方应拿一顶,而不是整架衣帽架。社团里一个人可以既是财务又管钥匙。零食轮值只应依赖“带零食”,不应依赖银行登录。
4
总是一起走的方法,不要拆
目标不是每个方法一张接口。为同一理由而变、且同一批调用方总是一起用的方法,应留在同一张脸上。YAGNI适用:在第二个调用方真正需要另一切片之前,不要发明角色。过度隔离会走出一座迷宫,类型虽多,移动仍是一块。
应用场景
当一个对象或表格服务几类受众,且一类受众的改动不应落到其他受众头上时,使用接口隔离原则。不要用它去打碎每个调用方本来就整套使用的内聚工具箱。在下一个调用方到来之前,先切开已公布的脸
若打印作业从不装订,就别让它们对着
staple() 编译。若只读报表从不保存,就别让它实现 save()。给每个调用方一个它能兑现的角色。开放封闭原则随后才有槽位,新调用方可填进去,而不必重开旧的。写一份替补真正接得住的职务或义工角色
一张“家长义工”表若同时要驾照、餐饮卫生证和过夜医疗同意,只会吓走只带蛋糕的人。先列出这一趟轮值必须做的事,再只问那些。必须跳过一半表格的副手,不是报名成功,是被用不到的字段挡住了。
把公务表格按角色来印,而不是印成一份总包
短期签证、借书证、楼宇通行证,不该共用一份“任何公务申请”PDF。公民用多跑一趟和交错附件来付这笔错配。若有些申请人永远用不到某个字段,它就不该出现在他们的父表格上。用拆表来藏,而不是用八磅字写“如不适用请跳过”。
别把管理权力放在日常表面上
实习生门户若加载薪酬、董事会议纪要和生产紧急关闭开关,违的是同一条规则。只依赖角色需要的切片。这是把最小权限用在知识上,而不只是用在权限上。关闭开关的一次改动,不应重发实习生首页。
经典案例
接口隔离原则作为公开 API 的带编号窗口,是 Java 的鼠标事件监听器——不是声称一个 GUI 包等于全部耦合理论。 JDK 1.1(1997 年 2 月 19 日)用委托取代旧的 AWT 继承式事件模型:你实现一个监听器并注册它。鼠标流量是故意切开的。MouseListener 收五个按键与边界方法:mouseClicked、mousePressed、mouseReleased、mouseEntered、mouseExited。MouseMotionListener 收两个运动方法:mouseMoved 和 mouseDragged。O’Reilly 的 Java AWT Reference 用数字说出理由:这一刀让你监听点击,而不被成千上万次运动事件打扰。这一刀是原则在工作。从不跟踪运动的调用方,不依赖运动。
剩下的点击接口仍然胖。多数教程示例只关心五个方法里的一个,其余四个写成空。JDK 自己的答法,同样始于 1.1,是 MouseAdapter:方法体为空的抽象类,你只覆盖关心的事件。Oracle 的 Java SE 文档至今仍这么写。截至 Java SE 26(2026 年),五方法接口和适配器都还在发货。WindowListener 用七个方法和一个 WindowAdapter 重复同一模式。
适配器是便利,不是被隔离的契约。调用方仍对着一个列出自己永不调用的方法的类型来编译。若给 MouseListener 加上第六个鼠标方法,每个直接实现者都得改。Java 8(2014 年 3 月)加入了默认方法,本可以把空实现放到接口上。已公布的监听器类型并未按这个来重做。新设计该抄的是 1997 年那一刀运动/按键切开,而不是适配器补丁:在印出空方法之前,按调用方需要来拆。边界说明:必须同时处理按下、拖动、松开的绘图工具,应把这些方法留在同一张脸上。失效的是把无关受众混在一起的契约,不是每一个方法数大于一的接口。
边界与失效场景
当切开比调用方更细时,接口隔离原则失效。若每个调用方总是一起使用lock、unlock 和 isOpen,三个单方法类型只增加导航,并不去掉耦合。这些方法是一个角色。留在同一张脸上。
它也会在已公布的胖类型上作为补救而失效。MouseListener 就是展品:适配器把空方法盖住,按键事件该切的那一刀从未真下。把 1997 年的适配器称作“模式”,并不使它适合抄进新 API。抄运动/按键那一刀,不要抄五个空桩。
常见误用是每个方法一张接口,然后一堆仍会一起变的类型。那是仪式,不是隔离。另一种误用是把这条原则当成单一职责原则的换名。职责管的是模块为何而变。隔离管的是调用方被迫看见什么。一个模块可以只有一个变更理由,却仍把同一扇胖窗出示给三类无关调用方。出乎意料的空方法也和快速失败作对:程序确实失败了,却失败在胖类型本已广告为平常的一次调用上。
常见误区
中文名会撞上“每个接口一个方法”、撞上“只有带interface 关键字的语言才算”,也撞上把适配器当成切开的替代。
接口隔离原则就是每个接口一个方法
接口隔离原则就是每个接口一个方法
马丁的 ATM 切开是按调用方,不是按方法个数。存款需要一张存款脸,上面可以有好几条存款消息。同一批调用方总是一起用的方法,应留在一起。一个谁也不需要单独拆出的单方法类型,不是成功。那是镀金之后剩下的灰。
接口隔离原则只适用于面向对象的接口
接口隔离原则只适用于面向对象的接口
马丁 1996 年的专栏用 C++ 抽象类,因为那是这份杂志。同一形状出现在 API、公文、门户和职务说明里。若一个槽位写着“任何义工”“任何附件”“任何员工”,顶替者不该被迫携带这个槽位从不用的字段或模块。微服务为了一个函数去导入胖共享库,是没有 class 关键字的同一种失败。
空适配器和默认方法可以让胖接口过关
空适配器和默认方法可以让胖接口过关
适配器和默认方法减少打字。它们不缩小契约。调用方仍依赖一个列出自己忽略的方法的类型,以后再加东西仍然会涟漪。空方法体是推迟的切开。若一个角色不用这个方法,这个角色的类型就不该提到这个方法。
相关概念
这些页面挨着同一个问题:如何让几类受众使用同一个对象或表格,而不让每一类受众背上别人的行李。单一职责原则
每个模块一个变更理由。这条原则接着问:某一个调用方被迫看见了其中哪些理由。
里氏替换原则
胖父类型上的空方法或不支持方法,既是替换窟窿,也是隔离失败。
开放封闭原则
新调用方应插进瘦槽位。胖槽位会在新人到来时重开旧人。
关注点分离
把不同决定分开。隔离是这些决定如何被出示给不同调用方。
YAGNI 原则
在第二个调用方需要另一切片之前,不要发明角色。过度拆分是投机架构。
最小权限原则
只依赖你需要的访问。这条原则是同一刀切在方法与模块上,而不只切在权限上。