Go 代码重构

0 阅读4分钟
1.提取方法

提取一段按意图分组的代码,并转移到新方法中。通过提取,将一个长方法或函数分解为多个小方法,每个小方法负责特定的逻辑。小方法的名称通常能够清楚地表明它的用途。

下面的示例显示了应用此重构技术之前和之后的情况。我的主要目标是通过将复杂逻辑拆分为不同功能模块,从而降低整体复杂度。

func StringCalculator(exp string) int {
 if exp == "" {
  return 0
 }

 var sum int
 for _, number := range strings.Split(exp, ",") {
  n, err := strconv.Atoi(number)
  if err != nil {
   return 0
  }
  sum += n
 }
 return sum
}

重构为:

func StringCalculator(exp string) int {
 if exp == "" {
  return 0
 }
 return sumAllNumberInExpression(exp)
}

func sumAllNumberInExpression(exp string) int {
 var sum int
 for _, number := range strings.Split(exp, ",") {
  sum += toInt(number)
 }
 return sum
}

func toInt(exp string) int {
 n, err := strconv.Atoi(exp)
 if err != nil {
  return 0
 }
 return n
}

StringCalculator 函数更简单了,但是当添加了两个新的函数时,它会增加复杂性。这是一个我愿意做出慎重决定的牺牲,我将此作为参考而不是规则,从某种意义上说,了解应用重构技术的结果可以很好地判断是否应用重构技术。

2.移动方法

有时,在使用提取方法后,我发现了另一个问题:此方法应该属于此结构或包吗?Move Method 是一种简单的技术,包括将方法从一个结构移动到另一个结构。我发现一个技巧,来确定某个方法是否应该属于该结构:弄清楚该方法是否访问了另一个结构依赖项的内部。看下面的例子:

type Book struct {
 ID    int
 Title string
}

type Books []Book

type User struct {
 ID    int
 Name  string
 Books Books
}

func (u User) Info() {
 fmt.Printf("ID:%d - Name:%s", u.ID, u.Name)
 fmt.Printf("Books:%d"len(u.Books))
 fmt.Printf("Books titles: %s", u.BooksTitles())
}

func (u User) BooksTitles() string {
 var titles []string
 for _, book := range u.Books {
  titles = append(titles, book.Title)
 }
 return strings.Join(titles, ",")
}

如你所见,User 的方法 BooksTitles 使用了 books(具体是 Title)中的内部字段多于 User,这表明该方法应归于 Books。应用这种重构技术将该方法移动到 Books 类型上,然后由用户的Info 方法调用。

func (b Books) Titles() string {
 var titles []string
 for _, book := range b {
  titles = append(titles, book.Title)
 }
 return strings.Join(titles, ",")
}

func (u User) Info() {
 fmt.Printf("ID:%d - Name:%s", u.ID, u.Name)
 fmt.Printf("Books:%d"len(u.Books))
 fmt.Printf("Books titles: %s", u.Books.Titles())
}

通过这种重构,Books 类型的职责更加明确,成为唯一直接管理自身字段和属性的结构。同样,这是在深思熟虑之前进行的思考过程,知道应用重构会带来什么结果。

3.引入参数对象

你见过多少像下面方法一样,有很多参数的:

func (om *OrderManager) Filter(startDate, endDate time.Time, country, state, city, status string) (Orders, error)

即使函数内部代码不可见,仅从参数数量来看,也可以推测它可能涉及复杂操作。

有时,我发现这些参数之间高度相关,并在以后定义它们的方法中一起使用。这为重构提供了一种使该场景更加面向对象的方式进行处理的方法,并且建议将这些参数分组为一个结构,替换方法签名以将该对象用作参数,并在方法内部使用该对象。

type OrderFilter struct {
 StartDate time.Time
 EndDate   time.Time
 Country   string
 State     string
 City      string
 Status    string
}

func (om *OrderManager) Filter(of OrderFilter) (Orders, error)

看起来更干净,并且可以确定这些参数的身份,但是这要求更新所有调用此方法的地方,并在调用前用 OrderFilter 创建参数对象。同样,在尝试进行此重构之前,我会尽力思考并考虑后果。当你的代码中的影响程度很低时,我认为此技术非常有效。

4.用符号常量替换魔数

此方法通过将硬编码值替换为具备明确含义的常量变量,使代码更具可读性和语义化。

func Add(input string) int {
 if input == "" {
  return 0
 }

 if strings.Contains(input, ";") {
  n1 := toNumber(input[:strings.Index(input, ";")])
  n2 := toNumber(input[strings.Index(input, ";")+1:])

  return n1 + n2
 }

 return toNumber(input)
}

func toNumber(input string) int {
 n, err := strconv.Atoi(input)
 if err != nil {
  return 0
 }
 return n
}

其中 ; 字符是什么意思?如果此字符含义不够明确,可以使用常量变量代替硬编码值,为其赋予清晰的语义。

func Add(input string) int {
 if input == "" {
  return 0
 }

 numberSeparator := ";"
 if strings.Contains(input, numberSeparator) {
  n1 := toNumber(input[:strings.Index(input, numberSeparator)])
  n2 := toNumber(input[strings.Index(input, numberSeparator)+1:])

  return n1 + n2
 }

 return toNumber(input)
}

func toNumber(input string) int {
 n, err := strconv.Atoi(input)
 if err != nil {
  return 0
 }
 return n
}

在进行重构时,要权衡利弊,避免过度重构或引入额外的复杂性。每种技术都有特定的应用场景,理解它们的结果和潜在影响是重构成功的关键。