refactor-basic.md 11 KB

#重构篇

什么是重构?

重构,一言以蔽之,就是在不改变外部行为的前提下,有条不紊地改善代码。

相似的

代码重构(英语:Code refactoring)指对软件代码做任何更动以增加可读性或者简化结构而不影响输出结果。

##网站重构

与上述相似的是:在不改变外部行为的前提下,简化结构、添加可读性,而在网站前端保持一致的行为。也就是说是在不改变UI的情况下,对网站进行优化,在扩展的同时保持一致的UI。

过去人们所说的网站重构

把"未采用CSS,大量使用HTML进行定位、布局,或者虽然已经采用CSS,但是未遵循HTML结构化标准的站点"变成"让标记回归标记的原本意义。通过在HTML文档中使用结构化的标记以及用CSS控制页面表现,使页面的实际内容与它们呈现的格式相分离的站点。"的过程就是网站重构(Website Reconstruction)

依照我做过的一些案例,对于传统的网站来说重构通常是

  • 表格(table)布局改为DIV+CSS
  • 使网站前端兼容于现代浏览器(针对于不合规范的CSS、如对IE6有效的)
  • 对于移动平台的优化
  • 针对于SEO进行优化

过去的网站重构就是“DIV+CSS”,想法固然极度局限。但也不是另一部分的人认为是“XHTML+CSS”,因为“XHTML+CSS”只是页面重构。

而真正的网站重构

应包含结构、行为、表现三层次的分离以及优化,行内分工优化,以及以技术与数据、人文为主导的交互优化等。

深层次的网站重构应该考虑的方面

  • 减少代码间的耦合
  • 让代码保持弹性
  • 严格按规范编写代码
  • 设计可扩展的API
  • 代替旧有的框架、语言(如VB)
  • 增强用户体验

通常来说对于速度的优化也包含在重构中

  • 压缩JS、CSS、image等前端资源(通常是由服务器来解决)
  • 程序的性能优化(如数据读写)
  • 采用CDN来加速资源加载
  • 对于JS DOM的优化
  • HTTP服务器的文件缓存

可以应用的的方面

  • 使用Ngx_pagespeed优化前端
  • 解耦复杂的模块
  • 对缓存进行优化
  • 针对于内容创建或预留API
  • 需要添加新API,如(weChat等的支持)
  • 用新的语言、框架代码旧的框架(如VB.NET,C#.NET)

###网站重构目的

希望自己的网站

  • 成本变得更低
  • 运行得更好
  • 访问者更多
  • 维护愈加简单
  • 功能更强

##代码重构

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

写代码容易,读代码难。

##使用工具重构

##借助工具重构

  • 当你写了一大堆代码,你没有意识到里面有一大堆重复。
  • 当你写了一大堆测试,却不知道覆盖率有多少。

这就是个问题了,于是偶然间看到了一个叫code climate的网站。

###Code Climate

Code Climate consolidates the results from a suite of static analysis tools into a single, real-time report, giving your team the information it needs to identify hotspots, evaluate new approaches, and improve code quality.

Code Climate整合一组静态分析工具的结果到一个单一的,实时的报告,让您的团队需要识别热点,探讨新的方法,提高代码质量的信息。

简单地来说:

  • 对我们的代码评分
  • 找出代码中的坏味道

于是,我们先来了个例子

Rating Name Complexity Duplication Churn C/M Coverage Smells
A lib/coap/coap_request_handler.js 24 0 6 2.6 46.4% 0
A lib/coap/coap_result_helper.js 14 0 2 3.4 80.0% 0
A lib/coap/coap_server.js 16 0 5 5.2 44.0% 0
A lib/database/db_factory.js 8 0 3 3.8 92.3% 0
A lib/database/iot_db.js 7 0 6 1.0 58.8% 0
A lib/database/mongodb_helper.js 63 0 11 4.5 35.0% 0
C lib/database/sqlite_helper.js 32 86 10 4.5 35.0% 2
B lib/rest/rest_helper.js 19 62 3 4.7 37.5% 2
A lib/rest/rest_server.js 17 0 2 8.6 88.9% 0
A lib/url_handler.js 9 0 5 2.2 94.1% 0

分享得到的最后的结果是:

Coverage

####代码的坏味道

于是我们就打开lib/database/sqlite_helper.js,因为其中有两个坏味道

Similar code found in two :expression_statement nodes (mass = 86)

在代码的 lib/database/sqlite_helper.js:58…61 < >

SQLiteHelper.prototype.deleteData = function (url, callback) {
    'use strict';
    var sql_command = "DELETE FROM  " + config.table_name + "  where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url);
    SQLiteHelper.prototype.basic(sql_command, callback);

lib/database/sqlite_helper.js:64…67 < >

SQLiteHelper.prototype.getData = function (url, callback) {
    'use strict';
    var sql_command = "SELECT * FROM  " + config.table_name + "  where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url);
    SQLiteHelper.prototype.basic(sql_command, callback);

只是这是之前修改过的重复。。

原来的代码是这样的

SQLiteHelper.prototype.postData = function (block, callback) {
    'use strict';
    var db = new sqlite3.Database(config.db_name);
    var str = this.parseData(config.keys);
    var string = this.parseData(block);

    var sql_command = "insert or replace into " + config.table_name + " (" + str + ") VALUES (" + string + ");";
    db.all(sql_command, function (err) {
        SQLiteHelper.prototype.errorHandler(err);
        db.close();
        callback();
    });
};

SQLiteHelper.prototype.deleteData = function (url, callback) {
    'use strict';
    var db = new sqlite3.Database(config.db_name);
    var sql_command = "DELETE FROM  " + config.table_name + "  where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url);
    db.all(sql_command, function (err) {
        SQLiteHelper.prototype.errorHandler(err);
        db.close();
        callback();
    });
};

SQLiteHelper.prototype.getData = function (url, callback) {
    'use strict';
    var db = new sqlite3.Database(config.db_name);
    var sql_command = "SELECT * FROM  " + config.table_name + "  where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url);
    db.all(sql_command, function (err, rows) {
        SQLiteHelper.prototype.errorHandler(err);
        db.close();
        callback(JSON.stringify(rows));
    });
};

说的也是大量的重复,重构完的代码

SQLiteHelper.prototype.basic = function(sql, db_callback){
    'use strict';
    var db = new sqlite3.Database(config.db_name);
    db.all(sql, function (err, rows) {
        SQLiteHelper.prototype.errorHandler(err);
        db.close();
        db_callback(JSON.stringify(rows));
    });

};

SQLiteHelper.prototype.postData = function (block, callback) {
    'use strict';
    var str = this.parseData(config.keys);
    var string = this.parseData(block);

    var sql_command = "insert or replace into " + config.table_name + " (" + str + ") VALUES (" + string + ");";
    SQLiteHelper.prototype.basic(sql_command, callback);
};

SQLiteHelper.prototype.deleteData = function (url, callback) {
    'use strict';
    var sql_command = "DELETE FROM  " + config.table_name + "  where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url);
    SQLiteHelper.prototype.basic(sql_command, callback);
};

SQLiteHelper.prototype.getData = function (url, callback) {
    'use strict';
    var sql_command = "SELECT * FROM  " + config.table_name + "  where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url);
    SQLiteHelper.prototype.basic(sql_command, callback);
};

重构完后的代码比原来还长,这似乎是个问题~~

##测试驱动开发

###一次测试驱动开发的故事

之前正在重写一个物联网的服务端,主要便是结合CoAP、MQTT、HTTP等协议构成一个物联网的云服务。现在,主要的任务是集中于协议与授权。由于,不同协议间的授权是不一样的,最开始的时候我先写了一个http put授权的功能,而在起先的时候是如何测试的呢?

curl --user root:root -X PUT -d '{ "dream": 1 }' -H "Content-Type: application/json" http://localhost:8899/topics/test

我只要顺利在request中看有无req.headers.authorization,我便可以继续往下,接着给个判断。毕竟,我们对HTTP协议还是蛮清楚的。

    if (!req.headers.authorization) {
      res.statusCode = 401;
      res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"');
      return res.end('Unauthorized');
    }

可是除了HTTP协议,还有MQTT和CoAP。对于MQTT协议来说,那还算好,毕竟自带授权,如:

    mosquitto_pub -u root -P root -h localhost -d -t lettuce -m "Hello, MQTT. This is my first message."

便可以让我们简单地完成这个功能,然而有的协议是没有这样的功能如CoAP协议中是用Option来进行授权的。现在的工具如libcoap只能有如下的简单功能

coap-client -m get coap://127.0.0.1:5683/topics/zero -T

于是,先写了个测试脚本来验证功能。

	var coap     = require('coap');
	var request  = coap.request;
	var req = request({hostname: 'localhost',port:5683,pathname: '',method: 'POST'});

	...

	req.setHeader("Accept", "application/json");
	req.setOption('Block2',  [new Buffer('phodal'), new Buffer('phodal')]);

	...

	req.end();

写完测试脚本后发现不对了,这个不应该是测试的代码吗? 于是将其放到了spec中,接着发现了上面的全部功能的实现过程为什么不用TDD实现呢?

###说说测试驱动开发

测试驱动开发是一个很"古老"的程序开发方法,然而由于国内的开发流程的问题——即开发人员负责功能的测试,导致这么好的一项技术没有在国内推广。

测试驱动开发的主要过程是:

  1. 先写功能的测试
  2. 实现功能代码
  3. 提交代码(commit -> 保证功能正常)
  4. 重构功能代码

而对于这样的一个物联网项目来说,我已经有了几个有利的前提:

  1. 已经有了原型
  2. 框架设计

###思考

通常在我的理解下,TDD是可有可无的。既然我知道了我要实现的大部分功能,而且我也知道如何实现。与此同时,对Code Smell也保持着警惕、要保证功能被测试覆盖。那么,总的来说TDD带来的价值并不大。

然而,在当前这种情况下,我知道我想要的功能,但是我并不理解其深层次的功能。我需要花费大量的时候来理解,它为什么是这样的,需要先有一些脚本来知道它是怎么工作的。TDD变显得很有价值,换句话来说,在现有的情况下,TDD对于我们不了解的一些事情,可以驱动出更多的开发。毕竟在我们完成测试脚本之后,我们也会发现这些测试脚本成为了代码的一部分。

在这种理想的情况下,我们为什么不TDD呢?