likes
comments
collection
share

浅谈依赖注入与控制反转

作者站长头像
站长
· 阅读数 30

DI(依赖注入)

先来看下面这段代码,主函数中,我们通过GetUserInfo()函数获取用户信息,GetUserInfo()函数通过,新建数据库连接实例->查询数据库,获取所需数据。

func main() {
  // i want go get userinfo
  userInfo,err := GetUserInfo()
  if err!=nil {
    panic(err)
  }
  fmt.Println(userInfo)
}

func GetUserInfo() (info *res.UserInfo, err error) {
    db,err  := gorm.Open(mysql.Open("root:123456@tcp(127.0.0.1:3306)/gvb?charset=utf8mb4&parseTime=True&loc=Local"))

    var user entity.AdminUser

    if err = db.Select("name", "avatar").First(&user).Error; err != nil {
        return nil, err
    }
    return &res.UserInfo{
        Username: user.Name,
        Avatar:   user.Avatar,
    }, nil
}

假如我们需要多次调用GetUserInfo()函数,显然,每次调用时,都会初始化一个数据库连接实例,而与数据库连接是一个耗时的io操作。

假如我们不止一个GetUserInfo()函数,而还有其他的从数据库中操作的函数,而某一天数据库更改密码或用户名,这样,我们不得不将所有的数据库操作函数都进行更改,这显然是非常麻烦的。

而且,如果我们想要测试GetUserInfo()函数,而又希望在测试环境的数据库进行,又不得不去函数内部改来改去,十分麻烦。

db是一个可复用连接实例,也就是说,我们可以仅初始化一个db,在多次调用的过程中重复使用。因此,我们不在函数内部初始化连接,而是将db初始化在函数外部,并作参数传递进入函数。

这种将函数内部所需组件(依赖)作为参数传递的方式,我们称之为DI(依赖注入)。


func main() {
  db,err  := gorm.Open(mysql.Open("root:123456@tcp(127.0.0.1:3306)/gvb?charset=utf8mb4&parseTime=True&loc=Local"))
  userInfo,err := GetUserInfo(db)
  if err!=nil {
    panic(err)
  }
  fmt.Println(userInfo)
  userInfo,err := GetUserInfo(db)
  if err!=nil {
    panic(err)
  }
  fmt.Println(userInfo)
  userInfo,err := GetUserInfo(db)
  if err!=nil {
    panic(err)
  }
  fmt.Println(userInfo)
}

func GetUserInfo(db *gorm.DB) (info *res.UserInfo, err error) {
    var user entity.AdminUser

    if err = db.Select("name", "avatar").First(&user).Error; err != nil {
        return nil, err
    }
    return &res.UserInfo{
        Username: user.Name,
        Avatar:   user.Avatar,
    }, nil
}

DI模式中,方法只专注于调用所依赖的组件方法,而无需关心组件如何实现,从何而来。

更进一步,按照经典OOP(面向对象)设计思想,我们可以将函数和依赖封装到一起,在类的内部,使用构造函数将方法(函数)所需依赖注入到类的成员变量中,方法无需通过参数获取依赖,而是直接调用成员变量即可。

func main() {
    db,err  := gorm.Open(mysql.Open("root:123456@tcp(127.0.0.1:3306)/gvb?charset=utf8mb4&parseTime=True&loc=Local"))
    userRepo := NewUserRepo(db)
    userInfo,err := userrepo.GetInfo()
    if err!=nil {
      panic(err)
    }
    fmt.Println(userInfo)
    userInfo,err := userrepo.GetInfo()
    if err!=nil {
      panic(err)
    }
    fmt.Println(userInfo)
    userInfo,err := userrepo.GetInfo()
    if err!=nil {
      panic(err)
    }
    fmt.Println(userInfo)
}

type UserRepo struct {
    db *gorm.DB
}

func NewUserRepo(db *gorm.DB) *UserRepo {
    return &UserRepo{
        db: db,
    }
}

func (u *UserRepo) GetInfo() (info *res.UserInfo, err error) {
    var user entity.AdminUser
    if err = u.db.Select("name", "avatar").First(&user).Error; err != nil {
        return nil, err
    }

    return &res.UserInfo{
        Username: user.Name,
        Avatar:   user.Avatar,
    }, nil
}

Interface(接口)

上面提到,组件无需关心所依赖的组件如何构造,而只调用其方法,由此,我们思考这样一个场景。

很多云服务厂商都提供了自然语言模型API接口供开发者调用,比如,我想给我的个人博客实现一个自动生成文章摘要的功能。

定义一个接口,接口类似于一个通用组件,不关心组件所需依赖是什么,只关心组件是否包含所需方法,只要另一个组件实现了接口内定义的方法,就可以成为此接口这样的通用组件。

依赖注入的核心思想恰好也是只调用组件方法,不关注如何实现。

package inter

// ai接口
type AiService interface {
    GetSummary(content string) (string, error) // ai获取文章摘要
}

在我的Service组件中依赖这个接口

type Service struct {
    UserService     *UserService
    ArticleService  *ArticleService
    CateService     *CateService
    TagService      *TagService
    SiteInfoService *SiteInfoService
    AiService       inter.AiService  // AI接口
}

调用此接口的实现方法

// 生成摘要并加载到搜索引擎
func (a *ArticleService) GenerateSummary(uuid uint32, content string) {
    summary, err := Svc.AiService.GetSummary(content)
    if err != nil {
        global.Log.Warn(fmt.Sprintf("Article generate summary err:%v uuid:%d", err, uuid))
        return
    }

    if err := a.articleRepo.UpdateSectionByUuid(uuid, "summary", summary); err != nil {
        global.Log.Warn(fmt.Sprintf("Article gpdate summary err:%v uuid:%d", err, uuid))
        return
    }
    if err := a.articleRepo.UpdateSummarySearch(uuid, summary); err != nil {
        global.Log.Warn(fmt.Sprintf("Article update summary to meilisearch  err:%v uuid:%d", err, uuid))
        return
    }
}

让腾讯混元大模型实现这个接口

type AiHunyuan struct {
    Client  *hunyuan.Client
    Request *hunyuan.ChatCompletionsRequest
}

const endpointURL = "hunyuan.tencentcloudapi.com"

const systemStr = "system"
const userStr = "user"

const systemSummaryContent = "我是博客博主 我会给你一篇文章 以博主为主语 返回我文章摘要即可 字数在200左右"

func NewAiHunyuan(config config.AiHunyuan) *AiHunyuan {
    if config.SecretId == "" || config.SecretKey == "" {
        panic("ai hunyuan config is empty")
    }

    credential := common.NewCredential(
        config.SecretId,
        config.SecretKey,
    )
    // 实例化一个client
    cpf := profile.NewClientProfile()
    cpf.HttpProfile.Endpoint = endpointURL
    client, err := hunyuan.NewClient(credential, "", cpf)
    if err != nil {
        panic(err)
    }

    // 创建请求对象
    request := hunyuan.NewChatCompletionsRequest()
    request.Model = common.StringPtr(config.Model)
    return &AiHunyuan{
        Client:  client,
        Request: request,
    }
}
func (a *AiHunyuan) GetSummary(articleContent string) (string, error) {
    a.Request.Messages = []*hunyuan.Message{
        {
            // 系统角色 获取文章摘要提示词设定
            Role:    common.StringPtr(systemStr),
            Content: common.StringPtr(systemSummaryContent),
        },
        {
            // 用户角色 输入文章内容
            Role:    common.StringPtr(userStr),
            Content: common.StringPtr(articleContent),
        },
    }
    response, err := a.Client.ChatCompletions(a.Request)
    if err != nil {
        return "", err
    }
    if response.Response != nil {
        return *response.Response.Choices[0].Message.Content, nil
    } else {
        return "", errors.New("response is nil")
    }
}

Go语言中,接口的实现是隐式的,没有类似Java的implements关键字显式实现接口,一个结构体实现接口的全部方法,即可以成为该接口类型。

我们可以把AiHunyuan注入到Service中

    aiSvc := NewAiHunyuan(aiConfig.Hunyuan)

    return &Service{
        UserService:     NewUserService(userRepo),
        ArticleService:  NewArticleService(articleRepo, tagRepo),
        CateService:     NewCateService(cateRepo),
        TagService:      NewTagService(tagRepo),
        SiteInfoService: NewSiteInfoService(db, cache, siteInfoPath),
        OssService:      ossSvc,
        AiService:       aiSvc,
    }

如果想更换其他家的大模型,只需实现同样的方法后注入即可,甚至可以根据配置文件指定注入哪个实现,无需更改service内部任何代码,实现解耦。

IOC(控制反转)

上文中,我们成功抽象出了一个UserRepo对象,但是在实际开发中,类似的对象可能有很多个,如果我们从配置文件开始从上手动构建整个依赖,会显得很复杂,比如像下面这样。

var Svc *Service

type Service struct {
    UserService     *UserService
    ArticleService  *ArticleService
    CateService     *CateService
    TagService      *TagService
    SiteInfoService *SiteInfoService
    OssService      inter.OssService // OSS接口
    AiService       inter.AiService  // AI接口
}

func New(config config.Config) *Service {
    db := core.NewGormDB(config.Mysql)
    search := core.NewMeiliSearchClient(config.Meilisearch)
    cache := core.NewRedisCache(config.Redis)

    userRepo := dao.NewUserRepo(db)
    articleRepo := dao.NewArticleRepo(db, cache, search)
    tagRepo := dao.NewTagRepo(db)
    cateRepo := dao.NewCateRepo(db)

    //预留接口 实现可拓展 可选择不同Oss服务注入
    ossSvc := NewOssQinui(config.Oss.OssQiniu)
    aiSvc := NewAiHunyuan(config.Ai.Hunyuan) 

    siteInfoPath := config.Sys.SiteInfoPath

    return &Service{
        UserService:     NewUserService(userRepo),
        ArticleService:  NewArticleService(articleRepo, tagRepo),
        CateService:     NewCateService(cateRepo),
        TagService:      NewTagService(tagRepo),
        SiteInfoService: NewSiteInfoService(db, cache, siteInfoPath),
        OssService:      ossSvc,
        AiService:       aiSvc,
    }
}

经典的Web项目结构中,我们经常采用分层构建依赖,Controller层的组件依赖Service,Service层的组件依赖于Dao层组件,Dao层组件又依赖于配置文件完成数据库连接初始化。

浅谈依赖注入与控制反转

无数组件共同构成了整个系统,很多组件可能会被多个组件所依赖,这可能会导致过耦合,手动注入起来十分麻烦,解决这一问题的核心方案就是IOC(控制反转)。

在Java的Spring框架中,所有被标记为组件的类可以被扫描并通过xml文件或者注解标识被构造,并根据相互依赖关系构建出整个系统,将组件的创建+配置与组件的使用相分离,并且,由IoC容器负责管理组件的生命周期。

例如,我们可以定义一个Spring配置类,并添加一个组件扫描包,Spring将扫描包下所有带有@Component注解的类,构造并放入IOC容器中。

package org.example.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("org.example.entity")
public class MainConfiguration {

}
package org.example.entity;

import org.springframework.stereotype.Component;

@Component
public class Student {
}

可以在Context上下文中获取到Student这个类。

package org.example;

import org.example.config.MainConfiguration;
import org.example.entity.Student;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(MainConfiguration.class);
        Student student = context.getBean(Student.class);
        System.out.println(student);
    }
}

在Spring的IoC容器中,我们把所有组件统称为JavaBean,即配置一个组件就是配置一个Bean。

谷歌官方也提供了Go语言的依赖注入框架,wire,不过并非是通过容器实现的,而是通过代码生成的方式,当时其核心思想(将组件实现和依赖注入交给程序执行而不是程序员自己)是相同的。

转载自:https://juejin.cn/post/7397597730632024100
评论
请登录