GO面向对象(做CRUD专家)三 :业务升级

224 阅读5分钟

一、新增试用商品类型

业务模式为用户免费领取商品试用

试用商品:
价格:0

数据库增加类型字段

items表新增category字段,1为折扣商品 2为试用商品
id           自增
category     类型 
title        标题
stock        库存
price_market 市场价

面向过程代码实现

PriceCalculate函数增加if类型逻辑判断代码块,计算试用商品价格

func (bo *Item) PriceCalculate() {
   if bo.Category == 2 {
      bo.Price = 0
   }
   
   bo.Price = bo.PriceMarket / 2
}

源码链接
读者可以回顾一下自己的编码经历,原有的业务逻辑上新增需求时,在老代码上增加if else分支的方式是不是经常发生。

面向对象代码实现

  1. 新增ItemPriceCalculator接口
  2. 新增ItemDiscount折扣商品实现类,继承Item基类
  3. 新增ItemTrial试用商品实现类,继承Item基类
  4. ItemDiscount和ItemTrial实现ItemPriceCalculator接口的Price函数
  5. 数据转换时通过断言接口计算商品价格
package domain
// 折扣商品
type ItemDiscount struct {
   *Item
}

// 实现价格计算器接口
func (bo *ItemDiscount) Price() int {
   return bo.PriceMarket / 2
}

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

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

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
}

源码链接

思考:面向过程用了if代码块三行代码解决的问题,面向对象用了1、2、3、4、5五个步骤,划算吗?

再接着看需求变动两种方式的应对方式;

二、新增返利商品类型

业务模式为用户成功购买商品返利一定比例的金额。

返利商品:
价格:市场价原价
返利金额:市场价的5% 

数据库增加类型字段

items表category字段,1为折扣商品 2为试用商品 3返利商品

面向过程代码实现

Price函数再增加if else分支代码块,计算返利商品价格

func (bo *Item) PriceCalculate() {
   if bo.Category == 1 {               // 折扣商品价格
      bo.Price = bo.PriceMarket / 2
   } else if bo.Category == 2 {        // 试用商品价格
      bo.Price = 0
   } else if bo.Category == 3 {        // 返利商品价格
      bo.Price = bo.PriceMarket
   }
}

面向对象代码实现

  1. 新增ItemRebate返利商品实现类,继承Item基类
  2. ItemRebate实现ItemPriceCalculator接口的Price函数
// 试用商品
type ItemRebate struct {
   *Item
}

// 实现价格计算器接口
func (bo *ItemRebate) Price() int {
   return bo.PriceMarket
}

现在我们体会到面向过程和面向对象如何应对需求新增:

1、面向过程增加if else分支
2、面向对象增加类层级

大家自行体会五分钟

编程书籍和教程经常提到两句话:

1、要针对接口编程,不要面向细节(或实现)编程
2、要隔离变化

什么是实现?后文解释

什么是细节?面向细节编程会有什么问题?

func (bo *Item) PriceCalculate() {
   if bo.Category == 1 {               // 针对类型的判断就是细节
      bo.Price = bo.PriceMarket / 2
   } else if bo.Category == 2 {        // 针对类型的判断就是细节
      bo.Price = 0
   } else if bo.Category == 3 {        // 针对类型的判断就是细节
      bo.Price = bo.PriceMarket
   }
}

面向细节编程可以理解为代码针对对象的某一个特征进行逻辑分类编程

什么是变化?不隔离变化会有什么问题?

func (bo *Item) PriceCalculate() {
   if bo.Category == 1 {              
      bo.Price = bo.PriceMarket / 2    // 计算价格的的逻辑就是变化
   } else if bo.Category == 2 {       
      bo.Price = 0                     // 计算价格的的逻辑就是变化
   } else if bo.Category == 3 {        
      bo.Price = bo.PriceMarket         // 计算价格的的逻辑就是变化
   }
}

针对每个商品类型有不同的计算逻辑,这就是变化

显而易见面向过程是用if else代码块来分割变化,还不能称为隔离,隔离需要一定的物理距离,在代码中可以理解在不同的类。

高质量代码的评价标准

高质量代码最常用的、最重要的评价标准是:

  1. 可扩展性
  2. 可维护性
  3. 可读性
  4. 可测试性

1. 可扩展性

面向过程:
针对细节编程,会导致代码出现大量的if else逻辑判断,每增加一个商品类型,PriceCalculate函数就要增加对应的if else分支,如果业务存在多个针对具体类型进行逻辑编码的逻辑,就要找到所有的地方分支,当商品类型随着业务的增长多达十几种,每种类型都有共有的和特有的业务逻辑相互混杂,导致代码一片混乱,每增加一个类型都是一种煎熬,生怕改漏了一个地方,漏了一个地方就会出现bug,这就是代码的可扩展性差

面向对象:
每一种商品的价格计算逻辑封装在自己的实现类,这就是隔离变化
外界通过断言接口调用PriceCalculate函数,这就是面向接口编程
每增加一个商品类型,实现对应的接口,已有代码不需要改动,这就是代码的可扩展性好

2. 可维护性

面向过程:
当排查bug时,大量的逻辑分支导致难以定位是哪一个分支的代码块出现问题,这就是代码的可维护性差

面向对象:
当排查bug时,可以快速定位到具体哪个实现类的函数中,,这就是代码的可维护性好

3. 可读性

面向过程:
随着商品类型的增加,每种类型又有非常复杂计算价格的算法过程,导致PriceCalculate函数行数剧增,程序越长越难以理解(可参照圈复杂度),这就是代码的可阅读性差

面向对象:
每种类型商品的价格计算对应各自的实现类中,PriceCalculate实现函数逻辑简单清晰,少而不杂,这就是代码的可阅读性好

4. 可测试性

面向过程:
PriceCalculate函数包含着每种商品类型的价格逻辑计算,导致针对的单元测试用例难以编写,这就是代码的可测试性差

面向对象:
每个商品实现类的PriceCalculate实现函数只专注干一件事,单元测试可以轻松全面覆盖各种正常和异常情况,这就是代码的可测试性好

下一篇文章,介绍这个案例基础上涉及到的设计原则的知识。