【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

相关文章

Java docx4j高效处理Word文档的实战指南

《Javadocx4j高效处理Word文档的实战指南》对于需要在Java应用程序中生成、修改或处理Word文档的开发者来说,docx4j是一个强大而专业的选择,下面我们就来看看docx4j的具体使用... 目录引言一、环境准备与基础配置1.1 Maven依赖配置1.2 初始化测试类二、增强版文档操作示例2.

Windows环境下解决Matplotlib中文字体显示问题的详细教程

《Windows环境下解决Matplotlib中文字体显示问题的详细教程》本文详细介绍了在Windows下解决Matplotlib中文显示问题的方法,包括安装字体、更新缓存、配置文件设置及编码調整,并... 目录引言问题分析解决方案详解1. 检查系统已安装字体2. 手动添加中文字体(以SimHei为例)步骤

Java JDK1.8 安装和环境配置教程详解

《JavaJDK1.8安装和环境配置教程详解》文章简要介绍了JDK1.8的安装流程,包括官网下载对应系统版本、安装时选择非系统盘路径、配置JAVA_HOME、CLASSPATH和Path环境变量,... 目录1.下载JDK2.安装JDK3.配置环境变量4.检验JDK官网下载地址:Java Downloads

MySQL 多列 IN 查询之语法、性能与实战技巧(最新整理)

《MySQL多列IN查询之语法、性能与实战技巧(最新整理)》本文详解MySQL多列IN查询,对比传统OR写法,强调其简洁高效,适合批量匹配复合键,通过联合索引、分批次优化提升性能,兼容多种数据库... 目录一、基础语法:多列 IN 的两种写法1. 直接值列表2. 子查询二、对比传统 OR 的写法三、性能分析

Python办公自动化实战之打造智能邮件发送工具

《Python办公自动化实战之打造智能邮件发送工具》在数字化办公场景中,邮件自动化是提升工作效率的关键技能,本文将演示如何使用Python的smtplib和email库构建一个支持图文混排,多附件,多... 目录前言一、基础配置:搭建邮件发送框架1.1 邮箱服务准备1.2 核心库导入1.3 基础发送函数二、

PowerShell中15个提升运维效率关键命令实战指南

《PowerShell中15个提升运维效率关键命令实战指南》作为网络安全专业人员的必备技能,PowerShell在系统管理、日志分析、威胁检测和自动化响应方面展现出强大能力,下面我们就来看看15个提升... 目录一、PowerShell在网络安全中的战略价值二、网络安全关键场景命令实战1. 系统安全基线核查

使用Docker构建Python Flask程序的详细教程

《使用Docker构建PythonFlask程序的详细教程》在当今的软件开发领域,容器化技术正变得越来越流行,而Docker无疑是其中的佼佼者,本文我们就来聊聊如何使用Docker构建一个简单的Py... 目录引言一、准备工作二、创建 Flask 应用程序三、创建 dockerfile四、构建 Docker

从入门到精通MySQL联合查询

《从入门到精通MySQL联合查询》:本文主要介绍从入门到精通MySQL联合查询,本文通过实例代码给大家介绍的非常详细,需要的朋友可以参考下... 目录摘要1. 多表联合查询时mysql内部原理2. 内连接3. 外连接4. 自连接5. 子查询6. 合并查询7. 插入查询结果摘要前面我们学习了数据库设计时要满

从原理到实战深入理解Java 断言assert

《从原理到实战深入理解Java断言assert》本文深入解析Java断言机制,涵盖语法、工作原理、启用方式及与异常的区别,推荐用于开发阶段的条件检查与状态验证,并强调生产环境应使用参数验证工具类替代... 目录深入理解 Java 断言(assert):从原理到实战引言:为什么需要断言?一、断言基础1.1 语

从入门到精通C++11 <chrono> 库特性

《从入门到精通C++11<chrono>库特性》chrono库是C++11中一个非常强大和实用的库,它为时间处理提供了丰富的功能和类型安全的接口,通过本文的介绍,我们了解了chrono库的基本概念... 目录一、引言1.1 为什么需要<chrono>库1.2<chrono>库的基本概念二、时间段(Durat