第四个Kata,名字叫“Data Munging”,一看Data大家就知道了,这次是要处理数据。
任务分三步:
- 处理weather.dat,找到温差最小的一天,第二列和第三列分别对应每天的最高温度和最低温度
- 处理football.dat,找到进球数和丢球数之差最小的一只球队,“-”符号左侧是进球数,右侧是丢球数
- 重构上面两步的代码
输入数据
我知道你们肯定懒得去点数据链接,所以我直接把数据贴在这里
weather.dat:
Dy MxT MnT AvT HDDay AvDP 1HrP TPcpn WxType PDir AvSp Dir MxS SkyC MxR MnR AvSLP
1 88 59 74 53.8 0.00 F 280 9.6 270 17 1.6 93 23 1004.5
2 79 63 71 46.5 0.00 330 8.7 340 23 3.3 70 28 1004.5
3 77 55 66 39.6 0.00 350 5.0 350 9 2.8 59 24 1016.8
4 77 59 68 51.1 0.00 110 9.1 130 12 8.6 62 40 1021.1
5 90 66 78 68.3 0.00 TFH 220 8.3 260 12 6.9 84 55 1014.4
6 81 61 71 63.7 0.00 RFH 030 6.2 030 13 9.7 93 60 1012.7
7 73 57 65 53.0 0.00 RF 050 9.5 050 17 5.3 90 48 1021.8
8 75 54 65 50.0 0.00 FH 160 4.2 150 10 2.6 93 41 1026.3
9 86 32* 59 6 61.5 0.00 240 7.6 220 12 6.0 78 46 1018.6
10 84 64 74 57.5 0.00 F 210 6.6 050 9 3.4 84 40 1019.0
11 91 59 75 66.3 0.00 H 250 7.1 230 12 2.5 93 45 1012.6
12 88 73 81 68.7 0.00 RTH 250 8.1 270 21 7.9 94 51 1007.0
13 70 59 65 55.0 0.00 H 150 3.0 150 8 10.0 83 59 1012.6
14 61 59 60 5 55.9 0.00 RF 060 6.7 080 9 10.0 93 87 1008.6
15 64 55 60 5 54.9 0.00 F 040 4.3 200 7 9.6 96 70 1006.1
16 79 59 69 56.7 0.00 F 250 7.6 240 21 7.8 87 44 1007.0
17 81 57 69 51.7 0.00 T 260 9.1 270 29* 5.2 90 34 1012.5
18 82 52 67 52.6 0.00 230 4.0 190 12 5.0 93 34 1021.3
19 81 61 71 58.9 0.00 H 250 5.2 230 12 5.3 87 44 1028.5
20 84 57 71 58.9 0.00 FH 150 6.3 160 13 3.6 90 43 1032.5
21 86 59 73 57.7 0.00 F 240 6.1 250 12 1.0 87 35 1030.7
22 90 64 77 61.1 0.00 H 250 6.4 230 9 0.2 78 38 1026.4
23 90 68 79 63.1 0.00 H 240 8.3 230 12 0.2 68 42 1021.3
24 90 77 84 67.5 0.00 H 350 8.5 010 14 6.9 74 48 1018.2
25 90 72 81 61.3 0.00 190 4.9 230 9 5.6 81 29 1019.6
26 97* 64 81 70.4 0.00 H 050 5.1 200 12 4.0 107 45 1014.9
27 91 72 82 69.7 0.00 RTH 250 12.1 230 17 7.1 90 47 1009.0
28 84 68 76 65.6 0.00 RTFH 280 7.6 340 16 7.0 100 51 1011.0
29 88 66 77 59.7 0.00 040 5.4 020 9 5.3 84 33 1020.6
30 90 45 68 63.6 0.00 H 240 6.0 220 17 4.8 200 41 1022.7
mo 82.9 60.5 71.7 16 58.8 0.00 6.9 5.3
football.dat:
Team P W L D F A Pts
1. Arsenal 38 26 9 3 79 - 36 87
2. Liverpool 38 24 8 6 67 - 30 80
3. Manchester_U 38 24 5 9 87 - 45 77
4. Newcastle 38 21 8 9 74 - 52 71
5. Leeds 38 18 12 8 53 - 37 66
6. Chelsea 38 17 13 8 66 - 38 64
7. West_Ham 38 15 8 15 48 - 57 53
8. Aston_Villa 38 12 14 12 46 - 47 50
9. Tottenham 38 14 8 16 49 - 53 50
10. Blackburn 38 12 10 16 55 - 51 46
11. Southampton 38 12 9 17 46 - 54 45
12. Middlesbrough 38 12 9 17 35 - 47 45
13. Fulham 38 10 14 14 36 - 44 44
14. Charlton 38 10 14 14 38 - 49 44
15. Everton 38 11 10 17 45 - 57 43
16. Bolton 38 9 13 16 44 - 62 40
17. Sunderland 38 10 10 18 29 - 51 40
-------------------------------------------------------
18. Ipswich 38 9 9 20 41 - 64 36
19. Derby 38 8 6 24 33 - 63 30
20. Leicester 38 5 13 20 30 - 64 28
可以看到,这个题目其实很简单,只要掌握基本的读文件和split功能很轻易就能实现。
初步实现
按照题目要求,我们先完成了前两步,我用的是Python,小伙伴用的是Nodejs,上代码。
Python版:
def answer_1():
min_1 = None
with open("weather.dat") as f:
for line in f.readlines():
temp = line.strip().split()
if line[0] == "mo":
continue
try:
temp_min = abs(int(temp[1].replace("*", "")) - int(temp[2].replace("*", "")))
except:
continue
if not min_1:
min_1 = temp_min
elif temp_min < min_1:
min_1 = temp_min
print min_1
def answer_2():
min_2 = None
with open("football.dat") as f:
for line in f.readlines():
temp = line.strip().split()
if len(temp) != 10:
continue
temp_min = abs(int(temp[6]) - int(temp[8]))
if not min_2:
min_2 = temp_min
elif temp_min < min_2:
min_2 = temp_min
print min_2
answer_1()
answer_2()
Nodejs版:
// 问题1
var fs = require('fs');
fs.readFile("weather.dat", "utf8", function(err, data) {
var i;
var mSpread = 100;
var spread;
var result;
if (err) throw err;
lines = data.trim().split(/\n/);
lines = lines.splice(2, 30);
lines = lines.map(function(line) {
line = line.trim().split(/\s+/);
return line;
});
for (var i = 0; i < 30; i++) {
spread = lines[i][1] - lines[i][2];
console.log(spread);
if (spread < mSpread) {
mSpread = spread;
result = lines[i][0];
}
}
console.log("最小温差是第" + result + "天。" + "最小温差是" + mSpread);
});
// 问题2
var fs = require('fs');
fs.readFile("football.dat", "utf8", function(err, data) {
var i;
var l;
var mSpread = 100;
var spread;
var result;
if (err) throw err;
lines = data.trim().split(/\n/);
lines = lines.splice(1, 20);
lines = lines.map(function(line) {
line = line.trim().split(/\s+/);
return line;
});
for (i = 0, l = lines.length; i < l; i++) {
spread = Math.abs(lines[i][6] - lines[i][8]);
console.log(lines[i]);
if (spread < mSpread) {
mSpread = spread;
result = lines[i][0];
}
}
console.log("第" + result + "队。")
});
nsole.log("第" + result + "队。")
});
和我们想的一样,代码非常简单。我对Python很熟悉,两个小程序基本上10分钟搞定,小伙伴Nodejs连查带写也是很快就完成了。下面就是重构了。
重构
这个练习的目的就是重构。
我觉得重构大体上有两个层面吧,一个是具体的代码层面,一个是思想层面。前者落实的时候主要是一些方法,比如提取公共函数、修改变量名可读性、优化内存使用等等,后者主要体现在编程模式上。
编程模式的话我之前看过《大话编程模式》,很不错的一本书,推荐给大家。
纸上得来终觉浅,尤其是模式这种东西,自己不写个十遍八遍的很难体会到其中的奥妙,也就更难发现模式存在的问题。世界上没有完美的模式,学习模式的终极目标其实就是抛弃模式,不断用新模式替换旧模式,不断让模式更专注于某个领域。
回到这个Kata。
由于代码太简单了,用模式来改写就意义不大了,所以我们基本上是采用了代码层面的重构。不过即使是这么简单的代码,我和小伙伴的重构结果也是差异极大。
Python重构:
def common_answer(filename, filterfunc, generate, compare):
result = None
with open(filename) as f:
for line in f.readlines():
temp = line.strip().split()
if filterfunc(temp):
continue
temp = generate(temp)
if not result:
result = temp
elif compare(result, temp):
result = temp
return result
print common_answer("weather.dat", lambda b: not b or b[0] in ["Dy", "mo"], lambda b: abs(int(b[1].replace("*", "")) - int(b[2].replace("*", ""))), lambda a, b: a > b)
print common_answer("football.dat", lambda b: "." not in b[0], lambda b: abs(int(b[6]) - int(b[8])), lambda a, b: a > b)
Nodejs重构出了一个新文件common.js
function parseTable(tableData, startline, lineNum) {
var lines;
lines = tableData.trim().split(/\n/);
lines = lines.splice(startline, lineNum);
lines = lines.map(function(line) {
line = line.trim().split(/\s+/);
return line;
});
return lines;
}
function findMinSpread(data, col1, col2) {
var i;
var l;
var mSpread = 1000;
var result;
for (i = 0, l = data.length; i < l; i++) {
spread = Math.abs(data[i][col1] - data[i][col2]);
if (spread < mSpread) {
mSpread = spread;
result = data[i][0];
}
}
return result;
}
exports.parseTable = parseTable;
exports.findMinSpread = findMinSpread;
具体解决问题的代码如下:
// 问题1
var fs = require('fs');
var myCommon = require('./common.js');
fs.readFile("weather.dat", "utf8", function(err, data) {
var lines;
if (err) throw err;
lines = myCommon.parseTable(data, 1, 20);
console.log("最小温差是第" + myCommon.findMinSpread(lines, 1, 2) + "天。");
});
// 问题2
var fs = require('fs');
var myCommon = require('./common.js');
fs.readFile("football.dat", "utf8", function(err, data) {
var lines;
if (err) throw err;
lines = myCommon.parseTable(data, 1, 20);
console.log("第" + myCommon.findMinSpread(lines, 6, 8) + "队。")
});
可以看出,Python的重构比较偏向整体架构调整,将处理逻辑分成多步,每一步都由传入的函数处理,相对来说偏向函数式编程一点。而Nodejs的重构比较偏向提取公共函数,处理流程的重心仍然放在代码本身。
我觉得两种方法本质上没有孰优孰劣,我重构Python的时候更多考虑的是通用性,整体的架构更像一个框架,使用的时候传入各部分的处理函数,框架负责运行。小伙伴重构Nodejs的时候更多考虑的是易用性,整体的架构更像一个工具库,使用的时候直接调用公共函数,由用户代码负责运行。
我的思路是大一统,小伙伴的思路是各司其职。相对来说,我的思路会很大地限制使用范围,但是在范围内使用是非常方便的,小伙伴的思路可以应用在非常大的范围中,但是给用户带来的便利是有限的。
一句话,区别就是深和广,具体选择哪个还是要看使用场景以及个人习惯。
反思重构
尽管这个Kata很小,并没有用各种高端的设计模式,但是仍然给我们带来很多思考。
首先,重构是否有必要。
代码重构完,从逻辑上来说确实更加清晰,代码量更少,但是重构本质是抽象,对于阅读代码的人来说实际上会增加一定的理解难度,大家从Python例子就可以看出,重构之前读一遍代码就能读懂,重构之后需要不断比对参数和函数内容才能理解作用,并没有那么直观。
不过重构还是需要的,不仅可以使代码逻辑更加清晰,代码量更少,还可以增加代码的可维护性,可以说重构带来的副作用主要就体现在代码的理解难度上,而这恰恰就是程序员的职责所在。
其次,重构思路不一样。
上文已经详细介绍了重构思路的区别,可以看到,即使是这样一个简单的例子都可以有完全不同的重构方法,那大项目就更不用说了。如何选择重构思路其实和如何选择编程语言是一样的,关键是合适不合适,而不是思路优越不优越或者语言牛逼不牛逼。
抓住本质,不要浮在表面,否则迟早有一天你会被冲走。