【kratos入门实战教程】番外篇之充血模型(1)

2023-10-08 21:59

本文主要是介绍【kratos入门实战教程】番外篇之充血模型(1),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前情回顾

在前篇文章中提到了充血模型和贫血模型的概念,本篇文章将会讨论充血模型在注册/登陆业务中的应用,分析充血模型是如何分离业务和策略的。如果读者对充血模型、贫血模型等概念不熟悉,建议先翻阅相关的资料,对相关概念有大致的轮廓会对读者更有帮助。

开干

登陆业务流程分解

业务流程只是一个需求实现的大致流程,到了具体实现的时候还会有其他的操作流程的。一般的登陆认证流程大致能抽象成如图所示的流程:
在这里插入图片描述
但是回顾我们在biz层实现的时候,还做了参数校验、获取用户信息、加密密码等操作。这种就属于是流水账式的编程,在业务简单的时候可以这样一把梭。但是业务复杂的时候,一个方法就会很庞大,代码就会很臃肿,维护起来也会很困难。

让实现流程更接近业务流程

目前的贫血模型实现的流程如下:
在这里插入图片描述
从图中可以看出,其实很接近业务的流程了。接下来开始改造代码,我们实现充血模型代替目前的贫血模型。如下所示:
在这里插入图片描述

登录参数封装成对象

我们在biz里添加一个LoginRequest的对象,用来封装登录的参数。对象的属性是私有的,通过提供的构造方法来构造对象。然后参数的校验逻辑就可以前置到对象的构造方法中,如下所示:

type LoginRequest struct {username stringpassword string
}func NewLoginRequest(username, password string) (*LoginRequest, error) {// 校验参数if username == "" {return nil, fmt.Errorf("用户名不能为空")}if password == "" {return nil, fmt.Errorf("密码不能为空")}return &LoginRequest{username: username,password: password,}, nil
}

引入用户领域对象

参考DDD的设计,我们把User对象改成充血模型,增加校验密码的方法作为User对象的行为,把凭证校验的功能流程交给User对象来做。如下所示:

type User struct {ID       int64  // 用户IDUsername string // 用户名Password string // 密码Nickname string // 昵称Avatar   string // 头像
}func (u *User) CheckAuth(ctx context.Context, password string, encryptService EncryptService) error {// 校验参数if username == "" {return nil, ErrMissingUsername}if password == "" {return nil, ErrMissingPassword}return &LoginRequest{username: username,password: password,}, nil
}

EncryptService改造成领域服务

有一些操作是不归属于任何的领域对象的,并且是过程式无状态的,那么这种操作可以归属领域服务。例如这种的签发令牌的逻辑,可以迁移到EncryptService中,作为一种服务向外暴露。如下所示:

type EncryptService interface {Encrypt(ctx context.Context, target []byte) (result []byte, err error)// Token 签发tokenToken(ctx context.Context, user *User) (string, error)
}type encryptServiceImpl struct {authConfig *conf.Auth
}func NewEncryptService(authConfig *conf.Auth) EncryptService {return &encryptServiceImpl{authConfig: authConfig,}
}func (e *encryptServiceImpl) Encrypt(ctx context.Context, target []byte) (result []byte, err error) {encodeToString := base64.StdEncoding.EncodeToString(target)return []byte(encodeToString), nil
}func (e *encryptServiceImpl) Token(ctx context.Context, user *User) (string, error) {claims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ExpiresAt: jwt.NewNumericDate(time.Now().Add(e.authConfig.GetExpireDuration().AsDuration())), // 设置token的过期时间})return claims.SignedString([]byte(e.authConfig.GetJwtSecret()))
}

最后改造登录用例

在上述步骤都完成后,我们就可以改造登录的用例的,如下所示:

//Login 登录,认证成功返回token,认证失败返回错误
func (a *AccountUseCase) Login(ctx context.Context, loginReq *LoginRequest) (token string, err error) {// 获取用户信息user, err := a.userRepo.FetchByUsername(ctx, loginReq.username)if err != nil {return "", fmt.Errorf("登录失败:%w", err)}// 校验密码err = user.CheckAuth(ctx, loginReq.password, a.encryptService)if err != nil {return "", fmt.Errorf("登录失败:%w", err)}// 生成tokentoken, err = a.encryptService.Token(ctx, user)if err != nil {a.logger.Errorf("登录失败,生成token失败:%v", err)return "", fmt.Errorf("登录失败")}return token, nil
}

整个登录的逻辑缩短成简简单单的19行代码,没有原本的流水账,取而代之的是接近业务流程的步骤。这样的代码一目了然,可读性、可测试、可运维性都能得到保证(登录这个例子比较简单,所以这里的改造看不出有多少收益)。改完用例后,我们还需要改service层,在service层构造LoginRequest对象,在service做参数错误的处理。如下所示:

func (a *accountService) Login(ctx context.Context, request *v1.LoginRequest) (*v1.LoginResponse, error) {loginRequest, err := biz.NewLoginRequest(request.GetPhone(), request.GetPassword())if err != nil {return nil, errors.New(500, "登录失败", err.Error())}token, err := a.auc.Login(ctx, loginRequest)if err != nil {return nil, errors.New(500, "登录失败", err.Error())}return &v1.LoginResponse{Token: token,}, nil
}

单元测试

下面我们来看看改造后的代码怎么写单元测试。

参数校验

首先,我们先做参数校验的单元测试。贫血一把梭的写法需要直接对处理登录业务的方法进行测试。但是这里我们只需要对包含了参数校验的构造登录请求参数的方法进行测试。换言之,把原本的逻辑进一步拆分成更细的单元。新建account_test.go测试文件,编写三个测试用例:缺少用户名、缺少密码和参数齐全。如下所示:

func TestNewLoginRequest(t *testing.T) {data := []struct {name     stringusername stringpassword stringwantErr  errorwantData *LoginRequest}{{name:     "缺少用户名",password: "123456",wantErr:  ErrMissingUsername,},{name:     "缺少密码",username: "admin",wantErr:  ErrMissingPassword,},{name:     "正常",username: "admin",password: "123456",wantData: &LoginRequest{username: "admin",password: "123456",},wantErr: nil,},}for _, item := range data {t.Run(item.name, func(t *testing.T) {got, err := NewLoginRequest(item.username, item.password)assert.Equal(t, item.wantErr, err)if item.wantErr == nil {assert.Equal(t, item.wantData.username, got.username)assert.Equal(t, item.wantData.password, got.password)}})}
}

登陆业务

登陆业务的单元测试比较复杂,因为需要一些外部的依赖,所以需要使用gomock,把依赖mock出来再自定义依赖的行为(gomock的东西不在本文的范围内)。按照登陆的业务流程,具体测试了用户不存在、密码错误和token生成失败的情况(篇幅限制)。代码如下:

func TestAccountUseCase_Login(t *testing.T) {controller := gomock.NewController(t)repo := NewMockUserRepo(controller)encryptService := NewMockEncryptService(controller)accountUseCase := NewAccountUseCase(log.DefaultLogger, &conf.Bootstrap{Auth: &conf.Auth{JwtSecret: "123",},}, repo, encryptService)data := []struct {name      stringmockFunc  func()wantErr   assert.ErrorAssertionFuncwantToken stringctx       context.Contextreq       *LoginRequest}{{name: "正常登陆",mockFunc: func() {encryptService.EXPECT().Encrypt(gomock.Any(), gomock.Any()).Return([]byte("123"), nil).Times(1)repo.EXPECT().FetchByUsername(gomock.Any(), gomock.Any()).Return(&User{Password: "123",}, nil).Times(1)encryptService.EXPECT().Token(gomock.Any(), gomock.Any()).Return("123", nil).Times(1)},wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {assert.NoError(t, err)return true},wantToken: "123",ctx:       context.Background(),req: &LoginRequest{username: "123",password: "123",},},{name: "用户不存在",mockFunc: func() {repo.EXPECT().FetchByUsername(gomock.Any(), gomock.Any()).Return(nil, ErrUserNotExist).Times(1)},wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {assert.ErrorAs(t, err, &ErrUserNotExist)return false},wantToken: "123",ctx:       context.Background(),req: &LoginRequest{username: "123",password: "123",},},{name: "密码校验不过",mockFunc: func() {encryptService.EXPECT().Encrypt(gomock.Any(), gomock.Any()).Return([]byte("1233"), nil).Times(1)repo.EXPECT().FetchByUsername(gomock.Any(), gomock.Any()).Return(&User{Password: "123",}, nil).Times(1)},wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {assert.ErrorAs(t, err, &ErrPasswordWrong)return false},wantToken: "123",ctx:       context.Background(),req: &LoginRequest{username: "123",password: "123",},},{name: "token生成失败",mockFunc: func() {encryptService.EXPECT().Encrypt(gomock.Any(), gomock.Any()).Return([]byte("123"), nil).Times(1)encryptService.EXPECT().Token(gomock.Any(), gomock.Any()).Return("", errors.New("123")).Times(1)repo.EXPECT().FetchByUsername(gomock.Any(), gomock.Any()).Return(&User{Password: "123",}, nil).Times(1)},wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {assert.ErrorAs(t, err, &ErrLoginFail)return false},wantToken: "123",ctx:       context.Background(),req: &LoginRequest{username: "123",password: "123",},},}for _, item := range data {t.Run(item.name, func(t *testing.T) {item.mockFunc()got, err := accountUseCase.Login(item.ctx, item.req)if !item.wantErr(t, err) {return}assert.Equal(t, item.wantToken, got)})}
}

总结

登陆的例子复杂度不够,不是很好例子。改成充血模型后,业务逻辑拆分更细,业务流程更加清晰。把不变的业务流程抽取出来,形成主干(校验凭证-发放令牌),把具体的业务策略封装到具体实现中(校验凭证<-检验密码实现,也可能是校验密码和验证码、邮箱之类的)。

这篇关于【kratos入门实战教程】番外篇之充血模型(1)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/168261

相关文章

Kali Linux安装实现教程(亲测有效)

《KaliLinux安装实现教程(亲测有效)》:本文主要介绍KaliLinux安装实现教程(亲测有效),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、下载二、安装总结一、下载1、点http://www.chinasem.cn击链接 Get Kali | Kal

springboot项目redis缓存异常实战案例详解(提供解决方案)

《springboot项目redis缓存异常实战案例详解(提供解决方案)》redis基本上是高并发场景上会用到的一个高性能的key-value数据库,属于nosql类型,一般用作于缓存,一般是结合数据... 目录缓存异常实践案例缓存穿透问题缓存击穿问题(其中也解决了穿透问题)完整代码缓存异常实践案例Red

Web技术与Nginx网站环境部署教程

《Web技术与Nginx网站环境部署教程》:本文主要介绍Web技术与Nginx网站环境部署教程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、Web基础1.域名系统DNS2.Hosts文件3.DNS4.域名注册二.网页与html1.网页概述2.HTML概述3.

spring security 超详细使用教程及如何接入springboot、前后端分离

《springsecurity超详细使用教程及如何接入springboot、前后端分离》SpringSecurity是一个强大且可扩展的框架,用于保护Java应用程序,尤其是基于Spring的应用... 目录1、准备工作1.1 引入依赖1.2 用户认证的配置1.3 基本的配置1.4 常用配置2、加密1. 密

WinForms中主要控件的详细使用教程

《WinForms中主要控件的详细使用教程》WinForms(WindowsForms)是Microsoft提供的用于构建Windows桌面应用程序的框架,它提供了丰富的控件集合,可以满足各种UI设计... 目录一、基础控件1. Button (按钮)2. Label (标签)3. TextBox (文本框

Spring Boot拦截器Interceptor与过滤器Filter深度解析(区别、实现与实战指南)

《SpringBoot拦截器Interceptor与过滤器Filter深度解析(区别、实现与实战指南)》:本文主要介绍SpringBoot拦截器Interceptor与过滤器Filter深度解析... 目录Spring Boot拦截器(Interceptor)与过滤器(Filter)深度解析:区别、实现与实

C#实现访问远程硬盘的图文教程

《C#实现访问远程硬盘的图文教程》在现实场景中,我们经常用到远程桌面功能,而在某些场景下,我们需要使用类似的远程硬盘功能,这样能非常方便地操作对方电脑磁盘的目录、以及传送文件,这次我们将给出一个完整的... 目录引言一. 远程硬盘功能展示二. 远程硬盘代码实现1. 底层业务通信实现2. UI 实现三. De

基于C#实现MQTT通信实战

《基于C#实现MQTT通信实战》MQTT消息队列遥测传输,在物联网领域应用的很广泛,它是基于Publish/Subscribe模式,具有简单易用,支持QoS,传输效率高的特点,下面我们就来看看C#实现... 目录1、连接主机2、订阅消息3、发布消息MQTT(Message Queueing Telemetr

Nginx使用Keepalived部署web集群(高可用高性能负载均衡)实战案例

《Nginx使用Keepalived部署web集群(高可用高性能负载均衡)实战案例》本文介绍Nginx+Keepalived实现Web集群高可用负载均衡的部署与测试,涵盖架构设计、环境配置、健康检查、... 目录前言一、架构设计二、环境准备三、案例部署配置 前端 Keepalived配置 前端 Nginx

Python日期和时间完全指南与实战

《Python日期和时间完全指南与实战》在软件开发领域,‌日期时间处理‌是贯穿系统设计全生命周期的重要基础能力,本文将深入解析Python日期时间的‌七大核心模块‌,通过‌企业级代码案例‌揭示最佳实践... 目录一、背景与核心价值二、核心模块详解与实战2.1 datetime模块四剑客2.2 时区处理黄金法