业务需求:
为了讲解清晰,我们假设目前只有折扣商品和返利商品两种类型,需要实现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的设计属于臃肿、不灵活,需要拆分,随着需求的变化而演化为刚刚好的代码是最合适的代码。
总结:涉及到单一、最小、最少等模棱两可的概念时,通常这部分知识点是实际开发中最难理解、最难把握,大家好好体会。