返利金额:市场价的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不是魔术值?
}
}
5和100是数学计算中的5和100,不代表其他的含义。
当阅读到这一段代码时,如果是别人或是写了很长时间的代码,第一时间的想法是数字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
}
}
我们再接着说面向对象代码实现返利金额:
- 新增ItemRebateCalculator接口
- ItemRebate实现ItemRebateCalculator接口的Rebate函数
- 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层比较合适。
至此我们解决了前端针对细节编程的问题。源码链接