Go函数重构案例研究
2022-03-065.8k 阅读
一、背景与初始代码
在软件开发过程中,随着业务需求的不断变化和功能的逐步扩展,代码往往会变得复杂且难以维护。函数作为代码的基本组成单元,其设计的优劣直接影响着整个项目的可维护性和可扩展性。我们以一个简单的电商订单处理系统为例,来探讨Go函数的重构过程。
假设最初的代码是为了处理订单的创建和简单的计算功能。下面是初始版本的代码示例:
package main
import (
"fmt"
)
// 定义订单结构体
type Order struct {
OrderID int
Items []Item
TotalPrice float64
}
// 定义订单项结构体
type Item struct {
ItemID int
Quantity int
Price float64
}
// 创建订单函数,此函数承担了过多职责
func CreateOrder(orderID int, itemIDs []int, quantities []int, prices []float64) (*Order, error) {
if len(itemIDs) != len(quantities) || len(itemIDs) != len(prices) {
return nil, fmt.Errorf("数量不匹配")
}
var items []Item
totalPrice := 0.0
for i := 0; i < len(itemIDs); i++ {
item := Item{
ItemID: itemIDs[i],
Quantity: quantities[i],
Price: prices[i],
}
items = append(items, item)
totalPrice += float64(quantities[i]) * prices[i]
}
order := Order{
OrderID: orderID,
Items: items,
TotalPrice: totalPrice,
}
return &order, nil
}
func main() {
orderID := 1
itemIDs := []int{101, 102}
quantities := []int{2, 3}
prices := []float64{10.5, 20.0}
order, err := CreateOrder(orderID, itemIDs, quantities, prices)
if err != nil {
fmt.Println("创建订单错误:", err)
return
}
fmt.Printf("订单ID: %d, 总价: %.2f\n", order.OrderID, order.TotalPrice)
}
1.1 初始代码问题分析
- 单一职责原则违背:
CreateOrder
函数不仅负责创建订单对象,还进行了订单项的构建以及总价的计算。这使得函数职责过多,违反了单一职责原则(SRP)。当需要修改订单项的构建逻辑或者总价的计算逻辑时,可能会影响到订单创建的核心功能。 - 代码可读性与可维护性差:由于一个函数承担了多项任务,函数体变得冗长。在阅读代码时,很难快速定位到特定功能的实现部分。如果后续需要对某一部分功能进行修改,例如调整总价的计算方式,需要在整个函数体中仔细查找相关代码,增加了维护的难度。
- 可测试性不佳:因为函数职责复杂,在编写单元测试时,很难对某一个特定功能进行单独测试。比如要测试总价的计算逻辑,就需要构建完整的订单创建环境,这使得测试变得复杂且耦合度高。
二、第一次重构:分离职责
为了改善上述问题,我们首先进行职责分离。将订单项的构建和总价的计算从CreateOrder
函数中分离出来,形成独立的函数。
package main
import (
"fmt"
)
// 定义订单结构体
type Order struct {
OrderID int
Items []Item
TotalPrice float64
}
// 定义订单项结构体
type Item struct {
ItemID int
Quantity int
Price float64
}
// 构建订单项函数
func buildItems(itemIDs []int, quantities []int, prices []float64) ([]Item, error) {
if len(itemIDs) != len(quantities) || len(itemIDs) != len(prices) {
return nil, fmt.Errorf("数量不匹配")
}
var items []Item
for i := 0; i < len(itemIDs); i++ {
item := Item{
ItemID: itemIDs[i],
Quantity: quantities[i],
Price: prices[i],
}
items = append(items, item)
}
return items, nil
}
// 计算总价函数
func calculateTotalPrice(items []Item) float64 {
totalPrice := 0.0
for _, item := range items {
totalPrice += float64(item.Quantity) * item.Price
}
return totalPrice
}
// 创建订单函数,职责更清晰
func CreateOrder(orderID int, itemIDs []int, quantities []int, prices []float64) (*Order, error) {
items, err := buildItems(itemIDs, quantities, prices)
if err != nil {
return nil, err
}
totalPrice := calculateTotalPrice(items)
order := Order{
OrderID: orderID,
Items: items,
TotalPrice: totalPrice,
}
return &order, nil
}
func main() {
orderID := 1
itemIDs := []int{101, 102}
quantities := []int{2, 3}
prices := []float64{10.5, 20.0}
order, err := CreateOrder(orderID, itemIDs, quantities, prices)
if err != nil {
fmt.Println("创建订单错误:", err)
return
}
fmt.Printf("订单ID: %d, 总价: %.2f\n", order.OrderID, order.TotalPrice)
}
2.1 第一次重构效果分析
- 单一职责原则遵循:通过分离出
buildItems
和calculateTotalPrice
函数,CreateOrder
函数的职责变得单一,只专注于订单的创建过程,而将订单项构建和总价计算的任务交给了专门的函数。这样每个函数都有明确的职责,符合单一职责原则。 - 代码可读性提升:重构后的代码结构更加清晰,每个函数的功能一目了然。在阅读
CreateOrder
函数时,可以很容易地理解订单创建的流程,而不需要在复杂的代码块中寻找特定功能的实现。 - 可维护性增强:当需要修改订单项的构建逻辑或者总价的计算逻辑时,只需要修改对应的
buildItems
或calculateTotalPrice
函数,不会对CreateOrder
函数的其他部分产生影响。这大大降低了代码维护的风险。 - 可测试性改善:现在可以针对每个独立的函数编写单元测试。例如,单独测试
buildItems
函数时,只需要关注其输入输出是否符合预期的订单项构建逻辑;测试calculateTotalPrice
函数时,只需要验证总价计算的正确性,而不需要考虑订单创建的其他因素,使得测试更加简单和独立。
三、第二次重构:参数优化
虽然第一次重构已经使代码有了很大的改善,但在参数传递方面还存在一些可以优化的地方。当前CreateOrder
函数接收多个切片作为参数,这使得函数调用时参数的顺序和数量需要特别注意,容易出错。我们可以通过创建一个新的结构体来传递参数,使函数调用更加清晰和安全。
package main
import (
"fmt"
)
// 定义订单结构体
type Order struct {
OrderID int
Items []Item
TotalPrice float64
}
// 定义订单项结构体
type Item struct {
ItemID int
Quantity int
Price float64
}
// 构建订单项函数
func buildItems(itemInfos []ItemInfo) ([]Item, error) {
if len(itemInfos) == 0 {
return nil, fmt.Errorf("没有订单项信息")
}
var items []Item
for _, info := range itemInfos {
item := Item{
ItemID: info.ItemID,
Quantity: info.Quantity,
Price: info.Price,
}
items = append(items, item)
}
return items, nil
}
// 计算总价函数
func calculateTotalPrice(items []Item) float64 {
totalPrice := 0.0
for _, item := range items {
totalPrice += float64(item.Quantity) * item.Price
}
return totalPrice
}
// 定义订单项信息结构体,用于传递参数
type ItemInfo struct {
ItemID int
Quantity int
Price float64
}
// 定义创建订单参数结构体
type CreateOrderParams struct {
OrderID int
ItemInfos []ItemInfo
}
// 创建订单函数,参数更清晰
func CreateOrder(params CreateOrderParams) (*Order, error) {
items, err := buildItems(params.ItemInfos)
if err != nil {
return nil, err
}
totalPrice := calculateTotalPrice(items)
order := Order{
OrderID: params.OrderID,
Items: items,
TotalPrice: totalPrice,
}
return &order, nil
}
func main() {
orderID := 1
itemInfos := []ItemInfo{
{ItemID: 101, Quantity: 2, Price: 10.5},
{ItemID: 102, Quantity: 3, Price: 20.0},
}
params := CreateOrderParams{
OrderID: orderID,
ItemInfos: itemInfos,
}
order, err := CreateOrder(params)
if err != nil {
fmt.Println("创建订单错误:", err)
return
}
fmt.Printf("订单ID: %d, 总价: %.2f\n", order.OrderID, order.TotalPrice)
}
3.1 第二次重构效果分析
- 参数传递清晰:通过创建
CreateOrderParams
结构体,将订单ID和订单项信息整合在一起作为CreateOrder
函数的参数。这样在调用函数时,参数的含义更加明确,不容易因为参数顺序错误而导致难以排查的问题。 - 代码可扩展性增强:如果后续订单创建需要更多的参数,例如客户信息、优惠信息等,只需要在
CreateOrderParams
结构体中添加相应的字段即可,而不需要修改函数的参数列表结构,使得代码的扩展性得到提升。 - 代码可读性进一步提升:在
main
函数中调用CreateOrder
函数时,通过结构体字面量的方式传递参数,代码更加清晰易懂,能够直观地看出每个参数的作用。
四、第三次重构:错误处理优化
在前面的代码中,错误处理相对简单,只是简单地返回错误信息。在实际的生产环境中,我们需要更细致的错误处理,例如区分不同类型的错误,以便上层调用者能够做出更合适的处理。
package main
import (
"errors"
"fmt"
)
// 定义订单结构体
type Order struct {
OrderID int
Items []Item
TotalPrice float64
}
// 定义订单项结构体
type Item struct {
ItemID int
Quantity int
Price float64
}
// 定义订单项构建错误类型
var ErrInvalidItemInfo = errors.New("无效的订单项信息")
// 构建订单项函数
func buildItems(itemInfos []ItemInfo) ([]Item, error) {
if len(itemInfos) == 0 {
return nil, ErrInvalidItemInfo
}
var items []Item
for _, info := range itemInfos {
if info.Quantity <= 0 || info.Price <= 0 {
return nil, ErrInvalidItemInfo
}
item := Item{
ItemID: info.ItemID,
Quantity: info.Quantity,
Price: info.Price,
}
items = append(items, item)
}
return items, nil
}
// 计算总价函数
func calculateTotalPrice(items []Item) float64 {
totalPrice := 0.0
for _, item := range items {
totalPrice += float64(item.Quantity) * item.Price
}
return totalPrice
}
// 定义订单项信息结构体,用于传递参数
type ItemInfo struct {
ItemID int
Quantity int
Price float64
}
// 定义创建订单参数结构体
type CreateOrderParams struct {
OrderID int
ItemInfos []ItemInfo
}
// 创建订单函数,错误处理更细致
func CreateOrder(params CreateOrderParams) (*Order, error) {
items, err := buildItems(params.ItemInfos)
if err != nil {
return nil, err
}
totalPrice := calculateTotalPrice(items)
order := Order{
OrderID: params.OrderID,
Items: items,
TotalPrice: totalPrice,
}
return &order, nil
}
func main() {
orderID := 1
itemInfos := []ItemInfo{
{ItemID: 101, Quantity: 2, Price: 10.5},
{ItemID: 102, Quantity: 3, Price: 20.0},
}
params := CreateOrderParams{
OrderID: orderID,
ItemInfos: itemInfos,
}
order, err := CreateOrder(params)
if err != nil {
if errors.Is(err, ErrInvalidItemInfo) {
fmt.Println("订单项信息无效:", err)
} else {
fmt.Println("其他错误:", err)
}
return
}
fmt.Printf("订单ID: %d, 总价: %.2f\n", order.OrderID, order.TotalPrice)
}
4.1 第三次重构效果分析
- 错误类型区分:通过定义
ErrInvalidItemInfo
错误类型,使得buildItems
函数能够返回特定类型的错误。在CreateOrder
函数的调用处,可以使用errors.Is
函数来判断错误类型,从而做出更有针对性的处理。 - 错误处理灵活性提升:上层调用者可以根据不同的错误类型采取不同的措施。例如,如果是订单项信息无效的错误,可以提示用户检查输入;如果是其他未知错误,可以记录详细的错误日志以便排查问题。
- 代码健壮性增强:更细致的错误处理使得代码在面对各种异常情况时能够更加稳健地运行,减少因为错误处理不当而导致的程序崩溃或数据不一致等问题。
五、第四次重构:代码复用与模块化
随着业务的发展,可能会有多个地方需要计算总价或者构建订单项。我们可以将这些功能模块独立出来,形成可复用的代码。
5.1 创建独立模块
首先,创建一个orderutils
包来存放与订单相关的工具函数。
// orderutils/orderutils.go
package orderutils
import (
"errors"
)
// 定义订单项结构体
type Item struct {
ItemID int
Quantity int
Price float64
}
// 定义订单项构建错误类型
var ErrInvalidItemInfo = errors.New("无效的订单项信息")
// 构建订单项函数
func BuildItems(itemInfos []ItemInfo) ([]Item, error) {
if len(itemInfos) == 0 {
return nil, ErrInvalidItemInfo
}
var items []Item
for _, info := range itemInfos {
if info.Quantity <= 0 || info.Price <= 0 {
return nil, ErrInvalidItemInfo
}
item := Item{
ItemID: info.ItemID,
Quantity: info.Quantity,
Price: info.Price,
}
items = append(items, item)
}
return items, nil
}
// 计算总价函数
func CalculateTotalPrice(items []Item) float64 {
totalPrice := 0.0
for _, item := range items {
totalPrice += float64(item.Quantity) * item.Price
}
return totalPrice
}
// 定义订单项信息结构体,用于传递参数
type ItemInfo struct {
ItemID int
Quantity int
Price float64
}
然后在主程序中使用这个模块:
package main
import (
"fmt"
"yourpackage/orderutils"
)
// 定义订单结构体
type Order struct {
OrderID int
Items []orderutils.Item
TotalPrice float64
}
// 定义创建订单参数结构体
type CreateOrderParams struct {
OrderID int
ItemInfos []orderutils.ItemInfo
}
// 创建订单函数,使用orderutils包
func CreateOrder(params CreateOrderParams) (*Order, error) {
items, err := orderutils.BuildItems(params.ItemInfos)
if err != nil {
return nil, err
}
totalPrice := orderutils.CalculateTotalPrice(items)
order := Order{
OrderID: params.OrderID,
Items: items,
TotalPrice: totalPrice,
}
return &order, nil
}
func main() {
orderID := 1
itemInfos := []orderutils.ItemInfo{
{ItemID: 101, Quantity: 2, Price: 10.5},
{ItemID: 102, Quantity: 3, Price: 20.0},
}
params := CreateOrderParams{
OrderID: orderID,
ItemInfos: itemInfos,
}
order, err := CreateOrder(params)
if err != nil {
if errors.Is(err, orderutils.ErrInvalidItemInfo) {
fmt.Println("订单项信息无效:", err)
} else {
fmt.Println("其他错误:", err)
}
return
}
fmt.Printf("订单ID: %d, 总价: %.2f\n", order.OrderID, order.TotalPrice)
}
5.2 第四次重构效果分析
- 代码复用性提高:通过将
BuildItems
和CalculateTotalPrice
函数封装到orderutils
包中,其他模块如果需要计算总价或者构建订单项,只需要引入这个包即可,避免了重复代码的编写。 - 模块化设计:将相关功能模块化,使得代码结构更加清晰。不同的模块专注于不同的功能,降低了模块之间的耦合度。例如,如果需要修改总价的计算逻辑,只需要在
orderutils
包中修改CalculateTotalPrice
函数,而不会影响到主程序的其他部分。 - 项目可维护性提升:模块化的设计使得代码的维护更加容易。当某个功能出现问题时,可以快速定位到对应的模块进行修改,而不会对整个项目造成过大的影响。同时,新的开发人员也更容易理解和上手项目,因为每个模块的功能都相对独立和明确。
六、总结重构过程与收获
通过以上四次重构,我们从一个简单但存在诸多问题的订单创建函数逐步优化为一个职责清晰、参数合理、错误处理完善且具有良好代码复用性和模块化的代码结构。在这个过程中,我们深刻体会到了以下几点:
- 单一职责原则的重要性:确保每个函数只负责一项主要功能,这样可以使代码更加清晰、可维护和可测试。
- 参数优化的意义:合理的参数设计可以提高代码的可读性和可扩展性,减少因参数传递错误而导致的问题。
- 细致的错误处理:能够增强代码的健壮性,使程序在面对异常情况时能够更好地应对,为用户提供更友好的反馈。
- 代码复用与模块化:可以提高开发效率,减少重复代码,降低模块之间的耦合度,提升整个项目的可维护性。
在实际的Go语言开发中,我们应该时刻关注代码的质量,通过不断的重构来优化代码,以适应业务的发展和变化。