概述
在上一篇笔记中我们记录了依赖注入的概念,简单来说就是不通过new()的方式在类内部创建依赖对象,而是将依赖对象在外部创建好之后,通过构造函数、函数参数等方法传递给类使用。在项目中的对象会随着项目规模越来越庞大,对象之间越来越复杂,出现对象之间的多重依赖关系,对象之间耦合度过高的问题。说到对象耦合度过高,面向接口编程的设计原则主要用于实现松耦合的代码结构,我们这片笔记来记录一下控制反转和面向接口编程,用这两种思路修改之间的项目代码。
IOC就是 of 的缩写ioc控制反转,就是控制反转。在项目没有使用IOC之前,对象D依赖对象C,对象C依赖对象B,对象B依赖对象A,这种称为依赖发现。IOC的思想是借助"第三方"实现具有依赖关系对象之间的解耦。在引入IOC容器之前,对象B依赖于对象A,那么需要主动创建对象A提供ioc控制反转,创建和使用对象A的方式都由程序来控制执行。在引入IOC容器后,对象A、B、C、D所需要的对象都由IOC容器提供,此时对象ABCD之间失去了直接联系,当对象B需要对象A时IOC容器会创建一个对象A注入到对象B需要的地方。因此IOC容器就好像"粘合剂"一样,同时我们可以看出之前对象B需要对象A从主动创建对象A,到IOC容器主动创建对象A注入到对象B所需要的地方,控制权反转来过来所以称为控制反转。
面向接口编程是一种设计原则,旨在实现松耦合的代码结构。通过定义接口我们可以将不同模块之间的依赖关系降到最低,从而使代码具有灵活性和可维护性。例如说之前数据本来在Mysql中存得好好的,老板不知道从哪里听来说数据放在redis贼快,虽然改起来麻烦但为了保住饭碗你还是照做了,到了第二天老板跟你说还是存Mysql稳妥一点改回来吧。遇到这种情况面向接口编程思路是你早就知道需求总是要变动的。
修改项目
我们从用户注册业务的流程修改代码。首先我们将第三方依赖通过ioc来初始化,例如说Mysql redis的初始化;然后还有Gin以及其中间件的初始化(注册路由)。
type UserHandler struct {
svc service.UserService
}
xxx
// 注册路由
func (h *UserHandler) RegisterRoutes(server *gin.Engine) {
ug := server.Group("/v2/users/")
ug.POST("/signin", h.SignIn)
ug.POST("/loginin", h.Login)
ug.POST("/loginsms/code/send", h.SendSMS)
ug.POST("/loginsms", h.VerifySMS)
}
然后就是正常的业务逻辑,从的结构体中写上,在中定义好各种接口,如这里写的是方法的实现,继续执行到层,再到dao层。
// Serivce层
type UserService interface {
Signup(ctx context.Context, u domain.User) error
Login(ctx context.Context, email string, password string) (domain.User, error)
FindOrCreate(ctx context.Context, phone string, id string) (domain.User, error)
}
xxx
func (svc *userService) Signup(ctx context.Context, u domain.User) (err error) {
id := snowflake.GenId()
hashStr, err := bcrypt.GetPwd(u.Password)
if err != nil {
return err
}
u.Password = hashStr
u.ID = strconv.Itoa(id)
return svc.repo.Create(ctx, u)
}
// repository层
type UserRepository interface {
Create(ctx context.Context, u domain.User) error
FindByEmail(ctx context.Context, email string) (domain.User, error)
FindByPhone(ctx context.Context, phone string) (domain.User, error)
}
func (repo *CacheUserRepository) Create(ctx context.Context, u domain.User) (err error) {
return repo.dao.Insert(ctx, u)
}
到这里大家可以注意一下了,dao层就开始操作数据库了。如下面这里是用Mysql实现的。那如果要改成其他数据库实现呢?
// dao层
type UserDAO interface {
Insert(ctx context.Context, u domain.User) error
FindByEmail(ctx context.Context, email string) (domain.User, error)
FindByPhone(ctx context.Context, phone string) (domain.User, error)
}
func NewUserDAO(db *gorm.DB) UserDAO {
return &GORMUserDAO{
db: db,
}
}
func (dao *GORMUserDAO) Insert(ctx context.Context, u domain.User) (err error) {
if err = dao.db.WithContext(ctx).Create(&u).Error; err != nil {
log.Println(err)
return err
}
return nil
}
此时如果要将数据存到redis当中,只需要改一处的代码,修改dao层接口的实现就可以了,之前层、层的代码都不需要改了。
type RedisUserDAO struct {
rdb *redis.Cmdable
}
func NewRedisUserDAO(rdb *redis.Cmdable) *RedisUserDAO {
return &RedisUserDAO{rdb: rdb}
}
func (rdb *RedisUserDAO) Insert(ctx context.Context, u domain.User) (err error) {
xxx
}
func (rdb *RedisUserDAO) FindByEmail(ctx context.Context, u domain.User) (domain.User, error) {
xxx
}
func (rdb *RedisUserDAO) FindByPhone(ctx context.Context, u domain.User) (domain.User, error) {
xxx
}
写在最后
本人是新手小白,如果这篇笔记中有任何错误或不准确之处,真诚地希望各位读者能够给予批评和指正。谢谢!练习的代码放在这里--↓
https://github.com/FengZeHe/LearnGolang/tree/main/project/BasicProjectV2