手撕设计原则:接口隔离
架构0期-W2-实战作业
请用接口隔离原则优化 Cache 类的设计,画出优化后的类图。
本篇文章主要描述我对依赖倒置原则的理解。并结合一个我们比较熟悉的框架对依赖倒置原则进行分析。在这个过程中希望我们能够一同学习。
经典设计原则
SRP 单一职责原则
就一个类而言,应该仅有一个引起他变化的原因。
OCP 开放封闭原则
软件实体(类、对象、模块等)应该是可以扩展的,但是不可修改。
LSP 里氏替换原则
子类必须能替换到他们的基本类型。
DIP 依赖倒置原则
抽象不应该依赖于细节。细节应该依赖于抽象。
ISP 接口隔离原则
不应该强迫客户依赖于他们不用的方法。接口属于客户,不属于他所在的类层次结构。
REP 重用发布等价原则
重用的力度就是发布的力度。
CCP 共同重用原则
一个包中的所有类应该是共同重用的。如果重用了一个类,就要重用这个包中的所有类。互相之间没有紧密联系的类不应该在一个包中。
CRP 共同封闭原则
一个包中的所有类对于同一类性质的变化应该是共同封闭的。一个变化若对一个包有影响,则将对包中所有类产生影响,而对其他的包不产生影响。
ADP 无依赖原则
在包的依赖关系中不允许存在环。细节不应该被依赖。
SDP 稳定依赖原则
朝着稳定的方向进行依赖。
SAP 稳定抽象依赖
一个包的抽象程度应该和其他稳定程度一致。
什么是接口隔离原则
接口隔离原则的英文翻译是“ Interface Segregation Principle”,缩写为 ISP。其原味儿英文定义如下:
Clients should not be forced to depend upon interfaces that they do not use。
翻译过来如下:
客户端不应该被强迫依赖它不需要的接口。
其中的“客户端”,可以理解为接口的调用者或者使用者。
在这个原则中提到了“接口”和“客户端”。就是我们在做设计时如何能做到针对不同的客户端将接口的使用隔离开。
我们在日常的开发工作中经常能遇到这类事。你在开发一个App,个人信息页面你需要通过一个接口来获取相关信息,找了一圈,终于找到一位大哥负责这个模块。大哥很热情,还没等你开口,甩给你你个接口文档。你打开文档一看,瞬间感觉头晕目眩。因为如果你基于这个接口文档做开发,完全可以做一整套的系统。
以上是一个生活中的例子,这种情况属于违反接口隔离原则的一种情况。你作为客户端,不应该看到不需要的内容。这位大哥这么做也是极不负责的。如果一个删除接口被滥用,后果非常严重。
在我们做面向对象设计时,讨论的接口隔离原则,这里的“接口”二字,一般会被理解为以下三种东西:
一组API接口集合
单个API接口或者函数
OOP中的接口
不同的场景理解也有所不同。下面结合以上三种场景来解读这个原则。
一组API接口集合
这个场景可以结合上面大哥的例子,不要将客户端不需要的接口释放给他。
现实工作场景中,一定不要不加限制的让客户端随意访问自己的接口。当然,我们可以使用现成的框架来解决接口授权访问的问题。如果没有框架支持,我们也可以通过对接口进行改造,来实现接口的隔离。下面给出一个小例子。
我们有上图这样的一个类,有四个方法,其中 put get delete 方法是需要暴露给应用程序的,rebuild 方法是需要暴露给系统进行缓存管理的。如果将 rebuild 暴露给应用程序,应用程序可能会错误调用 rebuild 方法,导致 cache 服务失效。
按照接口隔离原则:不应该强迫客户程序依赖它们不需要的方法。
下面我们对这个设计进行改造。
我们抽象出两个接口,将接口分类。普通操作的方法放到一个接口中,管理类的接口放到一个接口中。如下所示:
现在我们释放一个只有普通操作的Cache类,我们只需要让这个类实现CacheOperator接口。类图如下:
接下来我们再释放一个具有管理Cache功能的类,我们让管理类也具有普通的操作功能,类图如下所示:
根据以上的设计。我们实现了将一组API在接口层面进行隔离,使应用程序看不到管理方法。
也就是说,如果把“接口”理解为一组接口集合,可以是某个微服务的接口,也可以是某个类库的接口等。如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。
单个API接口或者函数
把接口理解为单个接口或函数。那接口隔离原则就可以理解为:函数的设计要功能单一,不要将多个不同的功能逻辑在一个函数中实现。接下来,我们还是通过一个例子来解释一下。
我们有一个负责统计的类,如下图所示:
很明显,count函数的功能不够单一,虽然count函数只是负责统计的事,但如果我们只是需要求max、min的统计值,并且数据量特别大时。明显有些疲累。(实际上,判定功能是否单一,除了很强的主观性,还需要结合具体的场景。)
下面我们把count方法拆分成力度更小的方法。
现在我们可以对外暴露一个只包含max和min的统计方法了。
根据上面的做法,我们将一个功能杂糅的方法拆分成力度较细的功能单一的方法。从这个角度符合了接口隔离原则。
也就是说,如果把“接口”理解为单个 API 接口或函数,部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。
OOP中的接口
在这个场景中,我们把接口理解成OOP中接口的概念,java语言中的interface。
我们还是用例子说明如何在实际设计中巧妙是使用接口,并符合接口隔离原则。
假设我们的系统接收文件后对文件进行存储,存储的target支持阿里云OSS,华为云OBS,以及本地存储LocalStorage。每个存储系统都有相应的配置参数,比如secret,token等。系统根据需求将不同的文件分发到不同的存储系统中。
现在我们为每个存储类型增加一个配置类。OSSConfig,OBSConfig以及LocalStorageConfig。
接下来这三个配合类可能会接受各种需求。
比如,要在其中的某个存储配置类中增加热更新的特性。而其他的类不需要。
再比如,想要查看其中某给存储的配置信息,而不需要查看另外的。
抽象来看就是有两个操作需要增加,第一个是update,第二个是view。那如何进行设计呢。
我们有两个选择:
1、设计两个功能单一的接口。
2、设计一个接口,里面将要方关于配置类所有的扩展,所有的配置类都实现这个接口。
很明显第一种设计思路更加灵活、易扩展、易复用。接口职责单一,通用性好,复用性也好。
第二种设计明显散发着一种臭味。他让有些类很累。做了很多不请愿的事。就像你可能写过这样的代码:
也就是说,如果把“接口”理解为 OOP 中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。
接口隔离原则与单一职责原则的区别
单一职责原则针对的是模块、类、接口的设计。接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面它的思考角度也是不同的。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。
总结
接口隔离原则最重要的就是理解“接口”二字。根据本篇所描述的三种接口场景。充分理解“客户端不应该被强迫依赖它不需要的接口”这句话。灵活的应用在面向对象设计过程中。
参考资料
关于设计原则大部分参考两方面来源
[1] 极客时间专栏《设计模式之美》 - 王争
[2] 《敏捷软件开发-原则、模式与实践》 - (美)Robert C. MArtin 著
评论 (6 条评论)