[go语言]避免过度重构
golang-nuts上有人提了一个问题[1],询问怎么样把减少两个类似数据结构和算法的重复代码。简而言之,有两个struct:QuickFindSet和QuickUnionSet,它们各有Count, IsConnected, Find, Union等方法。他发现这两个struct的一些函数的实现是一样的,因此他希望能消除这些重复代码。
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
|
// quick-find type QuickFindSet struct { numOfComponents uint items []uint } func NewSet(n uint) QuickFindSet { set := QuickFindSet{ numOfComponents: n, items: make([]uint, n) } for i, _ := range set.items { set.items[i] = uint(i) } return set } func (set *QuickFindSet) Count() uint { return set.numOfComponents } func (set *QuickFindSet) IsConnected (p, q uint) bool { return set.Find(p) == set.Find(q) } func (set *QuickFindSet) Find(p uint) uint { return set.items[p] } func (set *QuickFindSet) Union(p, q uint) { rootP := set.Find(p) rootQ := set.Find(q) if rootP == rootQ { return } for i, _ := range set.items { if set.items[i] == rootP { set.items[i] = rootQ } } set.numOfComponents-- } // weighted quick-union type QuickUnionSet struct { numOfComponents uint items []uint sizes []uint } func NewSet(n uint) QuickUnionSet { set := QuickUnionSet{ numOfComponents: n, items: make([]uint, n), sizes: make([]uint, n) } for i, _ := range set.items { set.items[i] = uint(i) set.sizes[i] = uint(1) } return set } func (set *QuickUnionSet) Count() uint { return set.numOfComponents } func (set *QuickUnionSet) IsConnected (p, q uint) bool { return set.Find(p) == set.Find(q) } func (set *QuickUnionSet) Find(p uint) uint { for p != set.items[p] { p = set.items[p] } return p } func (set *QuickUnionSet) Union(p, q uint) { rootP := set.Find(p) rootQ := set.Find(q) if rootP == rootQ { return } if set.sizes[rootP] < set.sizes[rootQ] { set.items[rootP] = rootQ set.sizes[rootQ] += set.sizes[rootP] } else { set.items[rootQ] = rootP set.sizes[rootP] += set.sizes[rootQ] } set.numOfComponents-- } |
可以看到,QuickFindSet和QuickUnionSet的Count和IsConnected函数的实现是一样的:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
func (set *QuickFindSet) Count() uint { return set.numOfComponents } func (set *QuickFindSet) IsConnected (p, q uint) bool { return set.Find(p) == set.Find(q) } func (set *QuickUnionSet) Count() uint { return set.numOfComponents } func (set *QuickUnionSet) IsConnected (p, q uint) bool { return set.Find(p) == set.Find(q) } |
作者说他编程的时候总是想把重复的代码消除掉,在其他的语言里可以用class或者宏来达到这个目的,他想知道在go语言里怎么样做比较好。
事实上这里还有一个问题:这里需要重构来消除重复代码吗?或者说,什么样的情况下需要重构代码?
我认为,以下条件同时满足时才需要重构代码:
代码非常的混乱以至于很难阅读
已有需有或者有潜在的需求需要修改代码
而对比这里的代码:
这里重复的代码非常少,只有几行代码。代码结构也比较清晰。
作为一个和业务逻辑关联不大的基础数据结构,变化的需求很小。
因此我的结论是:这里的代码不需要重构,不需要去消除那寥寥几行的重复代码。
也有人从另外一个角度提出意见:
减少这些算法的重复代码也意味着它们之间耦合更加紧密,如果以后需要修改其中的一个算法,会影响到其他的算法实现;而一定的重复代码可以保持它们之间的独立性。
Russ Cox也给出了类似的意见:
确实在其他语言里可以使用带有虚拟方法的抽象类来消除这样很小的代码重复,但它也会把这两个实际上只有很少共同点的算法实现捆绑在一起。保留独立代码的好处远远超过减少一两行重复代码的便利。(It is true that (in other languages) one could use 'abstract classes with virtual methods' to eliminate this minor duplication, but it would also tie together two implementations that really have very little in common. The benefits of keeping separate things separate far outweighs the minor convenience of avoiding a duplicated line or two. )
最后,我的结语是:
凡事都有个度,怎么样避免过度重构呢?也许我们可以从重构的动机和结果来考虑一下,而不只是为了重构而重构。