GO面向对象(做CRUD专家)八 :增加计算价格逻辑的复杂度(一)

71 阅读5分钟

前文曾经提过实际项目中计算价格的逻辑是一个复杂的过程,会根据用户的等级、活跃度、市场价的价格梯度等等参数进行联合计算,每一个用户有可能对应一个不同的数字,现在我们把用户等级参与价格计算中来;

业务需求:
用户等级分为普通、黄金、铂金、钻石等级
普通:价格不变
黄金:价格减1
铂金:价格减2
钻石:价格减3

package controller

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

   for _, item := range items {
      if user.IsLevelGold() {
         item.Price -= 1
      } else if user.IsLevelPlatinum() {
         item.Price -= 2
      } else if user.IsLevelDiamond() {
         item.Price -= 3
      }
   }

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

   c.JSON(http.StatusOK, resp)
}

// 获取商品详情
func (ctr *Item) Get(c *gin.Context) {

   user := ctr.repoUser.Get()
   item := ctr.repo.Get()

   if user.IsLevelGold() {
      item.Price -= 1
   } else if user.IsLevelPlatinum() {
      item.Price -= 2
   } else if user.IsLevelDiamond() {
      item.Price -= 3
   }

   var resp response.Item
   resp.Mapping(item)

   c.JSON(http.StatusOK, resp)
}

源码链接
代码分析:
根据前文中的案例发现,我们会发现这段代码存在的问题:
1、controller层存在业务逻辑
2、商品列表、商品详情接口业务逻辑重复

我们的目标是把根据重复的业务逻辑提取到一个对象封装起来,放在哪里好呢,放在Item对象里,Item对象要依赖User对象,Item对象通常只负责自己属性的相关逻辑计算,放在User对象里,同样道理也不合适;

这里我们新建一个商品价格计算器对象,把根据用户等级计算价格的逻辑封装到这个计算器中,然后放在service服务层,

package service

type ItemPrizeCalculator struct {
}

func NewItemPrizeCalculator() *ItemPrizeCalculator {
   return new(ItemPrizeCalculator)
}

func (srv *ItemPrizeCalculator) Handle(item *bo.Item, user *bo.User) {
   if user.IsLevelGold() {
      item.Price -= 1
   } else if user.IsLevelPlatinum() {
      item.Price -= 2
   } else if user.IsLevelDiamond() {
      item.Price -= 3
   }
}

---------------------------------------------------------------------------------------------
package controller

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

   for _, item := range items {
      ctr.prizeCalculator.Handle(item, user)
   }

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

   c.JSON(http.StatusOK, resp)
}

// 获取商品详情
func (ctr *Item) Get(c *gin.Context) {

   user := ctr.repoUser.Get()
   item := ctr.repo.Get()
   ctr.prizeCalculator.Handle(item, user)

   var resp response.Item
   resp.Mapping(item)

   c.JSON(http.StatusOK, resp)
}

源码链接
代码分析:
当某些业务行为需要多个对象参与协作,这个行为封装在其中的任一对象中都不合适的时候,就需要放在service服务层,例如上文中的商品价格需要商品对象和用户对象联合计算;但是我们必须把引入service服务层对象做为面向对象开发中承担业务逻辑的最后解决手段,千万不能滥用。

这里的service层相当于DDD架构的领域服务,对于DDD架构我的看法是面向对象的升级版,可以借鉴其中的思想,不能完全照搬,有兴趣的同学可以了解一下。

我们再看这段代码有没有优化空间:

   // 循环数组计算商品价格
   for _, item := range items {
      ctr.prizeCalculator.Handle(item, user)
   }

程序 = 算法 + 数据结构
这句话是大家见过听过最多一句名言,另外还有一句话不被大众所熟知:
算法 = 逻辑 + 控制
前者定义了程序的本质,层次和境界太高,后者跟日常编程更贴近一点,可以指导我们如何降低代码的复杂度。

业务逻辑与控制逻辑的耦合是导致大多数代码混乱的主要原因

程序存在两种代码,一种是业务逻辑代码,另一种代码是控制程序的代码,叫控制代码, 所谓控制,就是对程序流转与业务无关的代码控制,常见的是数据验证、数组循环、状态机场景;
不太好理解,举例说明:

func BubbleSort(nums []int) []int {
   length := len(nums)
   for i := 0; i < length; i++ {
      for j := i + 1; j < length; j++ {
         if nums[i] > nums[j] {
            temp := nums[i]
            nums[i] = nums[j]
            nums[j] = temp
         }
      }
   }
   return nums
}

什么是业务逻辑?
所谓业务逻辑就是对现实世界问题的定义
以排序问题来讲,有两个问题需要解决:
1、什么是有序?数字从小到大还是从大到小,代码用比较大小来解决;
2、如何有序?代码用数字交换来解决;

什么是控制逻辑?
控制就是如何合理地安排时间和空间资源去实现逻辑
以排序问题来讲,我们定义了两个问题并提出了解决方案,业务逻辑是不需要关心是无序到有序的过程中是用冒泡排序还是快速排序的方式来实现,控制是为业务逻辑服务的,是可以变化的。

所以我们把冒泡排序中的业务逻辑抽取出来,如下:


func Less(num1, num2 int) bool {
   if num1 > num2 {
      return true
   }
   return false
}

func Swap(nums *[]int, i, j int) {
   var temp int
   temp = (*nums)[i]
   (*nums)[i] = (*nums)[j]
   (*nums)[j] = temp
}

func BubbleSort(nums []int) []int {
   length := len(nums)
   for i := 0; i < length; i++ {
      for j := i + 1; j < length; j++ {
         if Less(nums[i], nums[j]) {
            Swap(&nums, i, j)
         }
      }
   }
   return nums
}

代码分析:
我们用Less和Swap方法封装了什么是有序和如何有序两个业务逻辑,当我们想提高排序效率,冒泡改成快排,Less和Swap方法就可以不用动,只需要改变控制逻辑就可以了。

熟练的区分业务逻辑和控制逻辑是成熟程序员的必备技能

我们再回到刚才的问题上,一共就三行代码,还会有什么问题?

   // 循环数组计算商品价格
   for _, item := range items {
      ctr.prizeCalculator.Handle(item, user)
   }

其实这段代码是控制逻辑的重复,for循环数组,然后对数组里的元素进行验证、过滤、业务运算在开发中最常见的操作,我们可以这段循环控制代码封装起来提高开发效率。

package slice

func Walker[T any](slice []T, f func(T)) {
   for _, e := range slice {
      f(e)
   }
}

-----------------------------------------------------------------------------------
package controller

func (ctr *Item) All(c *gin.Context) {
   ...
   slice.Walker(items, func(item *bo.Item) {
      ctr.prizeCalculator.Handle(item, user)
   })

   ...
}

源码链接
我们用Walker方法封装了循环数组的控制逻辑,当下次遇到相同的控制逻辑,可以直接拿来用了,这种方式在别的编程语言很普遍,比如java的forEach方法、php的array_walk方法,已经在语言层面上得到支持。

这里涉及到一个知识点:控制反转 (这里的控制跟上文的控制逻辑不是一个概率)

什么是控制反转:
A方法调用B方法,B方法里实现逻辑计算,这是B可以控制A的行为表现,这是我们常见的控制方式;
A方法调用B方法,A通过某种方式来控制B方法的逻辑计算,这就是控制反转;

Walker方法通过函数参数来实现逻辑的控制反转,和java的lambda表达式同出一辙,控制反转是实现上文中控制逻辑复用的重要手段,希望大家多多体会,熟练应用。