GO面向对象(做CRUD专家)五 :返利逻辑实现

340 阅读5分钟

返利金额:市场价的5%

面向过程代码实现:
Item类增加Rebate方法,计算返利金额

package bo

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

// 返利计算函数
func (bo *Item) RebateCalculate() {
   if bo.Category == 1 || bo.Category == 2 { // 折扣商品和试用商品返利金额为零
      bo.Rebate = 0
   } else if bo.Category == 3 { // 返利商品返利
      bo.Rebate = bo.PriceMarket * 5 / 100
   }
}

--------------------------------------------------------------------------
package entity

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

func (ent *Item) Mapping() *bo.Item {
   bo := new(bo.Item)
   bo.ID = ent.ID
   bo.Category = ent.Category
   bo.Title = ent.Title
   bo.Stock = ent.Stock
   bo.PriceMarket = ent.PriceMarket
   bo.PriceCalculate()  // 调用业务模型函数Price,计算商品价格
   bo.RebateCalculate() // 调用业务模型函数Rebate,计算返利金额
   return bo
}

源码链接

先优化一下代码中一直存在的一个坏味道

消除魔术值

一种代码的坏味道,又称隐形知识,知识都需要一定的学习成本和理解过程。


func (bo *Item) RebateCalculate() {
   if bo.Category == 1 || bo.Category == 2 { // 数字1、2就是魔术值
      bo.Rebate = 0
   } else if bo.Category == 3 {              // 数字3就是魔术值
      bo.Rebate = bo.PriceMarket * 5 / 100   // 为什么5和100不是魔术值?
   }
}

5100是数学计算中的5100,不代表其他的含义。

当阅读到这一段代码时,如果是别人或是写了很长时间的代码,第一时间的想法是数字2、3代表什么意思,经过查看文档、注释或者询问他人后确定数字2代表试用商品,数字3代表折扣商品;

魔术值会使代码可读性下降,提高代码的理解难度。在实际开发中,魔术值是非常非常常见的坏味道,希望大家重视。

消除魔术值:有意义的常量名称解释魔术值

const (
   ItemCategoryDiscount = iota + 1
   ItemCategoryTrial
   ItemCategoryRebate
)

func (bo *Item) RebateCalculate() {
   if bo.Category == ItemCategoryDiscount || bo.Category == ItemCategoryTrial { // 折扣商品和试用商品返利金额为零
      bo.Rebate = 0
   } else if bo.Category == ItemCategoryRebate { // 返利商品返利
      bo.Rebate = bo.PriceMarket * 5 / 100
   }
}

我们再接着说面向对象代码实现返利金额:

  1. 新增ItemRebateCalculator接口
  2. ItemRebate实现ItemRebateCalculator接口的Rebate函数
  3. entity层数据转换时通过断言接口计算商品返利
package bo

type ItemRebateCalculator interface {
   Rebate() *int
}

// 返利商品
type ItemRebate struct {
   *Item
}

// 返利计算函数
func (dom *ItemRebate) Rebate() int {
   return dom.PriceMarket * 5 / 100
}
---------------------------------------------------------------------------------
package entity

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

func (ent *Item) Mapping() *bo.Item {
   boItem := new(bo.Item)
   boItem.ID = ent.ID
   boItem.Category = ent.Category
   boItem.Title = ent.Title
   boItem.Stock = ent.Stock
   boItem.PriceMarket = ent.PriceMarket

   boItem.OfInstance()
   // 断言计算价格
   if priceCalculator, ok := boItem.Instance.(bo.ItemPriceCalculator); ok {
      boItem.Price = priceCalculator.Price()
   }

   // 断言计算返利
   if rebateCalculator, ok := boItem.Instance.(bo.ItemRebateCalculator); ok {
      boItem.Rebate = rebateCalculator.Rebate()
   }

   return boItem
}

源码链接

读者再次体会一下两种方式的不同点,加深理解。

我们分析一下前端如何对接服务器返回的Json数组

[
  {
    "id": 1,
    "category": 1,
    "title": "T shirt1",
    "stock": 1,
    "priceMarket": 100,
    "price": 50,
    "rebate": 0
  },
  {
    "id": 2,
    "category": 2,
    "title": "T shirt2",
    "stock": 2,
    "priceMarket": 10,
    "price": 0,
    "rebate": 0
  },
  {
    "id": 3,
    "category": 3,
    "title": "T shirt2",
    "stock": 3,
    "priceMarket": 100,
    "price": 100,
    "rebate": 5
  }
]

前端需求:

价格市场价返利
折扣商品显示显示不显示
试用商品显示显示不显示
返利商品显示不显示显示

返利商品价格和市场价相同,所以不显示

// 伪代码
var s = "价格" + item.price
if item.category != 3 {
    s += "市场价" + item.priceMarket
} 

if item.category == 2 {
    s += "返利" + item.rebate
}  
折扣商品:价格*** 市场价***
试用商品:价格*** 市场价***
返利商品:价格*** 返利***

前端也存在针对细节编程的问题。

我们根据前端的显示需求来分析一下这个Json数组:

1、返利金额是否显示
返利商品包含返利属性无异议,但是折扣、试用商品也包含返利属性就存在歧义(虽然数字是0),有人会想到前端通过判断数字等于0就不显示,这种方式就给数字0附上了另一种含义,试用商品类型,它的价格本身就是0,还必须要显示出来,前端显示逻辑就混乱了。

解决方案:
折扣商品Json不包含rebate属性,或者rebate属性等于null,前端判断rebate属性是否存在或者是否等于null来决定是否显示。

通常前端程序员特别喜欢整齐划一的Json数组,看到对象缺一个属性会担心undefined,请前后端沟通一致,个人倾向对象没有一个属性就不应该输出这个属性。

2、市场价是否显示问题
由于返利商品价格与市场价金额一致,不需要重复显示市场价,但是折扣商品返利商品和都包含市场价这个属性,不能通过对象是否包含市场价属性逻辑来定义显示逻辑,

解决方案:
由于App比较升级比较谨慎(市场审核、用户反感),部分显示逻辑会放在服务端控制,服务器增加显示逻辑控制字段priceMarketHidden,false显示,true隐藏。

最终Json数组结构如下:

[
  {
    "id": 1,
    "category": 1,
    "title": "T shirt1",
    "stock": 1,
    "priceMarket": 100,
    "price": 50,
    "priceMarketHidden": false // 控制市场价是否隐藏
    // 没有rebate属性
  },
  {
    "id": 2,
    "category": 2,
    "title": "T shirt2",
    "stock": 2,
    "priceMarket": 10,
    "price": 0,
    "priceMarketHidden": false // 控制市场价是否隐藏
    // 没有rebate属性
  },
  {
    "id": 3,
    "category": 3,
    "title": "T shirt3",
    "stock": 3,
    "priceMarket": 100,
    "price": 100,
    "rebate": 5,
    "priceMarketHidden": true // 控制市场价是否隐藏
  }
]

// 伪代码
var s = "价格" + item.price
if item.hasOwnProperty("rebate") {
    s += "返利" + item.rebate
} 

if !item.priceMarketHidden {
    s += "市场价" + item.priceMarket
}

服务器实现过程:
1、增加市场价是否隐藏接口
2、response层数据转换时通过断言接口计算市场价是否隐藏

package bo
type ItemRebate struct {
   *Item
}

func (bo *ItemRebate) PriceMarketHidden() bool {
   return true
}
---------------------------------------------------------------------------------
package response

type Item struct {
   ID                int    `json:"id"`
   Category          int    `json:"category"`
   Title             string `json:"title"`
   Stock             int    `json:"stock"`
   PriceMarket       int    `json:"priceMarket"`
   Price             int    `json:"price"`
   Rebate            int    `json:"rebate,omitempty"`   // omitempty 零值返利属性不输出
   PriceMarketHidden bool   `json:"priceMarketHidden"`
}

func (resp *Item) Mapping(boItem *bo.Item) {
   if boItem == nil {
      return
   }

   resp.ID = boItem.ID
   resp.Category = boItem.Category
   resp.Title = boItem.Title
   resp.Stock = boItem.Stock
   resp.PriceMarket = boItem.PriceMarket
   resp.Price = boItem.Price
   resp.Rebate = boItem.Rebate

   // 断言市场价是否显示
   if rebateCalculator, ok := boItem.Instance.(bo.ItemPriceMarketHidden); ok {
      resp.PriceMarketHidden = rebateCalculator.PriceMarketHidden()
   }
}

断言市场价页面是否显示代码我们放在response层,因为属性是否显示是前端逻辑,不包含具体业务逻辑,放在response层比较合适。

至此我们解决了前端针对细节编程的问题。源码链接