前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >依赖倒置就是每一个实现都要抽一个接口出来吗?

依赖倒置就是每一个实现都要抽一个接口出来吗?

作者头像
ThoughtWorks
发布2021-08-23 10:50:47
4300
发布2021-08-23 10:50:47
举报
文章被收录于专栏:ThoughtWorksThoughtWorksThoughtWorks

A: 我觉得我们现在的抽象有点多,infra 层里面每一个类都抽取了接口,这些被调用的类多半只有一个实现,我们是不是做的太细了? B: 从依赖倒置的角度讲,domain 层和 service 层并不应该直接调用 infra 层的实现,因此我们确实是需要每一个实现都抽一个接口出来。 A: 那依赖倒置就是每一个实现都要抽一个接口出来吗? B: 这个...

看来小伙伴 A 不经意间触碰到了 S.O.L.I.D. 的深水区...

相比于单一职责、开闭、接口隔离等原则,依赖倒置与里氏替换类似,属于更偏向操作指导的一类原则,比如从依赖倒置的定义来看:

依赖倒置:高层模块不应直接依赖低层模块,他们都应该依赖于彼此间的抽象。

以开发的角度理解:高层不要直接调用低层,而是调用抽取出来的接口。

那这么说,依赖倒置就是每一个实现都要抽一个接口出来吗?

为了解释这个问题,我们尝试来提出一个新的问题:为啥要依赖倒置?

为啥要依赖倒置

先说结论:因为依赖倒置能隔离变化,使核心业务更稳定。代码是由业务需求驱动出来的,而业务驱动路径一定是从高层(较为稳定的核心业务层)逐渐传递至低层(较为多变的外围支撑层)。高层不会去了解低层的实现细节,而只会对低层给出需求的定义。依赖倒置就是要明确需求的定义。我们引入一个例子,业务需求如下:对于某文档管理系统,业务上需要对用户创建的文档进行存取。现有若干 Document 文档对象,期望提供某种服务,对文档进行存储,要求存储成功后拿到一个该文档的唯一标识 DocId,并且可以通过该 DocId 再次取回该文档对象。显然,该系统核心业务是对文档的管理与操作。而将文档存储至某种库,之后对其建立索引并关联唯一标识的工作,应该属于对核心业务的一种支撑。所以,将之设计为独立的低层模块比较合适。而高层模块只需要知道我能提供什么,以及我能得到什么即可。所以,高层业务可以抽象出如下 API 来描述这一需求:

id:DocId saveDocument(doc:Document)
doc:Document getDocument(id:DocId)

那么假如从低层即服务提供者角度来看呢?作为服务提供者,也就是需求实现方,我最先想到的也许是:文档对象应该就是文件吧?如果要存储某个文件,存储在独立的文件服务器上会比较稳妥。所以,首先我需要知道文件所在主机的 IP(如有必要还需要相关认证信息),以及文件的绝对路径。之后的实现过程可以分为以下几步:登录到主机上,根据路径找到文件;远程复制该文件到独立的文件服务器;生成一个唯一标识,并与文件服务器的真实路径关联;将唯一标识以 String 的形式返回给调用者。因此对于服务提供方,可能会提供如下 API:

id:String saveFile(ip:String, path:Path)
void getFile(id:String, ip:String, path:Path)

显然,提供方和消费方给出的 API 大相径庭。服务提供方甚至根本就默认把 ”文档“ 这样一个业务概念脑补成了 ”文件“ 。说了半天,没提依赖倒置呀?我们顺着上文的思路,来想一想,假如高低层模块都是由同一个团队来开发维护,并且按照业务驱动的模式来开发,上述需求会怎么一步步变成代码呢?出现业务需求,期望对文档进行存取团队认为,具体文档存取的实现应该不属于 domain 层,而是 infrastructure 层。为了不影响业务卡的开发,团队根据讨论,提取出了文档存取所需的抽象,即:

id:DocId saveDocument(doc:Document)
doc:Document getDocument(id:DocId)

某小伙伴领取实现文档存取的故事卡,先通过工具类获取本机 IP,之后从文档对象中拿到实际的 file,以及对应的元数据,之后存储至远端文件服务,元数据入库,返回唯一 id。后来,由于部署环境变更,远程文件服务不可用,文件存储要改为存储在本地,对于这个需求,做卡的小伙伴只要遵循抽象,重新实现一套本地存储的方案即可,对高层业务完全透明。可见,由于对需求进行了明确的定义,产出了需求的稳定抽象,基于此抽象的实现,不论如何变化,都不会影响到核心业务的稳定,这就是依赖倒置。由业务需求驱动的开发天然满足了依赖倒置的要求,层与层之间互相解耦,整个系统也就对变化表现出了更强的适应性。不过实际当中,很多时候不同模块的开发是由不同团队完成的,我们也许没办法左右已经提供了 API 的基础设施,这时怎么办呢?在实践 DDD 中,我们经常会听到六边形架构的概念,六边形架构内所有的业务逻辑与其他外部依赖之间,全部采用适配器(Adapter)进行适配,以尽可能的隔离业务边界,增加扩展性。所以引申到上述例子,假如系统现有的文件服务提供给我们的 API 必须要以 IP 和文件路径作为参数,那么为了防止业务与外部服务产生依赖,我们仍旧以业务需求驱动的方式,提取文档抽象,之后新增适配器,适配器一端依赖抽象,另一端依赖外部文件服务。通过这种办法就可以很好的实现依赖倒置。有了适配器,无论外部服务怎么变化,只要跟着改适配器,我们的业务仍然是高度内聚的。

回过头来

前文聊了聊为什么要依赖倒置以及怎么进行依赖倒置。现在,我们可以再回到最开始的问题本身:

依赖倒置就是每一个实现都要抽一个接口出来吗?答案显而易见了:恰恰相反,依赖倒置应该是先由业务消费方定义接口,再由服务提供方实现,只不过从最终产出物的角度看,的确是可能每个实现都抽取了一个接口而已。因此假如作为服务提供方,为了满足依赖倒置,臆想消费方的需求来抽取接口,那不叫依赖倒置,叫本末倒置。

总结一下

什么是依赖倒置:高层模块不应直接依赖低层模块,他们应该都依赖于彼此间的抽象。为什么要依赖倒置:因为依赖倒置能隔离变化,使核心业务更稳定。怎么实现依赖倒置:核心业务方定义需求抽象,服务提供方实现需求抽象。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-08-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 ThoughtWorks洞见 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档