一个编程小题目引发的思考(下)
此篇文章接上篇 一个编程小题目引发的思考(上)
其实很多园友已经给出答案了,不过我在这里还是要写一下自己的思路
再把题目叙述一遍
输入:一个小于12位的十进制正整数
输出:打印此数字的十进制计算器表示 例: 输入:145 输出:
__ ||__||__ | | __|于是我又重新思考了一下这道题目,并Review了一下当前的解决方案,发现这个冗长的switch是个很大的问题,这是我想到了代码大全2里提到的表驱动编程方法(就是用一个表来代替冗长的分支控制逻辑)。 小心起见,我从一个方法开始,对其进行了重新组织:
private void PrintTopBody(int value) { if (value != 0) { string[] table = { S1, S0, S1, S1, S0, S1, S1, S1, S1, S1 }; PrintTopBody(value / 10); Console.Write(table[value % 10]); } }测试之后发现运行结果正常,我考虑可以对另外两个方法进行这样的重构,但我发现这样写出的代码依然不好维护,虽然短了很多,但是S1,S0等莫名奇妙的全局变量仍然令人很头疼。 所以接下来就是怎么消除这些恼人的全局字符串常量了。 一时间没想到什么方法,于是又重新运行了一下这个程序,得到了下面的结果:
__ __ __ __ __ __ __ __ | __| __||__||__ |__ ||__||__|| | ||__ __| | __||__| ||__| __||__|这时我突然发现,这个结果不就是一个字符串表吗?为什么不直接利用这个表呢? 于是我打开Regex,利用正则替换,将上面的字符重组为一个二维字符串表格:
private static readonly string[,] TABLE = { {" "," __ "," __ "," "," __ "," __ "," __ "," __ "," __ "," __ "}, {" |"," __|"," __|","|__|","|__ ","|__ "," |","|__|","|__|","| |"}, {" |","|__ "," __|"," |"," __|","|__|"," |","|__|"," __|","|__|"}, };相应的,我新创造了一个LCDPrinter1类,并按照之前的逻辑,编写了对应的方法,代码如下:
class LCDPrinter1 { private static readonly string[,] TABLE = { {" "," __ "," __ "," "," __ "," __ "," __ "," __ "," __ "," __ "}, {" |"," __|"," __|","|__|","|__ ","|__ "," |","|__|","|__|","| |"}, {" |","|__ "," __|"," |"," __|","|__|"," |","|__|"," __|","|__|"}, }; public void PrintNum(int value) { for (int i = 0; i < 3; i++) { PrintOneLayer(value, i); Console.WriteLine(); } } private void PrintOneLayer(int value, int layer) { if (value != 0) { PrintOneLayer(value / 10, layer); Console.Write(TABLE[layer, value % 10]); } } }然后进行测试,我输入123,但惊奇的发现结果是:
__ __ __| __||__| |__ __| |非常像一个off-by-one错误,我看了下字符串表格的定义,原来是0的位置错了,修改之后重新运行,结果正常。 下面是最终的代码:
class LCDPrinter1 { private static readonly string[,] TABLE = { {" __ "," "," __ "," __ "," "," __ "," __ "," __ "," __ "," __ ",}, {"| |"," |"," __|"," __|","|__|","|__ ","|__ "," |","|__|","|__|",}, {"|__|"," |","|__ "," __|"," |"," __|","|__|"," |","|__|"," __|",}, }; public void PrintNum(int value) { for (int i = 0; i < 3; i++) { PrintOneLayer(value, i); Console.WriteLine(); } } private void PrintOneLayer(int value, int layer) { if (value != 0) { PrintOneLayer(value / 10, layer); Console.Write(TABLE[layer, value % 10]); } } }可以发现这个解决方案不但短小(只有25行),而且清晰易懂,逻辑一目了然,相比之前那个150行的解决方案,可谓是天壤之别。
反思:
- 作为一个程序员,当我接到一个task的第一反应就是CODING(我想这也是大多数程序员的通病吧),然而这时我可能并没有对这个任务有一个清晰的认识,然后写出一摊虽然可以run但是看起来莫名其妙的代码。在完成任务之后马上进行下一个task,然后这一摊weird code就被搁置在那里。等过了一段时间之后,连我自己都看不懂了,想改也没法改,一是没有时间,二是可能有一些人用到了我的代码,修改的话会引发其各种不想看见的连带效应。
- 所以Jon Bentley在他的Programming Pearls一书中提到:Good programmers are a little bit lazy: they sit back and wait for an insight rather than rushing forward with their first idea。而我们在编程时是怎么做的呢?真实的情况时,我们往往过早的陷入到了实现功能的误区中,而忘记了原本问题到底是什么。即使是到后来insight灵光一现,也已经是too late to modify了。所谓磨刀不误砍柴工,就是这个道理。
- 在Geogre Polya的神作How to solve it?一书中,Polya为解决问题定义了一个系统化的方法:理解题目->规划解决方案->执行解决方案->对解决过程进行反思。Polya提到,我们很多人都只注意到了前三步,而最后一步,也是他认为非常重要的一步却被忽视了。要知道我们解决新的问题往往是基于我们已有的经验的,而这些经验并不是由重复性的工作中而来,而主要从对工作的反思中而来。
- 此外,科学家往往有这样的思维,那就是越复杂的问题的解释往往是非常简单的。Dirac甚至说:“一个理论家宁可要一个美的方程,也不要一个丑的但结果与实验数据更一致的方程。”举个简单的的例子,我们在小时候的数学考试中,如果得到的答案是1、2或者是10,我们往往会欣然接受答案;但如果计算的答案是11/17、1.947此类的数字时,我们往往会怀疑自己是不是算错了,原因很简单,这些答案的样子太邪恶了。
- 回到程序员的视角,如果当我们对一个问题给出一个自己都认为丑陋无比的解决方案时,这时很可能就是哪里出了问题:对问题的理解不够深入?使用了错误的数据结构?此时不应该去继续CODE,而是应该进行仔细的思考,换句话说,在一些情况下,程序员应该像Dirac那样,对优美的CODE有着近乎偏执的追求。当然了,那些manager会不会允许程序员这么做,就是另外一回事了。