GO面向对象(做CRUD专家)七 :VIP价格实现

265 阅读4分钟

业务需求:
为了讲解清晰,我们假设目前只有折扣商品和返利商品两种类型,需要实现VIP价格功能;

代码实现:

package bo

// 价格、VIP价格计算器接口
type ItemPriceCalculator interface {
   Price() int
   PriceVIP() int
}

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


func (ent *Item) Mapping() *bo.Item {
   ...
   
   // 断言计算价格
   if priceCalculator, ok := boItem.Instance.(bo.ItemPriceCalculator); ok {
      boItem.Price = priceCalculator.Price()
      boItem.PriceVIP = priceCalculator.PriceVIP()
   }

   ...    
}

新增需求:
增加试用商品,试用商品没有VIP价格,前端不需要显示

需求分析:
开发者最头疼的就是特例,本来折扣、返利商品同时有价格和VIP价格逻辑,突然蹦出个试用商品类型没有VIP价格,试用商品实现ItemPriceCalculator接口必须实现PriceVIP函数,特别别扭。

// 试用商品
type ItemTrial struct {
   *Item
}

// 实现价格计算器接口
func (dom *ItemTrial) Price() int {
   return 0
}

// 实现VIP价格计算器接口(心不甘情不愿的实现)
func (dom *ItemTrial) PriceVIP() int {
   return 0
}

源码链接 这也是在项目开发中很常见的一种解决方式,这种方式会有什么问题?
1、ItemTrial类实现了本来不需要的PriceVIP方法;
2、前端如果通过商品类型判断PriceVIP价格是否显示就会出现细节编程的问题;
3、试用商品的的PriceVIP为0,前端如果想通过PriceVIP大于0就显示,等于0就隐藏,这就赋予数字0另外一层含义;假设为了提高VIP用户转化率,某些折扣类型商品的VIP价格也要为0,前端还必须要显示出来就会有问题;
4、如果前端想通过是否包含PriceVIP属性解决显示问题,服务器需要如下编程:

package response

func (resp *Item) Mapping(boItem *bo.Item) {
   ...

   if priceCalculator, ok := boItem.Instance.(bo.ItemPriceCalculator); ok {
      resp.Price = priceCalculator.Price()

      // 出现面向实现编程的问题
      if _, ok := boItem.Instance.(bo.ItemTrial); !ok {
         priceVIP := priceCalculator.PriceVIP()
         resp.PriceVIP = &priceVIP
      }
   }
   
   ...
}
   

接口分离原则

用于处理胖接口(fat interface)所带来的问题。如果类的接口定义暴露了过多的行为,则说明这个类的接口定义内聚程度不够好。 定义:
1、客户端不应该被强迫依赖它不需要的接口
2、类间的依赖关系应该建立在最小的接口上

客户端不应该依赖它不需要实现的接口

客户端可以理解为接口的调用者;

接口是开发中比较令人困惑的术语之一,对外提供的api可以称之为接口,类的公共属性、公共方法也可以称之为接口,接口是是调用方和被调用方之间约定的抽象,这里的接口可以理解为OOP概念的的接口、类的公共属性和公共方法;

func (ent *Item) Mapping() *bo.Item {
   ...
   
   // 断言计算价格
   if priceCalculator, ok := boItem.Instance.(bo.ItemPriceCalculator); ok {
      boItem.Price = priceCalculator.Price()
      boItem.PriceVIP = priceCalculator.PriceVIP()
   }

   ...    
}

当商品为试用类型时,调用方就依赖了它不需要的接口:PriceVIP方法

如何解决这个问题,就需要遵循第2点:

类间的依赖关系应该建立在最小的接口上

package domain

// 价格计算器接口
type ItemPriceCalculator interface {
   Price() int
}

// VIP价格计算器接口
type ItemPriceVIPCalculator interface {
   PriceVIP() int
}

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

func (ent *Item) Mapping() *bo.Item {
   ...
   // 断言计算价格
   if priceCalculator, ok := boItem.Instance.(bo.ItemPriceCalculator); ok {
      boItem.Price = priceCalculator.Price()
   }
   // 断言计算VIP价格
   if priceVIPCalculator, ok := boItem.Instance.(bo.ItemPriceVIPCalculator); ok {
      boItem.PriceVIP = priceVIPCalculator.PriceVIP()
   }
   ...

   return boItem
}
-----------------------------------------------------------------------
package response

func (resp *Item) Mapping(boItem *bo.Item) {
    ...
    if priceVIPCalculator, ok := boItem.Instance.(bo.ItemPriceVIPCalculator); ok {
       priceVIP := priceVIPCalculator.PriceVIP()
       resp.PriceVIP = &priceVIP
    }
   ...
}

代码分析:
1、ItemPriceCalculator接口拆分出ItemPriceVIPCalculator接口
2、entity层数据转换时,通过断言获取VIP价格

源码链接

何为最小的接口?

把所有的接口都设计成只包含一个函数不就是最小了吗?当然可以,接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性,但是如果过小,则会造成接口数量过多,使程序复杂化,所以一定要适度,如何把握这个度是难点。

代码要去适应需求,随着需求的改变而改变,当只有折扣商品和返利商品两种类型时,ItemPriceCalculator的设计可以称之为最小,当试用商品出现的时候ItemPriceCalculator的设计属于臃肿、不灵活,需要拆分,随着需求的变化而演化为刚刚好的代码是最合适的代码。

总结:涉及到单一、最小、最少等模棱两可的概念时,通常这部分知识点是实际开发中最难理解、最难把握,大家好好体会。