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