控制反转

IoC 简介

控制反转和依赖注入的主要目标是移除应用程序的依赖关系。这使得系统更加解耦和可维护。

依赖注入是一个适用于任何编程语言的概念。依赖注入背后的通用概念称为控制反转。根据这个概念,结构体不应该静态配置其依赖项,而应该从外部进行配置。

依赖注入设计模式允许我们移除硬编码的依赖关系,使我们的应用程序松散耦合、可扩展和可维护。

依赖注入

Hiboot 最重要的特性之一是依赖注入。Hiboot 实现了 JSR-330 标准。

依赖注入提供对象所需的依赖项。依赖项不是自己构造的,而是通过某种外部方式注入的。

基础示例

package service

import "github.com/hidevopsio/hiboot/pkg/app"

type UserRepository interface {
	FindById(id uint64) (*User, error)
}

type UserService struct {
	repository UserRepository
}

func init() {
	app.Register(newUserService)
}

func newUserService(repository UserRepository) *UserService {
	return &UserService{
		repository: repository,
	}
}

func (s *UserService) GetUser(id uint64) (*User, error) {
	return s.repository.FindById(id)
}

构造函数注入(推荐)

构造函数注入是 Hiboot 推荐的方式。它有以下优点:

  • 可测试性:易于使用 Mock 实现单元测试
  • 语法验证:IDE 可以验证类型并避免拼写错误
  • 不可变性:依赖项在构造后无法更改
  • 完整性:确保所有必需的依赖项都已设置

示例

package controller

import (
	"github.com/hidevopsio/hiboot/pkg/app"
	"github.com/hidevopsio/hiboot/pkg/at"
	"github.com/hidevopsio/hiboot/pkg/model"

	"myapp/service"
)

type userController struct {
	at.RestController
	at.RequestMapping `value:"/user"`

	userService *service.UserService
}

func init() {
	app.Register(newUserController)
}

// 依赖项通过构造函数参数注入
func newUserController(userService *service.UserService) *userController {
	return &userController{
		userService: userService,
	}
}

func (c *userController) GetById(id uint64) (model.Response, error) {
	user, err := c.userService.GetUser(id)
	response := new(model.BaseResponse)
	response.SetData(user)
	return response, err
}

字段注入

字段注入通过 inject:"" 结构体标签触发。当字段上存在此标签时,Hiboot 会尝试根据字段类型解析要注入的对象。

基础字段注入

type userController struct {
	at.RestController

	UserService *service.UserService `inject:""`
}

func newUserController() *userController {
	return &userController{}
}

func init() {
	app.Register(newUserController)
}

命名注入

如果同一接口有多个实现,可以使用字段名或标签值来区分:

type AuthService interface {
	Authenticate(username, password string) bool
}

// AuthService 的两个实现
type basicAuthService struct{}
type oauth2AuthService struct{}

type authController struct {
	at.RestController

	// 通过字段名注入(匹配 "basicAuthService")
	BasicAuthService AuthService `inject:""`

	// 通过字段名注入(匹配 "oauth2AuthService")
	Oauth2AuthService AuthService `inject:""`

	// 通过显式名称注入
	PrimaryAuth AuthService `inject:"basicAuthService"`
}

值注入

使用 value:"" 标签注入配置值:

type MyService struct {
	AppName  string `value:"${app.name}"`
	Port     int    `value:"${server.port}"`
	LogLevel string `value:"${logging.level:info}"` // 带默认值
}

默认值注入

使用 default:"" 标签指定默认值:

type Config struct {
	Timeout  int    `default:"30"`
	Host     string `default:"localhost"`
	Enabled  bool   `default:"true"`
}

方法注入

方法注入用于自动配置。依赖项通过方法参数注入:

package myconfig

import "github.com/hidevopsio/hiboot/pkg/app"

type configuration struct {
	app.Configuration
}

func init() {
	app.Register(newConfiguration)
}

func newConfiguration() *configuration {
	return &configuration{}
}

// Database 通过方法参数注入
func (c *configuration) UserRepository(db *Database) UserRepository {
	return &mysqlUserRepository{db: db}
}

// UserService 依赖于 UserRepository
func (c *configuration) UserService(repo UserRepository) *UserService {
	return &UserService{repository: repo}
}

注册

所有可注入的组件必须使用 app.Register() 注册:

func init() {
	// 注册构造函数
	app.Register(newUserService)
	app.Register(newUserController)

	// 一次注册多个
	app.Register(newFoo, newBar, newBaz)
}

注入规则

  1. 指针类型:依赖项必须是指针类型或接口
  2. 必须注册:组件必须通过 app.Register() 注册
  3. 无循环依赖:避免循环依赖链
  4. 接口解析:如果存在多个实现,使用命名来区分

有效

type Foo struct{}
type Bar struct {
	foo *Foo // 指针 - 会被注入
}

无效

type Foo struct{}
type Bar struct {
	foo Foo // 非指针 - 不会被注入
}

使用依赖注入进行测试

构造函数注入使测试变得简单:

package controller

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

// Mock 实现
type mockUserService struct {
	mock.Mock
}

func (m *mockUserService) GetUser(id uint64) (*User, error) {
	args := m.Called(id)
	return args.Get(0).(*User), args.Error(1)
}

func TestUserController_GetById(t *testing.T) {
	// 创建 Mock
	mockService := new(mockUserService)
	mockService.On("GetUser", uint64(1)).Return(&User{Name: "Test"}, nil)

	// 通过构造函数注入 Mock
	controller := newUserController(mockService)

	// 测试
	response, err := controller.GetById(1)
	assert.NoError(t, err)
	assert.NotNil(t, response)

	mockService.AssertExpectations(t)
}

下一步