GO面向对象(做CRUD专家)一 :案例实现及分析

285 阅读4分钟

为了证明案例是实际开发项目中的真实案例,不是凭空想象,放一张项目的UI设计图;

1657695370296.jpg

本系列描述的是一个项目业务从简单到复杂的过程,后文中会增加一步步增加商品类型,在这个过程中,大家可以清晰的对比和体会面向过程和面向对象各自如何应对需求新增和变更的不同点,请大家保持耐心。

项目介绍

一个做特卖的电商App,类似淘宝客,业务模式为用户购买商品可以获得一定的折扣。

实现UI图中的商品列表接口(赚、返字段先忽略)
折扣商品:
价格:市场价的50%(50%的比例需要随时调整,所以不能直接算好放在数据库里,要实时计算)
前端显示:价格50元 市场价100

读者可以停下来自己动手实现一下商品列表,然后对比文章中的实现方式,印象会更加深刻一点。

我们采用比较流行的web框架gin,用经典的MVC模式来代码实现:

业务模型:
package domain

type Items []*Item

type Item struct {
   ID          int
   Title       string
   Stock       int
   PriceMarket int
   Price       int
}
--------------------------------------------------------------------------------
controller层:
package controller

func NewItem() *Item {
   ctr := new(Item)
   ctr.repo = repository.NewItem()
   return ctr
}

// 获取商品列表
func (ctr *Item) All(c *gin.Context) {
   items := ctr.repo.All()

   for _, item := range items {
      item.Price = item.PriceMarket / 2 // 折扣计算逻辑
   }

   resp := response.Items{}
   resp.Mapping(items)

   c.JSON(http.StatusOK, resp)
}

// 获取商品详情
func (ctr *Item) Get(c *gin.Context) {
   item := ctr.repo.Get()
   item.Price = item.PriceMarket / 2 // 折扣计算逻辑

   var resp response.Item
   resp.Mapping(item)

   c.JSON(http.StatusOK, resp)
}

源码链接

思考:以上写法会有什么问题?

注:实际项目中计算价格的逻辑是一个复杂的过程,会根据用户的等级、活跃度、市场价的价格梯度等等参数进行联合计算,每一个用户有可能对应一个不同的数字,这里逻辑简单化。

代码分析:
1、计算折扣价格逻辑重复
商品列表和商品详情中的折扣计算逻辑重复,如果想改折扣率为1/4,需要找到两个地方把2改为4,有读者会有疑问,定义一个折扣全局变量,不就解决了吗,其实这里不仅仅是折扣变量2重复,是整个折扣计算逻辑重复,如果需要根据用户的等级进行不同的折扣计算,仅仅定义一个折扣变量还是解决不了问题。

重复代码是软件质量下降的重大因素

2、controller层责任巨大
controller层的代码一般只负责接收和响应请求,不负责具体的逻辑业务逻辑实现;

第一步:从仓储层获取商品数组;
第二步:遍历商品数组计算商品折扣价格;
第三步:业务模型转化为数据传输模型;
第四步:对外输出数据;

把业务逻辑放在controller层实现,是很多初学者常见的问题,经常在实际开发中会遇到controller层的一个方法有几百行代码,包含各种数据读取、转换、过滤、提取逻辑,造成代码的可读性很差,让人望而生畏;实际上第二步计算商品折扣逻辑不是controller层的责任。

3、单元测试难以编写
现在的代码只能针对Item.All方法进行单元测试,Item.All方法依赖gin.Context、repository.Item、response.Items,导致单元测试很难编写,如果想单独针对折扣计算逻辑进行测试,不好实现。

解决思路: 我们先以最朴素的想法把折扣计算逻辑抽取到一个公共的函数,然后提供给商品列表和商品详情调用,那么这个公共的函数放在哪里合适呢?放在bo.Item模型中,让Item这个对象自己拥有可以计算价格的方法;

package bo

type Item struct {
   ID          int
   Title       string
   Stock       int
   PriceMarket int
   Price       int
}

func (bo *Item) PriceCalculate() {
   bo.Price = bo.PriceMarket / 2 // 折扣计算逻辑
}

// 仓储层转换模型调用PriceCalculate函数
package entity

type Item struct {
   ID    int
   Title string
   Stock int
   PriceMarket int
}

func (ent *Item) Mapping() *bo.Item {
   bo := new(bo.Item)
   bo.ID = ent.ID
   bo.Title = ent.Title
   bo.Stock = ent.Stock
   bo.PriceMarket = ent.PriceMarket
   bo.PriceCalculate() // 这里
   return bo
}

源码链接

代码分析:
1、解决了重复代码的问题,只需要找到Item.PriceCalculate函数,就可以同时修改商品列表和商品详情的折扣逻辑;
2、controller层不包含业务逻辑代码,清爽了很多;
3、针对Item.PriceCalculate函数写单元测试是件很轻松的事;

总结:让bo.Item模型拥有PriceCalculate函数,使其具有可以计算自身折扣价格的能力,这就是面向过程迈向面向对象的第一步,也不是很难是吧,大家一起努力。