10. 接口而非实现编程
10.接口而非实现编程
目录介绍
01.接口编程原则
1.1 接口指导思想
02.如何理解接口
2.1 重点搞清楚接口
2.2 抽象的思想
03.来看一个案例
3.1 图片存储的案例
3.2 业务拓展问题
3.3 代码演变设计思想
3.4 重构后的代码
04.定义接口的场景
4.1 要有接口意识
4.2 接口具体的场景
4.3 定义接口掌握度
05.定义接口原则
5.1 接口定义原则
5.2 设计接口案例
5.3 不涉及接口案例
06.总结和重点回顾
01.接口编程原则
1.1 接口指导思想
基于接口而非实现编程。这个原则非常重要,是一种非常有效的提高代码质量的手段,在平时的开发中特别经常被用到。
“基于接口而非实现编程”这条原则的英文描述是:“Program to an interface, not an implementation”。
理解这条原则的时候,千万不要一开始就与具体的编程语言挂钩,局限在编程语言的“接口”语法中(比如 Java 中的 interface 接口语法)。
这条原则最早出现于 1994 年 GoF 的《设计模式》这本书,它先于很多编程语言而诞生(比如 Java 语言),是一条比较抽象、泛化的设计思想。
基于接口而非实现编程的主要思想是,代码应该依赖于抽象的概念和契约,而不是具体的实现细节。通过定义接口或抽象类,将具体的实现细节隐藏起来,使得代码更加灵活、可扩展和可维护。
02.如何理解接口
2.1 重点搞清楚接口
实际上,理解这条原则的关键,就是理解其中的“接口”两个字。从本质上来看,“接口”就是一组“协议”或者“约定”,是功能提供者提供给使用者的一个“功能列表”。
如果落实到具体的编码,“基于接口而非实现编程”这条原则中的“接口”,可以理解为编程语言中的接口或者抽象类。
这条原则能非常有效地提高代码质量,之所以这么说,那是因为,应用这条原则,可以将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低耦合性,提高扩展性。
“基于接口而非实现编程”这条原则的另一个表述方式,是“基于抽象而非实现编程”。后者的表述方式其实更能体现这条原则的设计初衷。
2.2 抽象的思想
在软件开发中,最大的挑战之一就是需求的不断变化,这也是考验代码设计好坏的一个标准。
越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。
而抽象就是提高代码扩展性、灵活性、可维护性最有效的手段之一。
03.来看一个案例
3.1 图片存储的案例
假设我们的系统中有很多涉及图片处理和存储的业务逻辑。图片经过处理之后被上传到阿里云上。为了代码复用,我们封装了图片存储相关的代码逻辑,提供了一个统一的 AliyunImageStore 类,供整个系统来使用。具体的代码实现如下所示:
整个上传流程包含三个步骤:创建 bucket(你可以简单理解为存储目录)、生成 access token 访问凭证、携带 access token 上传图片到指定的 bucket 中。
代码实现非常简单,类中的几个方法定义得都很干净,用起来也很清晰,乍看起来没有太大问题,完全能满足我们将图片存储在阿里云的业务需求。
3.2 业务拓展问题
过了一段时间后,我们自建了私有云,不再将图片存储到阿里云了,而是将图片存储到自建私有云上。
为了满足这样一个需求的变化,我们该如何修改代码呢?我们需要重新设计实现一个存储图片到私有云的 PrivateImageStore 类,并用它替换掉项目中所有的 AliyunImageStore 类对象。这样的修改听起来并不复杂,只是简单替换而已,对整个代码的改动并不大。
实际上,刚刚的设计实现方式,就隐藏了很多容易出问题的“魔鬼细节”,一块来看看都有哪些。
新的 PrivateImageStore 类需要设计实现哪些方法,才能在尽量最小化代码修改的情况下,替换掉 AliyunImageStore 类呢?这就要求我们必须将 AliyunImageStore 类中所定义的所有 public 方法,在 PrivateImageStore 类中都逐一定义并重新实现一遍。而这样做就会存在一些问题,我总结了下面两点。
首先,AliyunImageStore 类中有些函数命名暴露了实现细节,比如,uploadToAliyun() 和 downloadFromAliyun()。如果开发这个功能的同事没有接口意识、抽象思维,那这种暴露实现细节的命名方式就不足为奇了,毕竟最初我们只考虑将图片存储在阿里云上。而我们把这种包含“aliyun”字眼的方法,照抄到 PrivateImageStore 类中,显然是不合适的。如果我们在新类中重新命名 uploadToAliyun()、downloadFromAliyun() 这些方法,那就意味着,我们要修改项目中所有使用到这两个方法的代码,代码修改量可能就会很大。
其次,将图片存储到阿里云的流程,跟存储到私有云的流程,可能并不是完全一致的。比如,阿里云的图片上传和下载的过程中,需要生产 access token,而私有云不需要 access token。一方面,AliyunImageStore 中定义的 generateAccessToken() 方法不能照抄到 PrivateImageStore 中;另一方面,我们在使用 AliyunImageStore 上传、下载图片的时候,代码中用到了 generateAccessToken() 方法,如果要改为私有云的上传下载流程,这些代码都需要做调整。
3.3 代码演变设计思想
那这两个问题该如何解决呢?解决这个问题的根本方法就是,在编写代码的时候,要遵从“基于接口而非实现编程”的原则,具体来讲,我们需要做到下面这 3 点。
函数的命名不能暴露任何实现细节。比如,前面提到的 uploadToAliyun() 就不符合要求,应该改为去掉 aliyun 这样的字眼,改为更加抽象的命名方式,比如:upload()。
封装具体的实现细节。比如,跟阿里云相关的特殊上传(或下载)流程不应该暴露给调用者。我们对上传(或下载)流程进行封装,对外提供一个包裹所有上传(或下载)细节的方法,给调用者使用。
为实现类定义抽象的接口。具体的实现类都依赖统一的接口定义,遵从一致的上传功能协议。使用者依赖接口,而不是具体的实现类来编程。
3.4 重构后的代码
按照这个思路,把代码重构一下。重构后的代码如下所示:
04.定义接口的场景
4.1 要有接口意识
除此之外,很多人在定义接口的时候,希望通过实现类来反推接口的定义。
先把实现类写好,然后看实现类中有哪些方法,照抄到接口定义中。如果按照这种思考方式,就有可能导致接口定义不够抽象,依赖具体的实现。这样的接口设计就没有意义了。
不过,如果你觉得这种思考方式更加顺畅,那也没问题,只是将实现类的方法搬移到接口定义中的时候,要有选择性的搬移,不要将跟具体实现相关的方法搬移到接口中,比如 AliyunImageStore 中的 generateAccessToken() 方法。
在做软件开发的时候,一定要有抽象意识、封装意识、接口意识。
在定义接口的时候,不要暴露任何实现细节。接口的定义只表明做什么,而不是怎么做。而且,在设计接口的时候,我们要多思考一下,这样的接口设计是否足够通用,是否能够做到在替换具体的接口实现的时候,不需要任何接口定义的改动。
4.2 接口具体的场景
模块间通信:接口可以用于定义模块之间的通信协议。通过定义接口,不同的模块可以按照接口规范进行交互,实现模块之间的解耦合。
多态性:接口在实现多态性方面起到关键作用。通过定义接口,可以实现不同类的对象对同一个接口的实现,从而实现多态性。
框架开发:在开发框架或库时,接口是非常重要的。通过定义接口,可以为框架提供一组公共的规范和契约,供开发者使用和扩展。
面向对象编程:在面向对象编程中,接口是一种重要的概念。通过定义接口,可以定义一组方法和属性,以规范类的行为和功能。
依赖注入(DI):接口的定义在依赖注入中扮演着重要的角色。通过定义接口,可以将依赖关系从具体的实现中解耦出来。
API 设计:在设计应用程序编程接口(API)时,接口的定义是关键。通过定义接口,可以明确 API 的功能、参数和返回值,以提供给其他开发者使用。
数据访问层:在应用程序中,接口的定义可以用于抽象数据访问层。通过定义接口,可以定义一组数据访问操作,供不同的数据存储实现类进行实现。
4.3 定义接口掌握度
为了满足这条原则,我是不是需要给每个实现类都定义对应的接口呢?在开发的时候,是不是任何代码都要只依赖接口,完全不依赖实现编程呢?
做任何事情都要讲求一个“度”,过度使用这条原则,非得给每个类都定义接口,接口满天飞,也会导致不必要的开发负担。
至于什么时候,该为某个类定义接口,实现基于接口的编程,什么时候不需要定义接口,直接使用实现类编程,我们做权衡的根本依据,还是要回归到设计原则诞生的初衷上来。
只要搞清楚了这条原则是为了解决什么样的问题而产生的,你就会发现,很多之前模棱两可的问题,都会变得豁然开朗。
05.定义接口原则
5.1 接口定义原则
定义接口这条原则的设计初衷是,将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。
上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低代码间的耦合性,提高代码的扩展性。
5.2 案例分析对比
假设我们正在开发一个电子商务应用程序,其中有多个支付提供商(例如支付宝、微信支付、信用卡支付等)。希望能够轻松地切换和添加新的支付提供商,而不需要修改大量的代码。
如果没有定义接口,我们可能会在代码中直接使用特定支付提供商的实现类,这将导致以下问题:
高耦合性:代码中直接依赖于特定支付提供商的实现类,使得代码与该实现类紧密耦合。如果要更换支付提供商,需要修改大量的代码。
可维护性差:由于代码与特定实现类紧密耦合,修改一个支付提供商的实现可能会对整个代码库产生连锁反应。这增加了维护的复杂性。
那么按照今天学习的内容,通过定义接口,我们可以获得以下优势:
低耦合性:通过定义一个支付接口,代码只依赖于该接口,而不依赖于具体的支付提供商。使得代码更加灵活和可扩展。
可替换性:由于代码只依赖于支付接口,我们可以轻松地切换不同的支付提供商,只需提供符合接口定义的新实现即可,而不需要修改大量的代码。
可扩展性:通过接口定义,我们可以轻松地添加新的支付提供商,只需实现接口并提供相应的功能即可,而不需要修改现有的代码。
易于测试:通过接口,我们可以轻松地创建模拟实现来进行单元测试,而不需要依赖于真实的支付提供商。
从这个设计初衷上来看,如果在我们的业务场景中,某个功能只有一种实现方式,未来也不可能被其他实现方式替换,那我们就没有必要为其设计接口,也没有必要基于接口编程,直接使用实现类就可以了。
除此之外,越是不稳定的系统,越是要在代码的扩展性、维护性上下功夫。相反,如果某个系统特别稳定,在开发完之后,基本上不需要做维护,那我们就没有必要为其扩展性,投入不必要的开发时间。
5.3 不涉及接口案例
确实,在某些情况下,不定义接口也是可以的。以下是一个例子来说明不定义接口的情况:
假设我们正在开发一个简单的脚本,用于读取一个文本文件并对其进行处理。在这种情况下,如果我们只需要一个简单的功能,不需要考虑扩展性或可替换性,那么定义接口可能是不必要的。
在这个例子中,我们直接定义了一个具体的类 TextFileProcessor,它负责读取文件并进行处理。由于这个脚本只是一个简单的功能,不需要与其他组件进行交互或扩展,因此不定义接口也没有明显的劣势。
当涉及到更复杂的系统、模块之间的交互、可扩展性和可替换性时,定义接口通常是更好的选择。接口的使用可以提供更高的灵活性、可维护性和可测试性,使系统更易于扩展和修改。因此,在具体情况下,是否定义接口取决于需求和设计目标。
06.总结和重点回顾
什么是基于接口而非实现编程:代码应该依赖于抽象的概念和契约,而不是具体的实现细节。通过定义接口或抽象类,将具体的实现细节隐藏起来,可扩展和可维护。
定义接口主要是解决什么问题:通过定义接口或抽象类,将具体的实现细节隐藏起来,使得代码更加灵活、可扩展和可维护。
如何如何理解接口:可以理解为编程语言中的接口或者抽象类。可以将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。
设计接口的时候要注意什么:接口的定义只表明做什么,而不是怎么做。要多思考一下,是否能够做到在替换具体的接口实现的时候,不需要任何接口定义的改动。
定义接口的场景有哪些:比如数据访问层定义抽象接口可以方便不同数据存储方式;比如依赖注入通过定义接口,可以将依赖关系从具体的实现中解耦出来。
定义接口原则是什么:设计初衷是将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。
举一例子知道接口原则重要性:开发一个支付程序,多个支付提供商,例如支付宝、微信支付等,希望能够轻松地切换和添加新的支付提供商,需要用接口编程思想!
评论