【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

相关文章

SpringBoot 多环境开发实战(从配置、管理与控制)

《SpringBoot多环境开发实战(从配置、管理与控制)》本文详解SpringBoot多环境配置,涵盖单文件YAML、多文件模式、MavenProfile分组及激活策略,通过优先级控制灵活切换环境... 目录一、多环境开发基础(单文件 YAML 版)(一)配置原理与优势(二)实操示例二、多环境开发多文件版

Three.js构建一个 3D 商品展示空间完整实战项目

《Three.js构建一个3D商品展示空间完整实战项目》Three.js是一个强大的JavaScript库,专用于在Web浏览器中创建3D图形,:本文主要介绍Three.js构建一个3D商品展... 目录引言项目核心技术1. 项目架构与资源组织2. 多模型切换、交互热点绑定3. 移动端适配与帧率优化4. 可

从入门到精通详解Python虚拟环境完全指南

《从入门到精通详解Python虚拟环境完全指南》Python虚拟环境是一个独立的Python运行环境,它允许你为不同的项目创建隔离的Python环境,下面小编就来和大家详细介绍一下吧... 目录什么是python虚拟环境一、使用venv创建和管理虚拟环境1.1 创建虚拟环境1.2 激活虚拟环境1.3 验证虚

从原理到实战解析Java Stream 的并行流性能优化

《从原理到实战解析JavaStream的并行流性能优化》本文给大家介绍JavaStream的并行流性能优化:从原理到实战的全攻略,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的... 目录一、并行流的核心原理与适用场景二、性能优化的核心策略1. 合理设置并行度:打破默认阈值2. 避免装箱

基于C#实现PDF转图片的详细教程

《基于C#实现PDF转图片的详细教程》在数字化办公场景中,PDF文件的可视化处理需求日益增长,本文将围绕Spire.PDFfor.NET这一工具,详解如何通过C#将PDF转换为JPG、PNG等主流图片... 目录引言一、组件部署二、快速入门:PDF 转图片的核心 C# 代码三、分辨率设置 - 清晰度的决定因

Maven中生命周期深度解析与实战指南

《Maven中生命周期深度解析与实战指南》这篇文章主要为大家详细介绍了Maven生命周期实战指南,包含核心概念、阶段详解、SpringBoot特化场景及企业级实践建议,希望对大家有一定的帮助... 目录一、Maven 生命周期哲学二、default生命周期核心阶段详解(高频使用)三、clean生命周期核心阶

Python实战之SEO优化自动化工具开发指南

《Python实战之SEO优化自动化工具开发指南》在数字化营销时代,搜索引擎优化(SEO)已成为网站获取流量的重要手段,本文将带您使用Python开发一套完整的SEO自动化工具,需要的可以了解下... 目录前言项目概述技术栈选择核心模块实现1. 关键词研究模块2. 网站技术seo检测模块3. 内容优化分析模

Java 正则表达式的使用实战案例

《Java正则表达式的使用实战案例》本文详细介绍了Java正则表达式的使用方法,涵盖语法细节、核心类方法、高级特性及实战案例,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要... 目录一、正则表达式语法详解1. 基础字符匹配2. 字符类([]定义)3. 量词(控制匹配次数)4. 边

Java Scanner类解析与实战教程

《JavaScanner类解析与实战教程》JavaScanner类(java.util包)是文本输入解析工具,支持基本类型和字符串读取,基于Readable接口与正则分隔符实现,适用于控制台、文件输... 目录一、核心设计与工作原理1.底层依赖2.解析机制A.核心逻辑基于分隔符(delimiter)和模式匹

Python内存优化的实战技巧分享

《Python内存优化的实战技巧分享》Python作为一门解释型语言,虽然在开发效率上有着显著优势,但在执行效率方面往往被诟病,然而,通过合理的内存优化策略,我们可以让Python程序的运行速度提升3... 目录前言python内存管理机制引用计数机制垃圾回收机制内存泄漏的常见原因1. 循环引用2. 全局变