不限语言,谈谈如何避免循环依赖?

2022-09-05 16:08:10 +08:00
 vczyh

如何避免循环依赖,这个其实和语言没什么关系。

解决方式也比较简单,但是随着业务复杂起来,比较容易出现这种情况。

就用 Java 来说,大家怎么避免 service 互相调用的。

7021 次点击
所在节点    Java
65 条回复
Chad0000
2022-09-05 18:37:26 +08:00
@vczyh #37
@ChoateYao #30

同意 30 楼的,所有服务抽象成接口远程调用,不管你是订单依赖于用户还是用户依赖于订单,你依赖的东西全部都已经通过接口提供(实际为远程调用),除非你在两个服务中又分别调用对方的同一方法导致递归,就不存在依赖问题。

PS:我现在在公司就在梳理基于 Dapr 的微服务,现在已经将各服务抽象成接口了,调用方直接引用相应的接口库即可。这样你随便引用哪个接口库都无问题。
yrj
2022-09-05 18:40:36 +08:00
高度解耦,只依赖公共库或函数
lmshl
2022-09-05 18:45:09 +08:00
比如我这个例子,UserService ,OrderService 都是静态存在的,唯一依赖是运行时的 Connection ,再怎么互相调用,再怎么交错也不会出错
gfreezy
2022-09-05 18:47:23 +08:00
@vczyh Order 不返回 User 的信息,只返回 user_id 。Order 和 User 在上面加一层 UserOrder ,这一层根据 Order 的信息,从 User 中在获取对应 User 信息,最后拼接后返回。

但实际可能很多别的服务又依赖了 UserOrder 的返回结果,我们的方法是在这一层不要拆得太细,把相关的(依赖 UserOrder 的)都合并到一个模块,在很大程度上可以缓解这个问题。否则再加一层会非常复杂,得不偿失。
levelworm
2022-09-05 21:48:48 +08:00
上面加一层全知全能的总管,然后靠总管传来传去
shot
2022-09-05 22:24:40 +08:00
@vczyh #37

> 情况 1:查询用户,带出对应的订单(造成 User 依赖 Order.getListByUserId(long userId)接口)
> 情况 2:查询订单,带出用户的某些信息(造成 Order 依赖 User.getSomeInfo(long userId))

用户模块属于最基础的模块,不应依赖于其它业务模块。考虑两种情况:
1. 如果用户模块依赖于订单模块,那么添加支付功能就会依赖支付模块,添加消息功能就要依赖消息模块,最后用户模块就会成为一个「巨无霸类」,无法维护;
2. 如果把用户模块和订单模块拆分为独立的微服务,那么用户微服务里不应保存订单信息,所以用户模块也不应依赖订单模块。

回到「情况 1:查询用户,带出对应的订单」的问题。
从产品业务分析,我觉得这是标准的「基于用户 ID 查询订单」功能,应该由订单模块独立提供 API 接口和服务。
web/app 端拿到用户 ID 后调用这个 API 接口即可。
yannxia
2022-09-05 23:28:01 +08:00
@fiypig 写 Java 的时候几乎没遇见过,2 个 Class 相互依赖很好处理,依赖了也比较好解决,Go 反而连 package 都不让你依赖···
nothingistrue
2022-09-06 01:11:09 +08:00
@vczyh #37 再仔细想一下你说得这两个需求,是不是都用户界面需要的功能,这俩需求即不是 UserServive ,有也不是 OrderService 的目标。

这里的层和模块大致可以如此划分:

用户界面层:用户(再细分为用户注册登录等身份识别部分、我的订单等个人资料纯查询部分)、下单、。以上模块之间存在沟通,但不存在依赖。例如商品结算的时候,虽然它要调用下单方法,但它并不依赖订单模块,因为它这里只需将用户 ID 、商品 ID 等不变的值,作为方法参数传递出去即可。实际上这里商品模块是不能调用这一层的下单方法的,它直接调用的是业务逻辑层的下单方法,由业务逻辑层的下单方法处理完成之后再转给用户界面层的下单模块。这里完全可以认为,购物车结算的时候,购物车只需要把参数扔出去即可,谁负责接受并不归它管。你要有足够的资源,这里完全可以把方法调用模型,换成发布订阅 /推送模型。

业务逻辑层:UserService (这一层就只有身份识别相关的了,各种“我的资料”不归它管了)、OrderService (含写方向的下单、读方向的查询,以及所有与订单数据相关的业务)、UserOrderViewService (这是用来解决用户订单关联查询的,如果只是我的订单这种查询功能,用不到这个,那个靠订单模块就够了,但如果用户界面层有更复杂的查询就可以考虑加上这个)。这里涉及到一些读写分离的思想,但还没到读写分离设计模式的地步,用起来还是很容易的。


对于上面的划分,有几点需要说明一下,如果没理解的话看完下面的回去再看一遍应该就能理解了。

第一,方法 /函数调用,不等于依赖关系,Java 早期的简单分层模型,让人习惯了 Controller 只调用 Service 、Service 只调用 Dao 的模式,进而误以为调用方法就是依赖被调用方法的,实际情况不是这样。你可以在编写 Dao 前编写 Service ,但在 Dao 接口正式编写出来前,你这个 Service 是绝对用不了的,连打桩单元测试都不行,这是依赖。而方法调用不一定是这样,比如上面的下单这个处理,虽然最终运行的时候,是商品模块的某个方法,调用了下单模块的某个方法,但是商品模块可以自行独立开发然后打桩测试,完全不用管对方是否已经完成(双方都可以这样,甚至都不用提前协商好方法参数声明),这是没有依赖关系的方法调用。 简单来说,没有对方就能自行打桩测试的,是无依赖关系的方法调用。

第二,同层之内允许从上到下的调用链,而如果是同层同模块内部,允许双向依赖——不分场合的禁止双向依赖,是违反内聚原则的。

第三,有些跨多个模块的信息,可以设计成不变值(在 DDD 中有专有名词:值对象)。例如向商品 ID 、名称、价格这些信息,可以组合成“商品信息{ID 、名称、当时的价格、当时的描述信息}”不变值,整体作为订单的一个属性。这样对于订单详情界面来说,它只需要从 Order 实体 /表 当中就能获取全部信息,而不用再弄个 OrderGoodView 。
sora2blue
2022-09-06 08:48:59 +08:00
可以把查询用户带出订单信息的部分和查询订单带出用户信息的部分都分别做成一个插件吧。
用一个类统一管理这些查询操作,然后把需要的信息以插件类的形式注册到 UserService 或者 OrderService 。
vczyh
2022-09-06 10:55:24 +08:00
@nothingistrue
非常感谢老哥打这么多字解释。

我理解就是明确每个 Service 的职责和边界,如果需要组合那么这个就不是 User 或者 Order 的职责,这个功能应该放到别的 Service 中。

> 第二,同层之内允许从上到下的调用链,而如果是同层同模块内部,允许双向依赖——不分场合的禁止双向依赖,是违反内聚原则的。

对于这个我有一些疑问:我认为允许同层同模块互相依赖确实可以减少代码冗余,增加内聚,但我们在设计之初是否最好避免互相依赖呢。

能否通过这样的方式:在 service 下加 manager 层,manager 和 service 都不允许同层互相调用,service 可以组合多个 manager ,manager 提供一些细粒度的操作。
vishun
2022-09-06 10:55:44 +08:00
@Chad0000 #41 感觉这样也没有解决,依赖往狭义上说是各个类之间的依赖,往广义上说是各个微服务之间的依赖,这种封装成接口的,实际上各个微服务之间互相依赖,而大家推崇的是单向依赖,需要高度的解耦,感觉不是一件容易的事情。
vczyh
2022-09-06 11:01:48 +08:00
@shot
这个思路跟在 service 上加一层很类似,只不过这一层是 web/app 。
Joker123456789
2022-09-06 14:21:54 +08:00
循环依赖 只存在于单例的情况下,解决办法也很简单,把实例化 和注入 分两步进行 即可。

spring 就是用的这个方法,只不过他稍微复杂了一些,他是在每个 bean 实例化完成后 立刻就开始注入的,所以出现了 一级,二级缓存。

还有更简单的,实例化就单纯的实例化,不要注入,然后遍历所有 实例化后的对象 对其进行注入,因为前一步已经实例化完了,所以注入的时候 直接在内存里取相应的对象即可。

多例的情况 就不存在循环依赖,注入时直接 new 一个即可。

如果你说的是:循环调用,A 方法调用了 B 方法,B 方法又调用了 A 方法,那是程序猿的问题,这种东西无法避免的,只能开发者自己小心。
summerLast
2022-09-06 15:02:30 +08:00
设计之初就不允许 service 互相调用,公用逻辑可以在 sevice 下放到 manager , 或在 service 上建个调度层协调 service
summerLast
2022-09-06 15:07:19 +08:00
@vczyh 建议参考领域驱动 17 楼的查询可以 在 order 上增加 user 的一些冗余信息 如 id name ,其他的可以放在 faced 组合 或者 在 application 层进行调度,将服务分成两类 一类是应用服务 面向应用的,一类是领域服务 包含领域知识的,应用服务调度编排领域服务 面向应用
meiyoumingzi6
2022-09-06 15:11:21 +08:00
python 表示直接 import locally 不就完了,要啥自行车🤪🤪🤪
summerLast
2022-09-06 15:13:42 +08:00
设计订单的时候可以订单里的用户只是个值对象 而非实体
wanguorui123
2022-09-06 15:25:51 +08:00
提前规划化主从 Service ,然后约定
morty0
2022-09-06 15:48:38 +08:00
go 直接编译报错
vczyh
2022-09-06 17:07:49 +08:00
@summerLast 感谢~

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/877841

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX