最实用的10个重构小技巧排行榜,您都用过哪些呢?
发布时间:2013-09-09 23:52:00作者:左潇龙阅读(4107 )评论(31)
LZ最近一直在研究虚拟机源码,可惜目前还只是稍微有一点点头绪,无法与各位分享,庞大的JAVA虚拟机源码果然不是一朝一夕能搞定的,LZ汗颜。
本次我们抛开JAVA虚拟机源码这些相对底层的东西,LZ来与各位探讨一下几个代码重构的小技巧,这些内容部分来自于书籍当中,部分来自于LZ维护项目当中的一些实践经验。如果猿友们曾经用过这种手法,也不妨参与到文章的留言当中,将你的小心得、小体会共享与他人,也可以拿来冲击LZ自己定义的排行榜,LZ不甚欢迎。
重构的手法有很多种,相对而言,一篇文章的涵盖量自然是无法提到所有,LZ这里也只能提出一些平时会经常使用的一些手法,像一些比较高端的手法,各位有兴趣的可以去找一些专门的书籍涉猎。
另外还有一点,由于LZ是做JAVA开发的,因此部分重构小技巧可能与JAVA语言,或者说与面向对象的语言息息相关,不过大多数技巧,无论是面向过程的语言,还是面向对象的语言,都是可以相互通用的。
废话不多说,我们来看看实用重构技巧的排行榜吧。
No.1:重复代码的提炼
重复代码是重构收效最大的手法之一,进行这项重构的原因不需要多说。它有很多很明显的好处,比如总代码量大大减少,维护方便,代码条理更加清晰易读。
它的重点就在于寻找代码当中完成某项子功能的重复代码,找到以后请毫不犹豫将它移动到合适的方法当中,并存放在合适的类当中。
小实例
class BadExample {
public void someMethod1(){
//code
System.out.println("重复代码");/* 重复代码块 */
//code
}
public void someMethod2(){
//code
System.out.println("重复代码");/* 重复代码块 */
//code
}
}
/* ---------------------分割线---------------------- */
class GoodExample {
public void someMethod1(){
//code
someMethod3();
//code
}
public void someMethod2(){
//code
someMethod3();
//code
}
public void someMethod3(){
System.out.println("重复代码");/* 重复代码块 */
}
}
No.2:冗长方法的分割
有关冗长方法的分割,其实有时候与重复代码的提炼是有着不可分割的关系的,往往在我们提炼重复代码的过程中,就不知不觉的完成了对某一个超长方法的分割。倘若在你提炼了大部分的重复代码之后,某一些冗长方法依然留存,此时就要静下心来专门处理这些冗长方法了。
这其中有一点是值得注意的,由于我们在分割一个大方法时,大部分都是针对其中的一些子功能分割,因此我们需要给每一个子功能起一个恰到好处的方法名,这很重要。可以说,能否给方法起一个好名字,有时候能体现出一个程序猿的大致水准。
小实例
class BadExample {
public void someMethod(){
//function[1]
//function[2]
//function[3]
}
}
/* ---------------------分割线---------------------- */
class GoodExample {
public void someMethod(){
function1();
function2();
function3();
}
private void function1(){
//function[1]
}
private void function2(){
//function[2]
}
private void function3(){
//function[3]
}
}
No.3:嵌套条件分支的优化(1)
大量的嵌套条件分支是很容易让人望而却步的代码,我们应该极力避免这种代码的出现。尽管结构化原则一直在说一个函数只能有一个出口,但是在这么大量的嵌套条件分支下,让我们忘了这所谓的规则吧。
有一个专业名词叫卫语句,可以治疗这种恐怖的嵌套条件语句。它的核心思想是,将不满足某些条件的情况放在方法前面,并及时跳出方法,以免对后面的判断造成影响。经过这项手术的代码看起来会非常的清晰,下面LZ就给各位举一个经典的例子,各位可以自行评判一下这两种方式,哪个让你看起来更清晰一点。
小实例
class BadExample {
public void someMethod(Object A,Object B){
if (A != null) {
if (B != null) {
//code[1]
}else {
//code[3]
}
}else {
//code[2]
}
}
}
/* ---------------------分割线---------------------- */
class GoodExample {
public void someMethod(Object A,Object B){
if (A == null) {
//code[2]
return;
}
if (B == null) {
//code[3]
return;
}
//code[1]
}
}
No.4:嵌套条件分支的优化(2)
此处所说的嵌套条件分支与上面的有些许不同,它无法使用卫语句进行优化,而应该是将条件分支合并,以此来达到代码清晰的目的。由这两条也可以看出,嵌套条件分支在编码当中应当尽量避免,它会大大降低代码的可读性。
下面请尚且不明觉厉的猿友看下面这个典型的小例子。
小实例
class BadExample {
public void someMethod(Object A,Object B){
if (A != null) {
if (B != null) {
//code
}
}
}
}
/* ---------------------分割线---------------------- */
class GoodExample {
public void someMethod(Object A,Object B){
if (A != null && B != null) {
//code
}
}
}
No.5:去掉一次性的临时变量
生活当中我们都经常用一次性筷子,这无疑是对树木的摧残。然而在程序当中,一次性的临时变量不仅是对性能上小小的摧残,更是对代码可读性的亵渎。因此我们有必要对一些一次性的临时变量进行手术。
小实例
class BadExample {
private int i;
public int someMethod(){
int temp = getVariable();
return temp * 100;
}
public int getVariable(){
return i;
}
}
/* ---------------------分割线---------------------- */
class GoodExample {
private int i;
public int someMethod(){
return getVariable() * 100;
}
public int getVariable(){
return i;
}
}
No.6:消除过长参数列表
对于一些传递了大批参数的方法,对于追求代码整洁的程序猿来说,是无法接受的。我们可以尝试将这些参数封装成一个对象传递给方法,从而去除过长的参数列表。大部分情况下,当你尝试寻找这样一个对象的时候,它往往已经存在了,因此绝大多数情况下,我们并不需要做多余的工作。
小实例
class BadExample {
public void someMethod(int i,int j,int k,int l,int m,int n){
//code
}
}
/* ---------------------分割线---------------------- */
class GoodExample {
public void someMethod(Data data){
//code
}
}
class Data{
private int i;
private int j;
private int k;
private int l;
private int m;
private int n;
//getter&&setter
}
No.7:提取类或继承体系中的常量
这项重构的目的是为了消除一些魔数或者是字符串常量等等,魔数所带来的弊端自不用说,它会让人对程序的意图产生迷惑。而对于字符串等类型的常量的消除,更多的好处在于维护时的方便。因为我们只需要修改一个常量,就可以完成对程序中所有使用该常量的代码的修改。
顺便提一句,与此类情况类似并且最常见的,就是Action基类中,对于INPUT、LIST、SUCCESS等这些常量的提取。
小实例
class BadExample {
public void someMethod1(){
send("您的操作已成功!");
}
public void someMethod2(){
send("您的操作已成功!");
}
public void someMethod3(){
send("您的操作已成功!");
}
private void send(String message){
//code
}
}
/* ---------------------分割线---------------------- */
class GoodExample {
protected static final String SUCCESS_MESSAGE = "您的操作已成功!";
public void someMethod1(){
send(SUCCESS_MESSAGE);
}
public void someMethod2(){
send(SUCCESS_MESSAGE);
}
public void someMethod3(){
send(SUCCESS_MESSAGE);
}
private void send(String message){
//code
}
}
No.8:让类提供应该提供的方法
很多时候,我们经常会操作一个类的大部分属性,从而得到一个最终我们想要的结果。这种时候,我们应该让这个类做它该做的事情,而不应该让我们替它做。而且大部分时候,这个过程最终会成为重复代码的根源。
小实例
class BadExample {
public int someMethod(Data data){
int i = data.getI();
int j = data.getJ();
int k = data.getK();
return i * j * k;
}
public static class Data{
private int i;
private int j;
private int k;
public Data(int i, int j, int k) {
super();
this.i = i;
this.j = j;
this.k = k;
}
public int getI() {
return i;
}
public int getJ() {
return j;
}
public int getK() {
return k;
}
}
}
/* ---------------------分割线---------------------- */
class GoodExample {
public int someMethod(Data data){
return data.getResult();
}
public static class Data{
private int i;
private int j;
private int k;
public Data(int i, int j, int k) {
super();
this.i = i;
this.j = j;
this.k = k;
}
public int getI() {
return i;
}
public int getJ() {
return j;
}
public int getK() {
return k;
}
public int getResult(){
return i * j * k;
}
}
}
No.9:拆分冗长的类
这项技巧其实也是属于非常实用的一个技巧,只不过由于它的难度相对较高,因此被LZ排在了后面。针对这个技巧,LZ很难像上面的技巧一样,给出一个即简单又很容易说明问题的小例子,因为它已经不仅仅是小手段了。
大部分时候,我们拆分一个类的关注点应该主要集中在类的属性上面。拆分出来的两批属性应该在逻辑上是可以分离的,并且在代码当中,这两批属性的使用也都分别集中于某一些方法当中。如果实在有一些属性同时存在于拆分后的两批方法内部,那么可以通过参数传递的方式解决这种依赖。
类的拆分是一个相对较大的工程,毕竟一个大类往往在程序中已经被很多类所使用着,因此这项重构的难度相当之大,一定要谨慎,并做好足够的测试。
No.10:提取继承体系中重复的属性与方法到父类
这项技巧大部分时候需要足够的判断力,很多时候,这其实是在向模板方法模式迈进的过程。它的实例LZ这里无法给出,原因是因为它的小实例会毫无意义,无非就是子类有一样的属性或者方法,然后删除子类的重复属性或方法放到父类当中。
往往这一类重构都不会是小工程,因此这一项重构与第九种类似,都需要足够的谨慎与测试。而且需要在你足够确认,这些提取到父类中的属性或方法,应该是子类的共性的时候,才可以使用这项技巧。
结束语
由于LZ目前的工作就是维护一个相对古老的项目,因此上面这十种手法,LZ几乎都已经一一尝试过了,可喜的是效果都还不错。
限于最后两种与实际情况的联系太过紧密,因此LZ无法给出简单的实例,不过后面两种毕竟不是常用的重构手法,因此也算是可以接受了。不过不常用不代表不重要,各位猿友还是要知道这一点的。另外LZ还要说的是,上面的实例只是手法的一种简单展示,实际应用当中,代码的结构可能是千奇百怪,但却万变不离其宗。因此只要抓住每种手法的核心,就不难从这些乱军丛中安然穿过。
好了,本次的小分享到此结束,希望各位猿友如果觉得有所收获,可以推荐一下鼓励下LZ,顺便也让更多的人看到。这样的话,或许我们每一个接手的项目代码,都不至于十分的糟糕了,也算是给像LZ这样的项目维护者一条生路吧。
版权声明:本文版权归作者(左潇龙)所有,欢迎转载。但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
|
|
|
|
|
|
|
|
分类: 设计模式
非常感谢阁下的建议。
LZ这里也说一下自己对重构的理解。
重构最主要看重的是程序的可读性,以及维护起来比较方便。如果牺牲那多了一层堆栈的小小性能,能大大提高程序的可读性,这实在是非常值得的一件事。其实虚拟机内部对这种代码是有性能优化的,使用的就是方法内联,因此性能的损失几乎可以忽略不计。
至于这些分离出来的子功能能否被复用,LZ个人觉得这个问题不需要考虑它,能复用是最好的,但也并非是每一段代码都非得复用才能放到一个方法当中。将一段具有某种子功能的代码放到一个方法当中,并给方法取一个好名字,能让程序看起来很清晰。
比如一个对象的构建过程,假设我们本来是一大堆代码。现在是下面这样,你喜欢哪种呢?
public void build(){
init();
setProperties();
checkProperties();
//等等过程吧,LZ随意杜撰的
}
而且当你的程序结构优化之后,如果性能遇到了瓶颈,那么优化起来也会非常简单。
以上算是LZ的小感悟吧。
感谢阁下的建议。
LZ这里也浅谈一下临时变量的相关重构吧。
首先既然考虑重构,就得先排除一些必须的临时变量,这些临时变量不在考虑范围内。
那么对于一些可以删除或者说可有可无的临时变量,尤其是一次性使用的临时变量,它不仅会小小的降低程序的性能(不过这点其实倒无所谓,因为JAVA虚拟机也有相关的优化,也就是去除一些不必要的临时变量的声明与赋值),最主要的是它会大大增加代码的长度。
举个小例子。LZ现在所维护的代码经常看到这样的东西。
public void save(){
String id = request.getParameter("id");
String name = request.getParameter("name");
String type = request.getParameter("type");
String number = request.getParameter("number");
String productType = request.getParameter("productType");
String productName = request.getParameter("productName");
//等等一系列的临时变量,名字纯属LZ杜撰。
//然后下面使用的时候,这些变量统统只使用了一次
}
像这种临时变量,删除掉它们会大大增加代码的可读性,减少代码量。
还有一点是删除临时变量的重要原因,那就是为了配合其它的重构手法,在删除临时变量以后,会很容易采用其它手段进行进一步优化。
由于这些例子写起来可能篇幅较长,LZ这里就省略了。如果猿友们有兴趣,LZ后面会考虑是否要再开一篇博文介绍一下这些内容。
当然,临时变量的删除相对于其它的一些重构手法来讲,可能看起来并不是特别的紧迫,不过LZ还是建议如果有一次性的临时变量,尽量删除它们吧。
不过阁下的意思LZ的理解应该是这样的,不知理解的对不。应该是类似于我们想计算一个结果,但是这个结果需要好多步骤才能得到。那么每一个步骤我们就增加一个临时变量。类似下面这种。
public void cal(){
//这是注释,说明result1是怎么得到的或者是代表什么意思。
int result1 = //一些计算过程
//类似的注释,去说明result2的情况
int result2 = //一些计算过程,需要使用result1,也就是出现了result1只使用一次的情况
//以下一系列步骤,最终返回一个结果。
}
像这种情况,属于从无到有,也就是说增加临时变量的重构手法,只不过阁下技术高深,一开始就这么做了。
对于增加临时变量的重构手法,这里没有涉及,说到这里,LZ还是只能说,
如果猿友们有兴趣,LZ后面会考虑是否要再开一篇博文介绍一下这些内容。。。。
还有一点LZ要说的是,很多手法都是相斥的,比如我们有时候需要拆分一个方法,有时候也需要内联一个方法,有时候需要去掉临时变量,有时候需要增加临时变量。
但是无论如何,终于的目的就是为了程序看起来“好看”和“好维护”,只要符合这点,任何手法都是可取的。
说的多了,阁下见谅啊。
重构的目标,不外乎是增加代码的可维护性和提高程序效率。
现在很多编译器都对代码提供了优化
比如,a = 1 + 2 + 4;
上述代码,在Java这些语言中,编出的中间代码已经是a = 7;
已经自动优化过了(所以,翻译后的代码就是a = 7;而不是a = 1 + 2 + 4;)
所以,在小效率面前,可读性可维护性可以稍微多加关注。
就比如,Java的反射,其实很早就出现了,但是,刚开始的考虑到效率问题,就比较少有人用,但是,随着计算机的性能提高,它在效率上的消耗,完全比不上在它在可维护性上的提供的帮助,就比如说spring框架的流行。
为了性能上的优化,我了解的很少,我不是很了解
但是,我认为,一般的性能上的优化,会考虑几个地方
1,使用更效率的技术。
2,分析代码(尤其是注意哪些大量重复执行的代码是最容易提高性能的地方),然后查看可以优化的地方并修改优化,比如变量,对象等,可以优化的地方。
3,改变功能的实现方式,不惜增加辅助变量等方式,较少时间消耗。
LZ总结的这些方式都很不错,是工作中思考过的表现,小弟很是佩服工作善于思考的人。
其实阁下所说的内容已经超出了本文的范围,也就是性能。不过LZ这里斗胆反驳阁下一句话,重构的目标有提高可维护性没错,但是程序的效率并不是重构过程当中需要考虑的,或者说是可以次要考虑的。
重构的目标说的官方一点,主要是为了系统结构的优良,代码的可读性、可维护性以及扩展性,通俗点讲,就是为了开发这个系统的人走之后,维护的人员维护起来会省很多事,不论是改东西还是添加东西,从而减少维护成本和增加扩展的可能性。
其实在考虑重构的时候,是与效率或者说性能是无关的,这是两个话题了。
引用重构这本书中的思想,我们对项目的完成应该分三个阶段,先谈功能的正确实现,再谈代码的结构优良,最后再谈性能的问题。当然这不是说第一个阶段就只管功能,结构和性能都一点不管了,这里只是主次关系而已,并不是相斥的关系,管这个就完全不管另外两个了。
换句话说,第一阶段代码的结构与性能是次要的,功能是主要的。而第二个阶段要在保证功能正确的前提下,完成对结构的改善,性能是次要的。最后一个阶段,是在保证功能正确且尽量不破坏结构的前提下,对性能进行提高。
而重构则是这第二个阶段,在这个阶段中,性能并不是我们考虑的因素。而且重构也并不是性能的拖累,而是性能优化的前提。否则面对一堆散乱分布的代码,何谈性能优化?恐怕最终可能是性能没优化成,bug倒是被带出来不少。
总的来说,重构(或者说结构优化)与性能优化是两码事,二者不可同时进行,应该是分阶段进行的。
阁下谦虚的口气令LZ好感倍增啊,果然是越高深的人其实会越谦虚。
我和你理解的重构有很大的不一样。
1. “重构是指对已经已经实现过的业务逻辑进行扩展设计”
如果要对业务逻辑进行扩展设计,请发需求,请发工单。这已经不是重构了。
2. “主要目的是为了提高生产效率”
主要是为了可维护性。
2. “重构工作只会增加新的代码”
重构一个重大的特征几乎就是减少了代码(当然也可能多了很多被封装的中间代码,从而让业务代码看起来减少了),何来“只会增加新的代码”?
3. “而不修改以前的项目”
重构就是修改以前的项目。
4. “程序员的职责是以最快的速度通过测试,他们的绩效也是以此为标准考核”
关于这一点请加上“我们公司的”前缀。
5. “对于已经通过测试的代码不要修改了,员工没有义务免费工作”
如果你的工作就是改以前的代码呢(能通过测试,但需要重构)?
5. “而让能写代码的程序员干这种事情更是极大地浪费人力资源”
重构是技术活,一般头儿只会让技术好的人去做这种事。
你说的2.这一点也是分歧的关键,"为了可维护性"去修改以前的代码,
这一点完全站不住脚,自相矛盾,你们修改代码是为了更好的修改代码?
通过测试的代码为什么要改?给我个理由先
还有,一切修改代码的行为叫做:重写,是低级劳动,绝不是技术活,
企业最高级的设计师的主要职责是重构,而不是重写,
由于不断的重构,生产一线的程序员才不会每天重复实现那些已经实现过的业务
站内搜索
用户中心
用户名: | 登录 | 注册 | |
密 码: | ||
用户名支持字母,数字,下划线和中文 |
最新评论
随机推荐
JVM内存管理------杂...
左潇龙2013-08-17
浅谈程序猿简历的写法...
左潇龙2013-10-16
一次sql性能的简单优...
左潇龙2015-06-08
(八)适配器模式详解
左潇龙2013-08-16
设计模式详解(总纲)
左潇龙2013-08-16