浅谈依赖注入与控制反转
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