如何理解面向接口编程?

最近学Java时老师频繁提到面向接口编程,网上有资料说面向接口编程可以实现高内聚低耦合,不是太理解,假如有一天这个接口废弃了,不是还得改变继承这个接口的实现类吗,为啥感觉比不要接口编程依赖性更大了?

阅读 3.8k
4 个回答

一般来说,不会存在某个接口废弃的说法,因为即使不面向接口编程,那就相当于直接是把类给废弃了。

就拿常见的缓存来说,缓存可以有 文件缓存、数据库缓存、Redis 这种。如果你一开始就使用文件缓存,代码里面导出也充斥着文件缓存(类)的引用,那如果将来有一天,文件缓存已经不能满足承载了,你要更新到 Redis,那么你就要把你之前代码中所有的文件缓存相关的代码都改成 Redis。当然,如果你说你熟练使用 Replace ,把一些使用的地方 Replace 掉就好了,再把原来的 FileCache 类改改实现,就可以了。

那换个说法,现在某些场景下,需要使用 Redis 缓存、某些场景下需要文件缓存,且将来某个时机,都可能会迁移到 Redis 缓存去,你该怎么去设计?

现在,你只需要设计一个 Cache 的接口。包含 put/get/remove/flush 这几个方法,现在你只需要分别创建 FileCache 和 RedisCache 类来实现这个接口。

然后在实际使用时,指定类型提示为 Cache 接口,现在你就可以放心的使用了,即便是以后换了其他的缓存,只需要保证传递进去的是一个实现了 Cache 接口的对象实例就可以。

这样就实现了低耦合。

这个问题,我其实不太想回答,我看了@唯一丶 回答得挺好,但是我寻思了一下,你能问出这个问题,肯定是编程经验不足的,我就来说几句。

首先来,这种所谓的“面向接口编程”,这段名言来自“设计模式”相关的书籍,如果你看过设计模式就不会有这种疑问了。

设计模式的目的是为了降低耦合,提高程序的可重用性可扩展性的。

一. 设计模式根据目的划分为几种类型:

  1. 创建型模式:作用于对象的创建,将对象的创建与使用分离。其中囊括了单例、原型、工厂方法、抽象工厂、建造者5 种创建型模式。
  2. 结构型模式:将类或对象按某种布局组成更大的结构,其中以代理、适配器、桥接、装饰、外观、享元、组合 7 种结构型模式为主。
  3. 行为型模式:作用于类或对象之间相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。主要包含了模板方法、策略、命令、职责链、状态、观察 者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。

二. 根据作用范围来分:

  1. 类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,在编译时刻便确定下来了。工厂方法、(类)适配器、模板方法、解释器均属于该模式。
  2. 对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。除了以上 4 种,其他的都是对象模式。

其他具体细节可以自行找设计模式相关的书籍或者是资料自行研究。

你这里提到了个问题:

假如有一天这个接口废弃了,不是还得改变继承这个接口的实现类吗,为啥感觉比不要接口编程依赖性更大了?

这里应该分为两种情况:

  1. 假如有一天这个接口废弃了
  2. 改变继承这个接口的实现类

首先,如果接口被废弃了,说明依赖于这一套接口的整套业务代码都不需要了,所以不存在什么继承这个接口的实现类之类的东西,因为面向接口编程 的思想就是以“接口”为主,如果接口都不要了,修改继承/实现这个接口的类根本没有意义。

然后说改变这个接口的实现类,这里除非是实现这个接口的类有bug或者是功能不完善,那么随着业务需求扩展之类的,肯定要改,这个是基本的编程问题,没啥好说的。而如果你说,实现的类不需要了,或者说不采用这种方案实现,那么你新起一个类,按照这个接口用新方案实现一遍就行了,这就是一个最简单最基本的“工厂模式”,调用的时候统一切换成新的类就行了,如果一开始考虑周到,可能会采用“抽象工厂模式”或者其他高级的设计模式,如“依赖倒置”、“控制反转”等相关的设计模式,那么后续的代码修改肯定不需要做太多工作的,你甚至可以写个配置文件,根据实际情况在部署的时候采用不同的配置文件即可。

总结来说,你想要完全理解“面向接口编程”你必须要学会《设计模式》,而关于设计模式的内容,又不是一个回答里可以一时半会的能给你讲清楚,我这里给你回答算是抛砖引玉,让你找到后续的思考和学习的方向,参考这个方向持续学习就能得到正确答案。

看到这个问题,不太清楚该从何说起。

面向接口编程是一种编程思想,面得对象编程是面向接口编程思想的一种具体实现。思想本身就不是一朝一夕能形成的,思想的思想恐怕会堕入哲学的轮回 …… 扯远了!面向对象的思想本身就是一种抽象的编程思想,面向接口似乎还要更抽象一些。

什么是抽象?

与抽象相对的是具体,从具体的事物中找出其共同的规律或特点就是抽象。说起来似乎容易,理解起来还是要不断在实践中去领悟。

要理解面向接口,就得理解什么是接口。

从编程的角度来说,接口似乎是一种语言元素,是一种类型划分,通常用 interface —— 但这已经具体到某种语言了。面向接口编程中的“接口”是一个更为抽象的概念。这个概念更像是一种约定/规定。约定好了,大家就看这个规矩来。

比如 SegmentFault 上,有人发了一个问题,在阅读问题的时候,下方提供了一个回答按钮,点击之后就可以进行回答,而回答问题的时候可以使用 Markdown 语法…… 这就是一个接口的具体呈现。对于一个 SF 新手来说,可能并不知道该如何去回答,于是会去寻找这个接口,也注是约定 —— 该如何回答问题?方式是多样的,比如

  1. 可以去问身边的人或者客服
  2. 观察问题页面,根据经验找到可能是入口的地方
  3. ……

像这样的接口,我们称为 UI,也就是 User Interface。由于它是图形化呈现的,所以还称为 GUI,也就是 Graphic User Interface(与之对应的有 CLI,也就是 Command Line Interface)。除了这个接口之外,还有其他方式可以回答问题吗?有的 …… SF 网站提供了大量的 API (Application Programming Interface) 来实现数据交互,而其中不乏 Web API,也就是基于 Web(通常是基于 HTTP/S)的 API。这个接口一般人不会去用它,但是 SF 内部开发人员,尤其是前端人员会用得非常多。

除此之外,实际生活中也存在大量的“接口”,最典型的就是电源插座和插头。为什么插座有美标,国标,英标 ……,因为各个国家都制定了插座标准,只要按这个标准生产出来的插座和插头,就一定是能插上的。甚至还有万用插座和新国标(见图)

image.png

现在我们来抽象一点。在编程中,也存在各种各样的接口。通常一门语言都会提供 SDK,内含标准库 API,也就是我们用到的类啊、函数啊这之类的。我们在写程序的时候,定义了一个函数,这个函数会实现某一部分功能,这其实就已经定义了某种接口。调用者并不清楚函数是怎么实现的,只知道调用某个函数可以达到自己的目的,所以就去调用了。如果以后这个函数因为某些原则进行了优化,改变了实现,但是接口不变 —— 也就是说,调用它仍然能得到同样的结果(当然也有产生接口变更的情况,这个另说)。举例来说,调用了 List.sort 方法,对列表进行排序。结果是列表数组被排序,但是怎么实现的呢?有可能是冒泡,也有可能是快速排序……

对于 Java 来说,可能这个概念不是很明确,因为 Java 代码会把接口和实现写在一起(不是说 interface 定义的接口)。但是对熟悉 C/C++ 的人来说,就比较容易理解一些,因为 C/C++ 库分为头文件和实现库两个部分。头文件 (.h) 实际上是在定义接口,代码文件 (.c, .cpp) 是在实现接口。我们在使用某个库的时候,引入头文件好可编写并编译程序,实现的部分 (.lib 或 .dll) 则是在链接甚至运行的时候才会用到,至于是哪个版本的 .lib 或者 .dll,不重要,只要兼容接口就行 —— 这就是一种面向接口的编程方式。

回到 Java 上来,为什么基于 Java 6 编写的程序可以在 Java 8 上跑?可以肯定的是 Java 8 的实现和 Java 6 肯定有所不同,但是 —— SDK 中约定的接口是兼容的(不一定相同,但是兼容)。

现在回到一开始的问题,什么是面向接口编程?面向接口编程就是按照一定的约定来对程序进行分别的实现,由接口(约定)来将各部分联系在一起。面向接口编程的好处是,接口一定的情况下,实现部分是可替换的。某部分实现升级,只要符合,或者兼容原来的接口,就不会需变动其他部分来进行适配。在这种基本思想下,有很多实现手段和实现方法,而且也分为不同的层次。比如设计模式就是一套很好的接口编程经验总结。

那么另一个问题,接口如果废弃了怎么办?

接口废弃是件大事,因为可能产生很大的影响。其实不光是接口废弃,接口变更本身就是一件大事。还是从现实生活中来说,早些年我们的手机充电器都是 Mini-USB 接口,后来因为手机轻薄的需要,发面了 Micro-USB 接口。这些接口现在都还能看到,但是非专业/行业人士要想看到 Mini-USB 接口的设备和线材可能还是不容易了。而 Micro-USB 很大一部分也被 Type-C 代替,甚至 USB 都在被 Type-C 替代。这个就发生在身边的事情,描述的就是「接口变更」这么一回事。接口不是说废就能废的,都知道 Type-C 挺好,但是新旧接口的替代也会花上很长一段时间是。

回到程序上来,还是说 Java。Java 中存在大量声明为 deprecated 的接口,不知道的话,去看看 java.util.Date,在 JDK 17 中,Date 的 6 个构造函数就废了 5 个。但是考虑到兼容性问题,这些东西不能说废就废,只能声明废除。

另外 ,Java 语言在第 8 个主版本上(也就是 Java 8)发生了一件大事:引入了接口默认方法。说白了就是在接口中引入实现,破坏了接口的纯抽象特性。但不这样干不行啊,比如说 java.util.Collection 接口中添了 .stream() 方法,这就意味着所有实现了这个接口的类都必须实现 .stream() 方法,不然就不能通过编译。当然,实现这个接口的肯定不止 JDK 中的类,还有大量其他公司其他程序中的实现。所以,添加默认方法是无奈之举,就是为了避免因为添加一个接口方法造成的大破坏。

参阅:接口默认方法是什么鬼

但并不是说接口一定形成就不能改不能删。接口是可以改变的,但是应该存在一定的缓冲期。比如现在提倡的语义化版本,通常情况下会分为 major.minor.pacth 三个部分,一般会约定 .patch 用于 BUG 修复,不会造成接口的变化;.minor 升级表示存在接口变化,但是是非破坏性的,一般可以安全升级;.major 作为主版本变化,可能会有一些破解接口的变化,升级时应该谨慎。

注意:上述一般约定行为,但不表示所有库都会遵守,所以还要是注意阅读 Release Note,并在使用时 充分观察,做好技术预研。

而破解性的接口变化,也不是说变就变的,通过会先声明。比如在 v6 的时候声明某些接口将会在 v7 中停止维护,并在 v8 中废弃。Java 中的 deprecated 就是其中一种声明形式,另外还有在文档中申明、在官方博客中声明、登报声明等……多种多样的形式。

至于语法中的 interface …… 不要在意这些细节,只是若干接口中的一种而已,当然也是面向接口编程中非常重要的一种语法结构。

不知不觉又写了这么多,赶紧🤐,不然今天工作完不成了。

哈哈,我也来插一嘴。我总觉得面对对象和生活中很多场景是相似的。接口意味着高层抽象,实现则意味的 具体行动。在计算机应用中则有各种协议 代表着偏高层的首先,具体实现则是很多。在生活中也有很多例子,比如两个人,一个说我要活着,一个人说我要吃米饭。很明显第一个人拥有更多的灵活性。至于过时,确实是有可能过时的,协议也可能不实用了,但它过时肯定比具体实现要慢得多。

推荐问题
宣传栏