最近做了挺多从不同的网页抓取数据的工作,重复多了之后,有了重构的想法,使用的语言是java。
1. 以前的做法:
因为是一个功能性程序,所以把它当做了过称式程序,没有建立特别的类:
public static void main(String[] args) throws IOException, SQLException {
fetchData("http://...", "A");
}
private static void fetchData(String url, String timePoint) throws IOException, SQLException {
String content = getHttpContent(url); // 网页内容
Date dataDate = getDataDate(content); // 时间
List<MainBoard> boardList = getBoardList(content, dataDate); // 解析数据集
storeDB(boardList, dataDate, timePoint); // 写入数据库
}
而一些变量值也写死在程序中:
Connection dbConn = null;
dbConn = DriverManager.getConnection("jdbc:sqlserver://192.168.1.1:123;databaseName=AAAA", "12345", "12345");
Statement stmt = dbConn.createStatement();
用于获取时间的getBoardList()函数内部,通过正则表达式和遍历比较取出数据,返回相关的数据类。
storeDB函数负责写入数据库:
String sql = "delete from TABLE where dataDate='" + simpleDateFormat.format(date) + "' and timePoint='" + timePoint + "'";
stmt.execute(sql);
for (MainBoard board : boardList) {
String ss = "insert into TABLE values" + "('" + board.getCode() + "'," +
"'" + board.getStockName() + "'," +
board.getSH() + "," +
board.getDollar() + "," +
"'" + timePoint + "'," +
"'" + simpleDateFormat.format(board.getDataDate()) + "'," +
"'" + timeFormat.format(board.getUpdateTime()) + "')";
stmt.executeUpdate(ss);
}
最初的这个结构基本上可以看成是纯过程化,且没有根据功能放进不同的类文件。如果后续需求需要抓取更多的不同类型的网页,则代码会臃肿、混乱。
每有一个新的格式,上述的getDataDate、getBoardList、storeDB和MainBoard都需要更换,并且会产生空数据类。
2. 重构,抽象:
重复的多了之后,就有了提高代码架构的需求。先补充了理论知识,《重构》和《Head First》结合着看。
在《重构》中看到这么一段描述:
将过程化设计转向对象设计:
1.针对每一个记录类型,转变为只含访问函数的哑数据对象。
2.针对每一处过程化风格,将该处代码提炼到一个独立类中。
3.针对每一段长长的程序,将它分解,再将分解后的函数分别移到它所相关的哑数据类中。
4.重复上述步骤。
原来对象设计是以数据对象为基础,再把与此数据相关的操作或代码提炼成函数,放入此数据对象中。
以前编写程序时,是以过程化为主。
所以思维上的第一个变化是:
把程序抽象的看成是一个数据加工厂,加工厂由许多个模块/部门组合而成。数据看成是一个流,流过程序这个加工厂,被不同部门处理,被转换,但是最终都会有一个存储和展示形式(也就是载体)。
从更高的层次观察数据的处理是优化结构的重要方式,把相同的步骤/动作抽象出来,把具体的实现细节留给不同的类。
所以,程序的运行可以分为 数据流 和 对数据流的处理。每次有需求变更时,因为模块的接口是定义好的,修改不同模块的实现即可。
隐约有了抽象的思想后,再次结合程序重新思考代码的组织方式。
观察到哑数据对象,也就是只含有数据变量和相应取值/设值方法的类。结合实践发现把和此数据类相关的操作放入哑数据类中,确实是比较好的组织方式。
比如,上面的storeDB函数接收哑数据Data类作为参数,构造数据库语句。那么这里就可以把这个函数放入Data类中:
class Data {
// ...
public String generateInsertSql() {
String ss = "insert into TABLE values" + "('" + board.getCode() + "'," +
"'" + board.getStockName() + "'," +
board.getSH() + "," +
board.getDollar() + "," +
"'" + timePoint + "'," +
"'" + simpleDateFormat.format(board.getDataDate()) + "'," +
"'" + timeFormat.format(board.getUpdateTime()) + "')";
return ss;
}
}
思考为什么这个组织方式好于以前的形式?原因之一就是这样更符合人的思维方式。
再以这样的方式思考组织程序,继而思维产生了第二个变化:
把类看成以数据为中心,附属着对这个数据的各种操作作为函数。而程序就可以看成是各个附带着行为的数据之间的交互。这样以前总结出的数据流就分化为一个个的个体,对数据的加工操作附属于不同的个体。
感觉似乎摸到了面向对象的门道,决定再去知乎上看看大家的讨论,发现https://www.zhihu.com/question/19701980这篇蛮具有启发意义的。
进而更进一步认识了面向对象:
一个操作或一件事由谁来完成,强调的是“谁”。由此,程序变化为一群“活物”之间的交互。
“它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性”。根本目的是提高软件的重用性、灵活性和扩展性。
回到程序实践,从过程化结构中抽象出三个主体:UrlConstructor,HttpService,SqlConstructor。分别代表产生url字符串、读取网页内容、构造sql语句。UrlConstructor还有一个功能是从网页内容里解析提取目标数据。
这时流程变为,UrlConstructor构造出url,然后HttpService接收url并取得网页数据,交由UrlConstructor解析处理,并由SqlConstructor产生sql语句,最后又DB对象写入数据库。
但是这时又产生了一个新的困惑:虽然结构上比以前抽象的一些,但是感觉依然需要一些结构化的语句来处理对象间的交互。如何消除这部分影响?继续学习,发现一个讨论http://bbs.csdn.net/topics/40441744算是解释了心中的疑惑。
思维再次发生了变化:
面向对象是一种思维,和语言无关。不是写了顺序执行的代码就是面向过程,面向对象强调的是以什么样的思维来组织程序。
用c也可以写出面向对象的程序,而组织的不好,用Java写出来的也会是面向过程的程序。所以,如果组织的好,有顺序执行代码也是面向对象的。
比较常见的例子就是全局变量,在函数中使用了全局变量也就破坏了类的封装性,就不是面向对象编程了。好的做法是,全局变量都作为参数传入成员函数中,实现封装。这么做也可以方便单元测试,和提高清晰度。
ps. 对于工具类是否使用静态函数:如果不涉及到类变量或者不使用类变量做信息存储,可以使用静态函数,但是不要使用全局变量;另外需要考虑多线程冲突问题。
上述思维也很好的解释了面向对象的三大要素:封装,继承,多态。封装即把数据和操作当成一个整体,对外只暴露接口。继承和多态是的程序可以方便扩展,调用者无需关注实现细节,进而灵活应对需求变更。这方面的讨论可以看下https://www.zhihu.com/question/20275578里面尤其是“invalid s”的回答。
至此,终于理解了依赖倒置,依赖注入还有控制反转等一些以前没有领悟的概念。见https://www.zhihu.com/question/31021366核心思想即是面向接口编程。
在理清了面向对象思想之后,才算是可以初窥设计模式的门径。设计模式的目的是解耦,提高使用率。如装饰器模式、工厂模式、观察者模式等,从面向对象的角度去理解会非常快速。设计模式有几个原则:1.面向接口编程;2.对扩展开放,对修改封闭;3.组合大于继承。使用设计模式往往会遇到一些问题需要权衡各方面做决定。
但是反过来说,并不是面向对象编程就一定要往设计模式上面靠。“设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结”,设计模式一般都有一个适用场景,超出这个范围,也不见得还有效。
设计模式之上就是框架的设计,这个暂时不做深究。
接着再说说重构,重构我认为也可以算是一种思维,需要固化在脑中。并不是面向对象编程才需要重构,所有方式的编程都需要重构,某种意义上编程==重构。
重构我认为有几个关键思想:1.重复代码移动到统一的地方;2.如果需要修改,目标是只修改某一个地方;3.一个变化只影响一个类;4.一个类只受一个变化的影响。等等~~
因为“代码首先是为人写的,其次才是为计算机写的”。
最重要的是努力实践,现在我对面向对象思维的理解也才刚开始,以后肯定会回过头反复思考再实践。希望能越来越熟练,高效。
重构的另一个关键地方是测试,包括有单元测试和集成测试。单元测试以函数为目标,重点测试函数的行为是否符合预期,这也侧面反映了封装的好处,封装起来后,单元测试就不用考虑全局变量的因素;集成测试以整个系统为目标,测试系统在接收到可能的输入时,产生的输出和行为是否达到目标要求。
以前对单元测试不是很重视,独立开发了程序之后,发现单元测试是和正常程序功能同等重要的,必须要做到测试覆盖大部分功能。单元测试的原则是:一个功能一个测试,一个bug一个测试。可以说编写完功能函数只是完成了一半的任务,完成测试功能后才能算真正完成了主要任务。集成测试的编写相对复杂一些,因为需要调用完整的系统,但是一旦完成后,会节省很多系统的测试流程。
回到重构,重构最好是一小步一小步的修改,每次修改后,同步修改单元测试并进行测试。这样可以大大的减少犯错的可能性。
注释也是程序的重要部分,好的注释可以大大提高理解效率,而函数或者变量名可以承担一部分解释的功能。在注释的时候应该从结构上说明,如“用快速排序算法实现了对象列表的排序”,而不是对每一句代码说明做了什么。因为了解快速排序的人可以快速看懂代码的作用,而反过来通过每一句的注释推出快速排序就比较费事。
最后借着这次重构,记录下关于java正则性能的感想。
因为写正则表达式一般都会使用到通配符如:
<td .*><a.*>(.*)</a></td>
而正则并不意味着查找效率就高。通配符可能会导致匹配时间增加,所以一些简单的表达式使用诸如indexOf()这类的函数自己实现的话性能可能会提高一些。从测试结果看,简单表达式自己时间会提高一小部分性能,但是对比读取网页的时间微乎其微。所以如何使用正则?是自己实现还是用复杂的表达式,需要先进行测试,再做决定。