V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
vczyh
V2EX  ›  Java

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

  •  
  •   vczyh · 90 天前 · 4204 次点击
    这是一个创建于 90 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

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

    65 条回复    2022-09-17 10:05:37 +08:00
    Kontinue
        1
    Kontinue  
       90 天前
    1. 设计问题,重新设计
    2. 学阿里,加一层 manager 层
    sunjiayao
        2
    sunjiayao  
       90 天前
    接口和实现分不同的 module 。实现 module 禁止其他模块引用。多个接口模块需要同时引用的类放到 common-module ,common 不引用任何模块
    VeryZero
        3
    VeryZero  
       90 天前
    分层,如果发现 service 互相调用,就再加一层。。
    vczyh
        4
    vczyh  
    OP
       90 天前
    @Kontinue 那 service 调用 service 是否合理呢,还是直接禁止 service 依赖其他 service ,如果需要组合那么在 manager ( facade )层干这件事。
    superB
        5
    superB  
       90 天前
    只要一個 God Class 就不依賴的
    vczyh
        6
    vczyh  
    OP
       90 天前
    @VeryZero 那这样是不是直接禁止 service 互相调用更好?
    vczyh
        7
    vczyh  
    OP
       90 天前
    @Very
    @vczyh 写错了,应该是禁止 service 注入别的 service
    Kontinue
        8
    Kontinue  
       90 天前
    @vczyh
    我觉得 service 调用 service 是合理的,但是互相调用那就不合理了。。
    1. 一般设计时候能想到的公共方法,一般会单独抽出来,放到 manager 层或者跟 2L 说的一样,搞一个 common module 。
    2. 但写着写着,可能会出现新的 service 会用到之前 service 的某个方法,然后就开始互相调用了哈哈
    Akitora
        9
    Akitora  
       90 天前
    把需要调用的另一个服务的方法在上一层作为 callback 参数传进服务去
    frank1256
        10
    frank1256  
       90 天前
    无脑用,写完了,不同语言写工具去检测,spring 的 ioc ,或者 go 的 go-wire ,你让人去设计,不如让工具检测来的方便
    Jooooooooo
        11
    Jooooooooo  
       90 天前
    刻意追求这种规定干啥...
    optional
        12
    optional  
       90 天前 via iPhone
    application service 禁止互相调用
    hay313955795
        13
    hay313955795  
       90 天前   ❤️ 2
    ..循环依赖???不是有个懒加载就能解决了吗.现在没问题 能跑就行..等以后出了问题再说呗
    sadfQED2
        14
    sadfQED2  
       90 天前 via Android
    再加一层
    xuanbg
        15
    xuanbg  
       90 天前
    我从来不考虑循环依赖问题,因为我写代码前先画个思维导图。我就问你思维导图怎么产生交叉?
    vczyh
        16
    vczyh  
    OP
       90 天前
    @frank1256 主要是检查出来后又得重新设计,不如在设计之初想想如何避免。
    vczyh
        17
    vczyh  
    OP
       90 天前
    @Jooooooooo 不是追求规定,如果一开始没有规定或者设计不合理,之后大概率出现循环依赖。
    vczyh
        18
    vczyh  
    OP
       90 天前
    @hay313955795 在理哈哈
    Jooooooooo
        19
    Jooooooooo  
       90 天前
    @vczyh 有很多场景循环依赖是最简单合理的设计, 我认为刻意是规避没有意义.
    vczyh
        20
    vczyh  
    OP
       90 天前
    @xuanbg 你其实说到重点了,最根本的就是设计问题,谁依赖谁在设计之初就决定了,如果大家都了解那么没有问题,但是在业务发展的过程中,没有 review 很有可能破坏规则,那么可以可以在工程代码上指定一些规范来尽量避免,比如说:禁止 service 调用 service ?
    frank1256
        21
    frank1256  
       90 天前
    @vczyh 不是额,检查出来就提示哪里出现循环了,改掉那一处就行了,设计之初就想好的话,太考验设计了。代码里噼里啪啦的 service 疯狂创建,来不及设计了
    vczyh
        22
    vczyh  
    OP
       90 天前
    @Jooooooooo spring 是一定程度上是支持的,但是 Golang 是禁止的。
    vczyh
        23
    vczyh  
    OP
       90 天前
    @VeryZero 那这一层一般叫什么呢?
    vczyh
        24
    vczyh  
    OP
       90 天前
    @Kontinue 是的,主要是后面会出现的,前期没有这个问题。
    vczyh
        25
    vczyh  
    OP
       90 天前
    @frank1256 事实确实是你说的那样,很蛋疼。
    libook
        26
    libook  
       90 天前
    同级尽量避免互相调用,看是否重新规划服务层级和为服务划分是否能解决问题。
    vczyh
        27
    vczyh  
    OP
       90 天前
    有没有同学有比较好的实践
    vczyh
        28
    vczyh  
    OP
       90 天前
    @libook 我也是这么认为
    fiypig
        29
    fiypig  
       90 天前
    = = Java 真的会出现这情况,那时候在学就是这样,反倒 go 就不会出现这情况,没那么乱。。。
    ChoateYao
        30
    ChoateYao  
       90 天前
    Module 之间加多一个 转换层把其他 Service 的接口当成远程接口,模块内也不存在 Service 互相调用的情况,如果出现那么一定是业务逻辑抽离不干净。
    Jooooooooo
        31
    Jooooooooo  
       90 天前
    @vczyh 你说的是系统层面的循环调用, 我说的是业务层面的循环调用.

    如果你真想解决问题, 得解决后面这个问题. 前面这个问题更是一个小问题.
    vczyh
        32
    vczyh  
    OP
       90 天前
    @ChoateYao 那意思是
    之前:UserService.get(long id):获取用户信息->根据用户 ID 获取所有订单信息(造成 UserService 依赖 OrderService )
    改成:为了禁止 service 之间调用,在 service 层上加一层,在这一层组合 UserService 和 OrderService
    可以这么理解吗?
    ChoateYao
        33
    ChoateYao  
       90 天前
    @vczyh 你的场景是 OrderService 依赖 UserService ,你的主要场景是订单,不是用户。

    加多一层 ACL (防腐层)

    UserInfoAdpater 主要实现调用 UserService 的 get 方法放在 OrderModule 的 ACL 里面,转换成你想要的 User 对象

    在 OrderService 里面调用 UserInfoAdpater 得到 User 对象之后再查询 User Order Items
    frank1256
        34
    frank1256  
       90 天前
    @ChoateYao 看到 DDD 头疼
    nothingistrue
        35
    nothingistrue  
       90 天前   ❤️ 2
    @vczyh #32 如果 User 依赖 Order ,并且 Order 又反过来依赖 User ,那说明你们并没有把 User 跟 Order 解开。上面再套一层,还是没有解开,治标不治本。而且还是用毒药治那种,因为层是个很重的东西,加一层的成本是很高的。

    相互依赖,一个原因是同层之间不同模块没解开造成的。对于你这个例子,根据用户 ID 获取所有订单信息,这实际上只是 Order 自身的事,跟 User 屁关系都没有。请注意用户 ID 作为不变的值,即使它是通过 User 生成的,它也不属于 User 而是属于全局,或者所有对它感兴趣的模块。

    另一个原因,是根本没用好层造成的。分层既然隔离了技术,那自然要把基于技术的模块划分也给隔离了,不同层的模块划分原则是不一样的。还拿你这个例子来说,根据用户 ID 获取所有订单信息,在 UI 层往往是属于“我的……”这种用户个人资料模块的,但在业务逻辑层,或者数据模型层,它是属于 Order 模型的。
    zhuangzhuang1988
        36
    zhuangzhuang1988  
       90 天前
    直接用 F#
    编译都依赖文件顺序
    vczyh
        37
    vczyh  
    OP
       90 天前
    @nothingistrue
    情况 1:查询用户,带出对应的订单(造成 User 依赖 Order.getListByUserId(long userId)接口)
    情况 2:查询订单,带出用户的某些信息(造成 Order 依赖 User.getSomeInfo(long userId))
    请问这种 service 互相依赖怎么解决?
    urnoob
        38
    urnoob  
       90 天前
    A 和 B 中有循环依赖,把循环依赖的部分放到 A 或者 B ,或者放进新创建的 C
    xuanbg
        39
    xuanbg  
       90 天前
    @nothingistrue 是的,代码没写对地方,加多少层都没卵用。作为一个程序员,把代码写对地方才是第一重要的事。
    lmshl
        40
    lmshl  
       90 天前
    笨方法:延迟初始化
    普通方法:抽出一层
    聪明方法:不要用类来组织它们,自然就不存在循环依赖了
    Chad0000
        41
    Chad0000  
       90 天前
    @vczyh #37
    @ChoateYao #30

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

    如果你说的是:循环调用,A 方法调用了 B 方法,B 方法又调用了 A 方法,那是程序猿的问题,这种东西无法避免的,只能开发者自己小心。
    summerLast
        54
    summerLast  
       89 天前
    设计之初就不允许 service 互相调用,公用逻辑可以在 sevice 下放到 manager , 或在 service 上建个调度层协调 service
    summerLast
        55
    summerLast  
       89 天前
    @vczyh 建议参考领域驱动 17 楼的查询可以 在 order 上增加 user 的一些冗余信息 如 id name ,其他的可以放在 faced 组合 或者 在 application 层进行调度,将服务分成两类 一类是应用服务 面向应用的,一类是领域服务 包含领域知识的,应用服务调度编排领域服务 面向应用
    meiyoumingzi6
        56
    meiyoumingzi6  
       89 天前 via iPhone
    python 表示直接 import locally 不就完了,要啥自行车🤪🤪🤪
    summerLast
        57
    summerLast  
       89 天前
    设计订单的时候可以订单里的用户只是个值对象 而非实体
    wanguorui123
        58
    wanguorui123  
       89 天前
    提前规划化主从 Service ,然后约定
    morty0
        59
    morty0  
       89 天前
    go 直接编译报错
    vczyh
        60
    vczyh  
    OP
       89 天前
    @summerLast 感谢~
    EscYezi
        61
    EscYezi  
       89 天前 via Android
    spring 项目可以统一用构造器注入,出现循环依赖第一时间就能发现,用 autowire 倒是一般不用考虑这个问题,
    secondwtq
        62
    secondwtq  
       89 天前
    确实没有必要追求“完全没有循环依赖”

    循环依赖最简单的形式是单函数的递归,简单的递归等效于简单的循环,也就是说你写个循环就是有循环依赖了
    问题来了,你能不能不写循环 ...
    byte10
        63
    byte10  
       79 天前
    (⊙o⊙)… 这个互相调用是很正常的吧,有来有往。即便以后拆分微服务 也没啥问题啊
    vczyh
        64
    vczyh  
    OP
       79 天前
    @byte10
    没遇到过循环依赖么...
    byte10
        65
    byte10  
       78 天前
    @vczyh 这个 spring 会帮忙解决的,先不考虑。如果拆分微服务的话,还可以加一层,解决服务之间互相依赖,避免升级服务的时候产生矛盾。
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   4486 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 43ms · UTC 01:46 · PVG 09:46 · LAX 17:46 · JFK 20:46
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.