首页 > 其他分享 >用依赖倒置和控制反转,突破Golang循环调用限制之后的思考

用依赖倒置和控制反转,突破Golang循环调用限制之后的思考

时间:2024-07-29 18:55:22浏览次数:17  
标签:调用 实现 代码 领域 Golang 依赖 抽象 article 倒置

在软件开发中,随着项目规模的扩大和业务逻辑的复杂化,重构代码变得越来越重要。本文将介绍如何在既有代码基础上,通过依赖倒置(DIP)和控制反转(IoC),实现新增加的代码可以循环引用到服务层的代码。然后,我们将探讨接口隔离、设计小而清晰的接口和包,以及共同依赖原则等内容。

包引用时的循环依赖问题

在开发服务端代码的时候,我们通常会采用单体分层设计,通常会将大量的领域代码集中在Service层,因为是同一个包,所以互相之间的引用是完全开放的。但是随着项目逐渐变得越来越复杂,无论是否采用微服务和领域驱动,我们总会需要根据领域进行拆分和重构,简化代码的复杂度。

不同于Java,共同依赖原则体现在逻辑上,而Golang的设计理念,是以包为维度组织代码的,引入依赖的的层级就是包而不是单个文件,如果代码出现了包之间物理上的循环引用,在编译阶段就会直接报错。

图片

循环依赖和共同依赖原则

我觉得Golang的这种设计,体现的是面向对象设计中的共同依赖原则,设计的包,应该职责简单清晰,有明确的责任或者功能,因此,依赖了一个包就意味着会依赖包下的所有文件。

基于这个原则,我们常常可以推出两个编程原则。

第一个是,在选择是否依赖某个由其他人或者团队维护的包的时候,要分析这个包中的代码,是否会有其他不可预期的副作用,例如golang的init,或者包内某个文件,依赖的第三方包不够整洁;如果弊大于利,不如自己重写。

第二个是,我们自己在决定某段代码、函数或者类应该放在什么路径(包)的时候,要仔细的设计,是否自己跟当前的包足够内聚,语义匹配,新代码依赖的包或者库,有没有引入破坏当前所在包承诺的代码逻辑或者副作用。

一次代码重构经历

在一次重构代码经历中,我们计划开始渐进式地逐步将大型单体项目拆分成一些微服务,并且对单体代码库的代码进行优化,逐步的将可单独治理的基础设施,根据不同的领域,从大的BFF单体服务中抽离出去。

刚接触的时候,代码是很常见的简单分层架构,分层三层,接口(控制器)层、Service(复用的业务逻辑)层、数据持久化层。接口层的话常规上是一次性的复用可能性低的代码,处理网络请求、验证和响应数据的拼装;数据持久层一般是比较薄的一层代码,是对数据库访问的映射,将业务逻辑的语义翻译成数据引擎的语法,可能会有一些缓存和实体对象的转换。

最复杂的通常会是Service层的代码,在项目的探索阶段往往领域不清晰,资源有限,最简单的方法,是把复用的业务逻辑代码放在同一个包下面,以文件名和Struct为维度,作为相关领域函数的命名空间。

要抽离代码,首先找到一个相对独立,能够内聚的领域,我们首先要把这个领域对应的核心代码,抽离到一个单独的包下面,然后需要聚合数据或者处理某个大的业务逻辑的时候,通过领域包提供的方法,对领域的业务逻辑进行调用。

我们碰到的第一个问题是,因为驱动领域边界的最主要力量,是产品和业务,为了服务治理拆分的服务,往往互相之间不可避免会出现互相依赖,尤其是在BFF的代码库中。

Service层的代码会依赖特定领域的包,而特定领域的代码,也有很多逻辑,需要依赖目前依然在Service的代码。在Golang语言层面,当然不会允许循环调用。

突破循环调用的抽象

我们想到的是依赖倒置原则,要求我们在写代码的时候,高层不依赖于底层、底层也不依赖高层,他们都依赖于抽象,由使用者自己定义或者选择符合自己要求的接口,然后由服务提供方实现接口。

新实现的单独领域代码,可以遵循很多Golang类库的最佳实践,定义一个主包,主包只定义抽象接口,然后定义一个工厂方法和单例。

// ./domain/articletype SaveAndPublishArticleParam struct {    User *types.User    Draft *ArticleDraft    Action string}type ArticleDomain interface {    GetArticleById(ctx context.Context, id string) (*ArticleEntity, error)    SaveAndPublishArticle(ctx context.Context, id string, param *SaveAndPublishArticleParam) (*ArticleEntity, error)}type UserDomain interface {    GetUserById(ctx context.Context, id string) (*types.User, error)}// ./domain/article/factoryfunc CreateArticleDomain(userRepo article.UserDomain) (article.ArticleDomain, error) {    ad = *logic.SiteArticle{        User: userRepo,    }    return ad, nil}

其中的UserDomian,就是article这个领域定义的自己要完成业务逻辑,需要用户领域能提供的能力。UserDomain接口的方法,本来就是Service层已经实现的,所以只需要将service层的user对象直接传进来,就可以实现访问。

最终达到的目标是,service层依赖的是article包定义的抽象接口,而article包接口的实现类,依赖的也是article包定义的抽象,无论article的实现类还是service层都依赖的是抽象,而不是依赖具体实现。

定义好抽象和实现类之后,还差一步,才能实现运行时的有效调用,这个时候就是控制反转登场的时候,我们需要在一个不会被service层和article领域依赖的包中,初始化Service层的对象和article领域的实现类,通过调用各自提供的绑定方法,完成抽象依赖到运行时实现对象的绑定。完成了这一步,在静态代码层面,就解除了循环依赖。​​​​​​​

// ./serverfunc StartServer(ctx context.Context) error {    ur := service.NewUserService(ctx, ....)    ...    ad, _ := articlefactory.CreateArticleDomain(ur)    ur.BindArticleDomain(ad)    ...    runHttpServer(ctx, ...)}

通过示例,我们看到了,只需要通过很简单的代码依赖注入和控制反转的方法,就可以为代码带来很大的灵活性,使代码结构变得更加有弹性。

我们编程的时候,也要透过现象看到本质,在我们重构的代码中,运行时的控制流中,我们关注的应该是函数的循环调用,只有在函数之间互现嵌套调用,才真正有可能发生死循环之类的问题。

而静态代码层面,通过限制包层面的循环引用,更大限度的避免了不可预期的死循环发生,无论静态层面是否有显式循环依赖,运行时都有可能产生循环调用。

回顾依赖倒置和控制反转

依赖倒置原则包含两条主要规则:

1. 高层模块不应该依赖于低层模块。二者都应该依赖于抽象。

2. 抽象不应该依赖于具体实现。具体实现应该依赖于抽象。

换句话说,依赖倒置原则的核心思想是通过依赖抽象(如接口或抽象类)来实现模块之间的依赖,而不是直接依赖具体实现。

通过依赖抽象,模块之间的依赖关系变得更加松散。如果具体实现发生变化,只需要修改依赖于抽象的部分,而不需要修改依赖于具体实现的模块。由于高层模块依赖于抽象,可以轻松地替换或扩展具体实现,而不影响高层模块的功能。依赖抽象使得高层模块更容易进行单元测试,因为可以方便地替换具体实现为模拟对象(Mock)。

控制反转是一个广义的设计原则,它指的是将对象的创建和管理控制权从应用程序代码中移交给一个外部容器或框架。

通过这种方式,对象不再自行控制其依赖对象的创建和管理,而是由IoC容器来完成。这种方式反转了传统的控制流,因此称为“控制反转”。

依赖对象由IoC容器注入,减少了类与类之间的直接依赖,降低了代码耦合度。可以更容易地替换依赖对象,实现模块化开发和系统的灵活扩展。

用依赖倒置和控制反转,突破Golang循环调用限制之后的思考icon-default.png?t=N7T8https://mp.weixin.qq.com/s/ZfXabzh0I8hBcKmpQMu2JQ

标签:调用,实现,代码,领域,Golang,依赖,抽象,article,倒置
From: https://blog.csdn.net/liuwill/article/details/140778509

相关文章

  • 易优CMS模板标签uibackground背景图片在模板文件index.htm中调用uibackground标签,实现
    【基础用法】标签:uibackground描述:背景图片上传标签,使用时结合html一起才能完成可视化布局,只针对具有可视化功能的模板。用法:<divclass="eyou-edit"e-id="文件模板里唯一的数字ID"e-page='文件模板名'e-type="background"style="background-image:url({eyou:uibackgrounde......
  • Cmake配置Qt程序调用python库的配置方法
    在网上找了一些配置方法,最简单直接的是在cmake中加入如下语句:set(PYTHON_EXECUTABLE/Python/Python311/python.exe)include_directories("/PythonPython311/include")link_directories("/PythonPython311/libs")link_libraries(python3.lib)link_libraries(python311.lib)直......
  • pygame 文件调用 Pygame 文件时出现 MouduleNotFound 错误
    我遇到的错误如下:ModuleNotFoundError:Nomodulenamed'pygame.rect'我将错误追溯到sprite.py(一个pygame文件),它试图导入文件rect.py。这是我的文件路径:文件路径白色圈出的文件是尝试导入“rect.py”的文件。我能找到的唯一类似文件是用红色圈出的文件“r......
  • vue js事件方法调用api并返回值
    //确认confirm(id){if(this.isEdit&&this.form.list!==undefined&&this.form.list.length<=0){this.$notify({title:'表身为空时不能确认',type:'error'})}else{this.confirmLoading=trueinCo......
  • 日常学习--调用第三方接口和提供第三方接口时的注意事项--20240728
    1、调用第三方接口的注意事项   接口测试与验证:对第三方接口进行充分的测试,包括功能测试、性能测试和安全测试,确保接口的稳定性和安全性。 验证接口的可用性,包括接口地址、请求方式、请求参数、响应格式等是否正确。   参数校验与日志记录:在调用接口前,对请求......
  • 【Golang 面试 - 进阶题】每日 3 题(三)
    ✍个人博客:Pandaconda-CSDN博客......
  • 树莓派3b+使用官方屏幕后倒置问题——屏幕倒置
    树莓派3b+的屏幕本身就是倒置的,因此为了使树莓派在官方屏幕下能显示正常的屏幕画面因此需要通过设置把树莓派的官方屏幕的输出倒置一下,这样树莓派的官方屏幕的输出就是正常的了。解决方法:(源自:https://blog.csdn.net/t13506920069/article/details/121359178)官方正版屏幕旋转......
  • Linux内核-异常输出调用栈CallTrace与Ftrace工具集
    1dump_stack函数打印内核调用堆栈。举个例子:我们定义四个函数aaa、bbb、ccc、ddd,然后bbb中调用aaa,ccc中调用bbb,ddd函数谁都不调用。在入口函数中,我们调用ccc与ddd函数,看看堆栈打印效果如何:#include<linux/module.h>#include<linux/kernel.h>#include<linux/init.h>#incl......
  • 使用 Axios HTTP 客户端通过 GET 请求调用 FastAPI 后端时收到 422 错误响应
    我正在尝试从我的Vue3客户端(使用GETJavaScriptHTTP客户端)向FastAPI服务器发出axios请求,但我收到422unprocessableentity错误响应。据我了解,这种错误消息与错误的输入有关。这是vue3代码:import{authToken}from"../main.js"expo......
  • 利用Java调用银行卡二要素接口
    一、什么是银行卡二要素?银行卡二要素验证接口是一种API接口,主要用于验证用户提供的银行卡号与姓名这两个要素是否一致。二、银行卡二要素作用及场景有哪些?其作用是通过核验用户的身份信息,判断是否为目标用户本人操作,常用于安全级别要求一般的使用场景,具体应用场景包括:1.......