改善 GitHub 项目代码质量:重构

或许你应该知道了,重构是怎样的,你也知道重构能带来什么。在我刚开始学重构和设计模式的时候,我需要去找一些好的示例,以便于我更好的学习。有时候不得不创造一些更好的场景,来实现这些功能。

有一天,我发现当我需要我一次又一次地重复讲述某些内容,于是我就计划着把这些应该掌握的技能放到GitHub上,也就有了Artisan Stack 计划。

每个程序员都不可避免地是一个Coder,一个没有掌握好技能的Coder,算不上是手工艺人,但是手工艺人,需要有创造性的方法。

为什么重构?

为了更好的代码。

在经历了一年多的工作之后,我平时的主要工作就是修Bug。刚开始的时候觉得无聊,后来才发现修Bug需要更好的技术。有时候你可能要面对着一坨一坨的代码,有时候你可能要花几天的时间去阅读代码。而你重写那几十行代码可能只会花上你不到一天的时间。但是如果你没办法理解当时为什么这么做,你的修改只会带来更多的Bug。修Bug,更多的是维护代码。还是前人总结的那句话对:

写代码容易,读代码难。

假设我们写这些代码只要半天,而别人读起来要一天。为什么不试着用一天的时候去写这些代码,让别人花半天或者更少的时间来理解。

如果你的代码已经上线,虽然是一坨坨的。但是不要轻易尝试没有测试的重构

从前端开始的原因在于,写得一坨坨且最不容易测试的代码都在前端。

让我们来看看我们的第一个训练,相当有挑战性。

重构uMarkdown

代码及setup请见github: js-refactor

代码说明

uMarkdown是一个用于将Markdown转化为HTML的库。代码看上去就像一个很典型的过程代码:

  1. /* code */
  2. while ((stra = micromarkdown.regexobject.code.exec(str)) !== null) {
  3. str = str.replace(stra[0], '<code>\n' + micromarkdown.htmlEncode(stra[1]).replace(/\n/gm, '<br/>').replace(/\ /gm, '&nbsp;') + '</code>\n');
  4. }
  5. /* headlines */
  6. while ((stra = micromarkdown.regexobject.headline.exec(str)) !== null) {
  7. count = stra[1].length;
  8. str = str.replace(stra[0], '<h' + count + '>' + stra[2] + '</h' + count + '>' + '\n');
  9. }
  10. /* mail */
  11. while ((stra = micromarkdown.regexobject.mail.exec(str)) !== null) {
  12. str = str.replace(stra[0], '<a href="mailto:' + stra[1] + '">' + stra[1] + '</a>');
  13. }

选这个做重构的开始,不仅仅是因为之前在写EchoesWorks的时候进行了很多的重构。而且它更适合于重构到设计模式的理论。让我们在重构完之后,给作者进行pull request吧。

Markdown的解析过程,有点类似于Pipe and Filters模式(架构模式)。

Filter即我们在代码中看到的正规表达式集:

  1. regexobject: {
  2. headline: /^(\#{1,6})([^\#\n]+)$/m,
  3. code: /\s\`\`\`\n?([^`]+)\`\`\`/g

他会匹配对应的Markdown类型,随后进行替换和处理。而``str```,就是管理口的输入和输出。

接着,我们就可以对其进行简单的重构。

(ps: 推荐用WebStrom来做重构,自带重构功能)

作为一个示例,我们先提出codeHandler方法,即将上面的

  1. /* code */
  2. while ((stra = micromarkdown.regexobject.code.exec(str)) !== null) {
  3. str = str.replace(stra[0], '<code>\n' + micromarkdown.htmlEncode(stra[1]).replace(/\n/gm, '<br/>').replace(/\ /gm, '&nbsp;') + '</code>\n');
  4. }

提取方法成

  1. codeFilter: function (str, stra) {
  2. return str.replace(stra[0], '<code>\n' + micromarkdown.htmlEncode(stra[1]).replace(/\n/gm, '<br/>').replace(/\ /gm, '&nbsp;') + '</code>\n');
  3. },

while语句就成了

  1. while ((stra = regexobject.code.exec(str)) !== null) {
  2. str = this.codeFilter(str, stra);
  3. }

然后,运行所有的测试。

  1. grunt test

同理我们就可以mailheadline等方法进行重构。接着就会变成类似于下面的代码,

  1. /* code */
  2. while ((execStr = regExpObject.code.exec(str)) !== null) {
  3. str = codeHandler(str, execStr);
  4. }
  5. /* headlines */
  6. while ((execStr = regExpObject.headline.exec(str)) !== null) {
  7. str = headlineHandler(str, execStr);
  8. }
  9. /* lists */
  10. while ((execStr = regExpObject.lists.exec(str)) !== null) {
  11. str = listHandler(str, execStr);
  12. }
  13. /* tables */
  14. while ((execStr = regExpObject.tables.exec(str)) !== null) {
  15. str = tableHandler(str, execStr, strict);
  16. }

然后你也看到了,上面有一堆重复的代码,接着让我们用JavaScript的奇技淫巧,即apply方法,把上面的重复代码变成。

  1. ['code', 'headline', 'lists', 'tables', 'links', 'mail', 'url', 'smlinks', 'hr'].forEach(function (type) {
  2. while ((stra = regexobject[type].exec(str)) !== null) {
  3. str = that[(type + 'Handler')].apply(that, [stra, str, strict]);
  4. }
  5. });

进行测试,blabla,都是过的。

  1. Markdown
  2. should parse h1~h3
  3. should parse link
  4. should special link
  5. should parse font style
  6. should parse code
  7. should parse ul list
  8. should parse ul table
  9. should return correctly class name

快来试试吧, https://github.com/artisanstack/js-refactor

是时候讨论这个Refactor利器了,最初看到这个重构的过程是从ThoughtWorks郑大晔校开始的,只是之前对于Java的另外一个编辑器Eclipse的坏感。。这些在目前已经不是很重要了,试试这个公司里面应用广泛的编辑器。

Intellij Idea重构

开发的流程大致就是这样子的,测试先行算是推荐的。

  1. 编写测试->功能代码->修改测试->重构

上次在和buddy聊天的时候,才知道测试在功能简单的时候是后行的,在功能复杂不知道怎么下手的时候是先行的。

开始之前请原谅我对于Java语言的一些无知,然后,看一下我写的Main函数:

  1. package com.phodal.learing;
  2. public class Main {
  3. public static void main(String[] args) {
  4. int c=new Cal().add(1,2);
  5. int d=new Cal2().sub(2,1);
  6. System.out.println("Hello,s");
  7. System.out.println(c);
  8. System.out.println(d);
  9. }
  10. }

代码写得还好(自我感觉),先不管Cal和Cal2两个类。大部分都能看懂,除了c,d不知道他们表达的是什么意思,于是。

Rename

快捷键:Shift+F6

作用:重命名

  • 把光标丢到int c中的c,按下shift+f6,输入result_add
  • 把光标移到int d中的d,按下shift+f6,输入result_sub

于是就有

  1. package com.phodal.learing;
  2. public class Main {
  3. public static void main(String[] args) {
  4. int result_add=new Cal().add(1,2);
  5. int result_sub=new Cal2().sub(2,1);
  6. System.out.println("Hello,s");
  7. System.out.println(result_add);
  8. System.out.println(result_sub);
  9. }
  10. }

Extract Method

快捷键:alt+command+m

作用:扩展方法

  • 选中System.out.println(result_add);
  • 按下alt+command+m
  • 在弹出的窗口中输入mprint

于是有了

  1. public static void main(String[] args) {
  2. int result_add=new Cal().add(1,2);
  3. int result_sub=new Cal2().sub(2,1);
  4. System.out.println("Hello,s");
  5. mprint(result_add);
  6. mprint(result_sub);
  7. }
  8. private static void mprint(int result_sub) {
  9. System.out.println(result_sub);
  10. }

似乎我们不应该这样对待System.out.println,那么让我们内联回去

Inline Method

快捷键:alt+command+n

作用:内联方法

  • 选中main中的mprint
  • alt+command+n
  • 选中Inline all invocations and remove the method(2 occurrences) 点确定

然后我们等于什么也没有做了~~:

  1. public static void main(String[] args) {
  2. int result_add=new Cal().add(1,2);
  3. int result_sub=new Cal2().sub(2,1);
  4. System.out.println("Hello,s");
  5. System.out.println(result_add);
  6. System.out.println(result_sub);
  7. }

似乎这个例子不是很好,但是够用来说明了。

Pull Members Up

开始之前让我们先看看Cal2类:

  1. public class Cal2 extends Cal {
  2. public int sub(int a,int b){
  3. return a-b;
  4. }
  5. }

以及Cal2的父类Cal

  1. public class Cal {
  2. public int add(int a,int b){
  3. return a+b;
  4. }
  5. }

最后的结果,就是将Cal2类中的sub方法,提到父类:

  1. public class Cal {
  2. public int add(int a,int b){
  3. return a+b;
  4. }
  5. public int sub(int a,int b){
  6. return a-b;
  7. }
  8. }

而我们所要做的就是鼠标右键

重构之以查询取代临时变量

快捷键

Mac: 木有

Windows/Linux: 木有

或者: Shift+alt+command+T 再选择 Replace Temp with Query

鼠标: Refactor | Replace Temp with Query

重构之前

过多的临时变量会让我们写出更长的函数,函数不应该太多,以便使功能单一。这也是重构的另外的目的所在,只有函数专注于其功能,才会更容易读懂。

以书中的代码为例

  1. import java.lang.System;
  2. public class replaceTemp {
  3. public void count() {
  4. double basePrice = _quantity * _itemPrice;
  5. if (basePrice > 1000) {
  6. return basePrice * 0.95;
  7. } else {
  8. return basePrice * 0.98;
  9. }
  10. }
  11. }

重构

选中basePrice很愉快地拿鼠标点上面的重构

Replace Temp With Query

便会返回

  1. import java.lang.System;
  2. public class replaceTemp {
  3. public void count() {
  4. if (basePrice() > 1000) {
  5. return basePrice() * 0.95;
  6. } else {
  7. return basePrice() * 0.98;
  8. }
  9. }
  10. private double basePrice() {
  11. return _quantity * _itemPrice;
  12. }
  13. }

而实际上我们也可以

  1. 选中

    _quantity * _itemPrice

  2. 对其进行Extrace Method

  3. 选择basePriceInline Method

Intellij IDEA重构

在Intellij IDEA的文档中对此是这样的例子

  1. public class replaceTemp {
  2. public void method() {
  3. String str = "str";
  4. String aString = returnString().concat(str);
  5. System.out.println(aString);
  6. }
  7. }

接着我们选中aString,再打开重构菜单,或者

Command+Alt+Shift+T 再选中Replace Temp with Query

便会有下面的结果:

  1. import java.lang.String;
  2. public class replaceTemp {
  3. public void method() {
  4. String str = "str";
  5. System.out.println(aString(str));
  6. }
  7. private String aString(String str) {
  8. return returnString().concat(str);
  9. }
  10. }