BookShop demo 案例详解——从上手电商到上手CloudWeGo

原创
2023/04/10 11:43
阅读数 256
AI总结

缘起

我是2021年底开始关注 CloudWeGo 社区的,当时我作为校招生刚刚入职字节跳动,之前主要是关注 Java 方向的技术栈,对go的生态、微服务组件都不是很了解,所以当时在做需求、上线代码、定位线上问题时,都有一些似懂非懂朦朦胧胧的不踏实感,恰好当时 CloudWeGo 开始开源,于是便萌生了一个念头:从阅读框架源码的角度来了解、学习公司的技术栈,从而更好的保证自己的代码质量,提高自己定位线上问题的能力。

CloudWeGo 是我真正意义上接触的第一个开源社区,和很多人一样,一开始也不知道这么大的一个项目该从哪里开始学习好,在很长一段时间里面,也仅仅停留在没有聚焦和总结的通读代码层面上,在这段时间里面,提交了一些意义不是很大的typo fix,就这样,时间来到了2022年年初。

2022年3月份,当时Kitex仓库发布了一个 good first issue,主要是提高代码单测覆盖率,正愁找不到方向的我决定从这里入手,认领了一个 package 的单测任务,也是从那时候开始,参与 CloudWeGo 的社区开发者周会等活动

这个 issue 上手门槛不高,很快便完成了,在代码开发过程中,还发现了一处代码 bug 并提了 bugfix,让我作为一个新手对社区更有参与感了。完成之后,我注意到还有一个good first issue:

这个 issue 是提交一份 Kitex 和 Hertz 的业务工程实践,考虑到我是抖音电商业务的研发,在平时工作中对电商系统也是在持续的学习,做这个项目不光可以用自己的所学来回馈社区,而且通过做这个项目,也可以不断拓宽我的能力边界,不断促使我去发现不足然后弥补,于是当时我认领了电商demo系统开发的任务。

在做这个任务的过程中,有时候也会发现社区缺少的能力,然后我便会去做能力补齐,例如:在开发该项目的用户鉴权模块时,Hertz 还没有开源版的 JWT 中间件,在代码调试时,发现 Hertz 还缺少一款 Swagger 中间件,于是我当时去做了对应的代码贡献:

电商业务很复杂,单从商品的生命周期来说,从最底层的供应商,到分销商到商家,再到最上层的消费者,链路很长,在做这个demo的过程中我一边在学习相关知识,一边也在不断的推翻、删减,我将这次分享的主题定为“从上手电商到上手CloudWeGo”,其实从另一种意义上来说,它也是我这段时间的成长历程,从一年前到现在,我也是跌跌撞撞,尝试上手电商,上手 CloudWeGo。

下面给大家概述一下电商系统的组成

电商系统概述

不管是线下交易还是线上交易,一次交易流程中最重要的实体就是商品,电商——一个将商品交易从线下转为线上的系统,会出现很多原先我们在线下交易时根本不关注、或者压根没有的名词,这些名词的发明、演进的过程也正是人们在电商这个领域一点点积累、发展的过程,因此首先对这些名词做一个解释。这里敲个重点,下面的名词会在今天的分享中频繁提到。

  • 类目和属性

暂时无法在飞书文档外展示此内容

在最早期的电商系统中,其实根本没有类目和属性的概念,试想一下,假如一个购物网站,里面只有两三家店铺,几百个商品,那么不需要对商品分类或者只需做一个很简单的分类就能让系统运转起来,比如:服装、电器、日用品等等,但随着商品的丰富(男装女装、衬衫裙子)和商品数量级的增大(亿、十亿、百亿级别),简单的一层分类已经无法满足需要,因此便衍生出了类目乃至类目树(多级类目)的概念。商品继续丰富,有日韩风欧美风,阿迪耐克,如果都用类目承接那么类目就会变得异常深、异常复杂,不光不利于维护,而且也会出现商家类目错放、消费者流式的问题,于是又有了一个概念:属性,日韩风欧美风、阿迪耐克都是这个商品的属性

随着业务的演进,类目又有前台类目后台类目,属性又有销售属性(决定库存的属性)、类目属性(类目下商品应具有的特征)等等区分

  • SPU 和 SKU

SPU(Standard Product Unit) 中文为标准产品单元,通俗的理解为市面上的一款产品,如 iPhone14,雅诗兰黛特润修护肌活精华露(小棕瓶精华)

SKU(Stock Keeping Unit) 中文为库存量单位,库存最终是落在 SKU 维度,例如A商家售卖的一款iPhone14-紫色-512GB,库存还剩100件,这里的iPhone14就是A商家的商品,iPhone14-紫色-512GB就是这个商品下的一个 SKU

SKU、SPU、商品的关系图:

从电商系统全链路的角度来看,系统内主要有以下四种角色:

角色 场景
商家 商品管理,素材管理,库存管理,评价管理,履约发货、报名活动等
消费者 搜索商品、浏览商列商详、订单创建、退单、收货、评价等
运营 系统元数据配置(如类目维护、销售属性维护、SPU维护、资质维护)、营销活动、商家判罚等
审核员 商品审核、召回、解封,资质鉴真等

服务划分和职责:

电商系统从领域上大致可以划分成如下几个方向:商品、营销、交易、售后、供应链、安全、店铺、账号等等,每个方向根据职责的不同、重要性的不同又可以划分为多个微服务

暂时无法在飞书文档外展示此内容

系统架构&&代码分层设计

系统架构

由上文可知,电商系统链路长,内容多,从 demo 演示的角度考虑,进行了如下裁剪,希望可以以最低的门槛帮助大家了解电商、了解 CloudWeGo:

商品实体精简:精简掉了类目、类目属性、销售属性等概念,不强调 SKU 的概念,商品发布退化为图书标品(SPU)的发布,库存落在 SPU 上而不是 SKU 上(这也是 BookShop 的由来),库存只保留简单的现货库存,舍弃阶梯库存、区域库存、渠道库存等概念

struct BookProperty {
    1: string isbn // ISBN
    2: string spu_name // 书名
    3: i64 spu_price // 定价
}

struct Product {
    1: i64 product_id
    2: string name // 商品名
    3: string pic // 主图
    4: string description // 详情
    5: BookProperty property // 属性
    6: i64 price // 价格
    7: i64 stock // 库存
    8: Status status // 商品状态
}

链路精简:只保留了商品(负责管理商品、管理库存、搜索商品)、订单(负责订单创建、取消、列表、查询)、账号(负责创建、登录、鉴权)三个服务,串联商家发布商品->消费者购买这一条链路

架构图

暂时无法在飞书文档外展示此内容

组件选取

  • Hertz:用于编写Face的服务,统一对外http层

    • jwt 中间件:提供商家、消费者账号鉴权能力
    • swagger 中间件:自动生成文档、接口测试
    • gzip 中间件:压缩传输数据,减少带宽
    • pprof 中间件:服务性能分析工具
  • Kitex:用于编写Item、User、Order服务,系统RPC层

  • MySQL:数据库存储

  • Redis:缓存,用于存储用户账号信息

  • ETCD:分布式存储系统,用于服务注册发现

  • ES:搜索引擎,用于商品C端搜索

  • Kibana:用于ES数据可视化操作

代码分层架构

为什么要分层

分层的本质是管理软件的复杂度,将不同复杂度、不同变更频率的模块区分开

暂时无法在飞书文档外展示此内容

通过代码分层我们可以做到

  • 高内聚:分层的设计可以隔离变化速度不同的内容,简化系统,让不同层专注于做不同的事情
  • 可扩展:分层之后可以对某一层更容易做扩展(比如典型的换 repository)
  • 可复用:分层之后同一层可以有多种用途,同时因为复用,我们可以降低代码维护成本和改动风险
  • ...

如何分层

三层架构

经典的三层架构如图所示

暂时无法在飞书文档外展示此内容

DDD四层架构

《领域驱动设计:软件核心复杂性应对之道》中推荐的分层架构

暂时无法在飞书文档外展示此内容

  • 用户接口层:负责向用户显示信息或者解释命令。
  • 应用层:定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。应用层要尽量简单,不包含业务规则或者业务知识,只为下一层中的领域对象协调任务、分配工作,使它们相互协作。
  • 领域层:负责表达业务概念,业务状态信息以及业务规则。尽管保存业务状态的技术细节是由基础设施层实现的,但是反应业务情况的状态是由本层控制并且使用的。领域层是业务软件的核心,领域模型就位于这一层。
  • 基础设施层:为其他层提供通用的技术能力,包含三层架构中的数据访问层,也包含从三层架构的业务逻辑剥离出来的技术框架、中间件系统、其他系统调用的相关代码。

优点:

  • 第一次把代码的分层和 DDD 的领域建模对应起来了,匹配了业务逻辑和业务实体的建模结果
  • 对三层架构做了进一步细分,每层的职责更加明确

缺点:

  • 没有解决业务逻辑(应用层+领域层)层依赖基础设施层的问题。即没有消除因为基础设施层里具体技术实现的变化可能导致的应用服务层和领域层的变化。
改进的DDD四层架构

为了解决四层架构在基础设施层的依赖问题,在《实现领域驱动设计》一书中提出了依赖倒置的改进方案,如下所示:

暂时无法在飞书文档外展示此内容

所谓依赖倒置:高层模块不直接依赖于低层模块,两者都依赖于抽象,抽象不依赖细节,细节应该依赖抽象。

Repository和DI

“ DDD设计的目标是关注领域模型而并非技术来创建更好的软件,假设开发人员构建了一个SQL,并将它传递给基础设施层中的某个查询服务然后根据表数据的结构集取出所需信息,最后将这些信息提供给构造函数或者Factory,开发人员在做这一切的时候早已不把模型看做重点了,这个整个过程就变成了数据处理的风格 ”

——摘 Eric Evans《领域驱动设计》

在改进的DDD四层架构里,我们提到了repository(仓储)DI(依赖注入)的概念,Repository是领域层定义的一个接口,它抽象了业务逻辑对实体的访问(包括读取和存储)的技术细节,它的作用就是通过隔离具体的存储层技术实现来保证业务逻辑的稳定性。在DDD改造后的四层架构中,仓储接口的定义在领域层,实现在Infrastructure层。

暂时无法在飞书文档外展示此内容

在图左中,必然很容易让领域模型对数据库、内存等这里基础设施的代码产生依赖,从而让基础设施的概念入侵到领域模型变得容易。我们习惯于面向数据和过程的开发,当这类代码和领域模型的代码界限变得没那么明显的时候,聚焦于模型也容易被破坏,倒置依赖整洁架构分层给了我们解决这个问题很好的实践。我们可以把仓储的行为抽象为基本的接口,然后利用控制反转,把实现该节点的仓储注入领域模型的运行态中,如图右。

结合本电商项目中的代码举例:

在item服务的领域层,提供了创建商品(AddProduct)的能力,它不直接依赖基础设施层,而是依赖 Repository的一个接口:

// domain/service/product_update_service.go
func (s *ProductUpdateService) AddProduct(ctx context.Context, entity *entity.ProductEntity) error {
        err := repository.GetRegistry().GetProductRepository().AddProduct(ctx, entity)
        if err != nil {
                return err
        }
        return nil
}

// domain/repository/product_repository.go 
type ProductRepository interface {
        AddProduct(ctx context.Context, product *entity.ProductEntity) error

        UpdateProduct(ctx context.Context, origin, target *entity.ProductEntity) error

        GetProductById(ctx context.Context, productId int64) (*entity.ProductEntity, error)

        ListProducts(ctx context.Context, filterParam map[string]interface{}) ([]*entity.ProductEntity, error)
}

infrastructure层实现这个接口

// infras/repository/product_repo_impl.go
func (i ProductRepositoryImpl) AddProduct(ctx context.Context, product *entity.ProductEntity) error {
        if product == nil {
                return errors.New("插入数据不可为空")
        }
        po, err := converter.ProductDO2POConverter.Convert2po(ctx, product)
        if err != nil {
                return err
        }
        return DB.WithContext(ctx).Create(po).Error
}

使用前,进行依赖注入

repository.GetRegistry().SetProductRepository(productRepository)

具体来说,这样做可以带来什么好处?

假如有一天,随着商品的量级逐渐变大,即使是B端商家检索商品也不再适合直接使用DB,而是使用ES,那么我们可以直接新增一个repository接口的ES实现,将新实现依赖注入,这样完全不需要变更上层任何代码,层与层之间的耦合更小更轻,更利于项目的维护

项目代码讲解

  1. 详见回放视频(“27:18”处开始):https://meetings.feishu.cn/s/1is2wi7ytn8jl?src_type=2
  2. GitHub :https://github.com/cloudwego/biz-demo/blob/main/book-shop/README.md

总结

以上是我本次分享的全部内容,最后是本项目和一些相关项目的链接,前文说到,电商是个很庞大的系统,我只不过是实现了其中非常小的一部分,欢迎感兴趣的同学来参与贡献,一起让这个demo变得更完善🎉


项目地址

活动链接:https://github.com/cloudwego/community/issues/58

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
0 评论
0 收藏
0
分享
AI总结
返回顶部
顶部