代码味道:大类和长方法 (1分)

  • 主题发起人 主题发起人 曹晓钢
  • 开始时间 开始时间

曹晓钢

Unregistered / Unconfirmed
GUEST, unregistred user!
(石一楹)
前次,我讲到代码重复是OO系统的一个硬伤,人人必将除之而后快.
但到底什么样子算是代码重复?代码重复的原因是什么?代码重复会令系统呈现何种面貌?如何
才能消除重复代码?诸如此类的问题,还需要深入地探讨.否则,就算我们能够隐约辨识出这种
异味,也不知道从何入手,如何入手,到什么样的程度才停止进一步的锤炼.
大类和长方法是产生代码重复的一个极其重要的原因,同时也是重复代码产生的必然后果.
我在这篇文章中试着来探究其中的一些缘由.
One reposibility?
在面向对象以前的结构化编程语言中,由于缺乏OO语言大多数的重用机制,最重要的重用方
法就是函数库.为了方便客户代码使用,这些函数库的基本组成部分—函数--通常需要处理
属于同一类型的各种不同变种.这样,客户代码可以使用不同的参数来调用这些函数实现不
同的机能,而在实现这些函数库时,你必须根据不同的参数值来选择具体行为.
这样做的结果是这些函数往往具有”强大的功能”,同时,它使得这些函数库能够被各种各
样的不同应用程序重用.正因为如此,在很多人看来,这甚至是编写程序的极高境界.
然而,这些”强大的功能”不是免费得来的,它需要我们付出更高的代价.当你需要对这个
能够处理各种各样不同情况的函数进行修改,譬如,需要对其中的一种情况处理加以变化
的时候,可怕的事情发生了.你不得不一头扎进这个充满着switch,if,flag和exit的大泥
潭,来分辨哪些代码用来处理你所针对的这种情况,哪些又是处理其他情况,或者某些代
码和很多情况有关.即使你能够找到这样一个个分离的代码块,对这些代码块的修改完
全可能会影响其他你并不关心的情况.当你自认为完成了修改,你测试了你意图修改的
代码,你以为万事大吉.但是很有可能,你已经让原先正确的其他情况发生了错误.再退
一步,就算你的代码没错,如果那种情况的修改需要你增加特别的参数,那么原先所有
引用该函数的代码度需要重新修改,编译,测试,所有这些行为,都可能以级联的方式
迅速扩张到整个系统,最后,所有的代码都需要重新编译,测试.
这种多职责的方法引起的修改,维护和测试代价被证明是软件昂贵甚至失败的罪魁祸
首.Bertrend Meyer在著名的《Object-Oriented Software Construction》一书中提
出了面向对象系统至今为止最广为人知的Open-Closed Principle。他说:
Modules should be both open and closed。
Meyer用Module来指称类、方法、包等等系统元素。他指出,一个模块,一旦被交付,
就必须Closed,也就是不能被修改。同时,一个模块也必须是open的,它能够被扩展。
两者看起来矛盾,然而确是OO方法增量开发的本质特点。
如上所述,一个具有很多责任的类或者方法必然是难以close的,它违反了OCP原则的
第二部分。
How to OAOO and Why?
认识到过程型思想方法的局限性是基础,我们还需要更往前走一步。 在《代码重复》
中我已经讲到,Once And Only Once(OAOO)是排除重复的最终目标。但是要做到这一
点并非如我们想象的那么简单,Ralph Johnson说:
I've read this pagedo
zens of times in the past. Reading it again today made
my heart sink. It is hard to explain. Once And Only Once is a profound concept,
but difficult to apply. I've spent my entire professional life (25 years)
learning how to apply it to programs. This page makes it seem so sterile,
so dead. Since the last time I read it, it was rewritten to make
OnceAndOnlyOnce seem like a simple rule to apply, instead of a prime principle.
Once And Only Once is NOT easy! And it was wrong to refactor this page so
that all hints of tension and disagreement are removed from it.
大类常常难以理解、维护和重用。要达到OAOO的境界,就需要不断的排除重复,排除重
复的结果,系统最终将有许多职责明确的小段代码块组成,因为好的系统需要:
l 可替换的对象。好的编程风格产生易替换的对象。在一个理想的系统中,每次当用户
想完成一件不同的事情,开发人员应当能够方便地实现一个对象(常常通过继承),然
后把它直接插入(plug)到已经存在的系统中。正如OCP告诉我们,你通过面向对象的重
用机制用新的对象替换老的对象来改变系统行为,而不修改原来的代码。要实现这样的目
标,你必须有很小但又职责明确的类和方法。
l 适应不同的场景。好系统的重要特点之一就是他们的对象能够方便地移植到一个不同
的上下文中工作。当你第一次把对象移到一个与你原始开发不同的上下文时,你完全可
能会失败。于是,你把相同的抽象和不同的抽象分开,原来看起来原子的职责分为几个
更小的职责,每次你移植到一个不同的环境,你的代码抽象级别越小,但是适应能力越
强,从而就越可以重用,越能够适应现在尚未预料的场景。
l 隔离不同的变化率:不同的状态,即使处于同一个类内部也会有不同的变化率。面向
对象的可重用性就是通过隔离相对变化较慢和相对较快的部分而得来的。这通常也是我
们经常所说的“抽象“包含的意义。
How to achieve understanding?
适应变化的能力非常重要,同样重要还有程序的可理解性。你如何阅读自己的代码,即
使写在几个月甚至是数年之前?你如何理解别人的代码,从而与别人交流思想?你如何
记录你的设计,保证它们不会随着程序的evolution而过时?
计算机程序的读者不仅仅是芯片,还有很多和你一样的程序员,包括你自己。程序员最
讨厌的事情之一是翻遍所有的API文档,最后发现还是对系统的理解和使用没有帮助。
程序员也讨厌些大量的注释,因为程序代码的修改要求同时修改注释,而结果就是注释
往往和你的程序不一致,看了比不看还差。程序员更讨厌的事情是要写一大堆详细设计
文档,可是结果发现这些设计文档到最后和程序实现的结果千差万别。
所有的这些都要求程序自身具有文档的功能,就象一部文学作品,尽管可以写出无数的
注释、分析、评论。但理解它最重要的依据还是它自己。任何其他的东西只会帮你理解
一部分,甚至把你引入歧途。而理解是一切的起源,没有理解,就没有一切。
要使你的程序能够忠实记录你的意图和设计,必须使类和方法,特别是方法短小精悍,
每一个方法使用一个Intent Revealing Name(揭示意图的名字),也就是要说出它做的
是什么而不是它如何完成一件事。当长方法经过这样的解析,最后将成为一段段自然的
英文,其中每一个词组(对象名+方法名+加上参数)告诉你在什么对象上用什么东西做
什么操作。在这个时候,注释已经成为多余,代码就是设计。
我经常使用JRefactory的统计工具来度量自己的代码。一般来说,整个系统平均每个方
法的代码行数位3-7行,平均每个方法的参数个数为1-2个左右。当然,这不是绝对的,
某些时候,很长的代码只包含一个意图,如大量从数据库字段读取对象的属性(不使用
O/R Mapping),那么这些代码的长度可能大大增加。衡量方法是不是太长的重要标准
就是它是否完成一件事情,你可以用一个动词短语或者断言把它说出来。
与大类和长方法相关的Refactoring
Martin Fowler《Refactoring》一书中对长方法的Refactoring也不少。其中最重要的
就是Extract Method(110).但我觉得Kent Beck的Composed Method更达意。使用
Extract Method,通常要涉及临时变量的问题,你可以使用Replace Temp with Query.
方法的参数太长,你可以使用Introduce Parameter Object和Preserver Whole Object。
临时变量实在太多的话Kent Beck的Method Object则很好。Extract条件和循环也非常
重用,通常你可能用Decompose Conditional来处理条件表达式。
大类的处理包括Extract Class, Extract Subclass,以及Extract Interface等等。
实例
下面的原始代码来自Duane A. Bailey的《Data Structure in Java for the
Principled Programmer》,我将展示如何进行Refactoring,从而让它符合我自己的审
美观,有好的味道、好的性能:
代码:
class Vector …
protected Object elementData[];
protected int elementCount;
protected int capacityIncrement;
public Vector(int initialCapacity, int capacityIncrement) {
    elementData = new Object[initialCapacity];
    elementCount = 0;
    this.capacityIncrement = capacitIncrement;
}
这是一个Vector类,Vector内部通过一个对象数组来实现,Vector有一个构建函数带2个
参数,第一个参数是初始的数组大小,第二个参数则是当加入新元素发现数组大小不足以
容纳时,需要递增的数组尺寸。
Duane A. Bailey的addElement实现如下:
代码:
public void addElement(Object obj) {
    ensureCapacity(elementCount + 1);
    elementData[elementCount] = obj;
    elementCount++;
}
这里ensureCapacity是为了保证数组大小足以容纳新加入的元素,它还可以被重用到
insertElementAt:
代码:
public void insertElementAt(Object obj, int index) {
    int i;
    ensureCapacity(elementCount + 1);
    for(i = elementCount;
i > index;
i--) {
        elementData[i] = elementData[i-1];
    }
    elementData[index] = obj;
    elementCount++;
}
这一段代码,你能够直接理解吗,我想不见得,所以,原文在for循环之前注释:
//N.B. must copy from right to left to avoid destroying data
这段代码需要注释的原因是循环语句让人一下子不知所云,实际上它做了两件事情,让
我们用extract method,并给它一个好的名字:
代码:
protected void moveEachToNext(int startIndex, int endIndex) {
    for(int i = endIndex+1;
i > startIndex;
i--) {
        elementData[i] = elementData[i-1];
    }
}
public void insertElementAt(int index,Object anObject) {
    ensureCapacity(elementCount + 1);
    moveEachToNext(index,elementCount-1);
    elementData[index] = obj;
    elementCount++;
}
我应当不需要做任何解释了。
上面的refacotering给你一点感觉,再让我们来看看Duane A. Bailey原来的
ensureCapacity:
代码:
public void ensureCapacity(int minCapacity) {
    if (elementData.length < minCapacity) {
        int newLength = elementData.length;
        if (capacityIncrement == 0) {
            if (newLength == 0) newLength = 1;
            while (newLength < minCapacity) {
                newLength *= 2;
            }
        } else
 {
            while(newLength < minCapacity) {
                newLength += capacityIncrement;
            }
        }
        Object newElementData[] = new Object[newLength];
        int i;
        for(i = 0;
i < elementCount;
i++) {
            newElementData[i] = elementData[i];
        }
        elementData = newElementData;
    }
}
}
这样的代码非常普遍,但是你不头痛吗,它在干什么?
让我们从头来:
if (elementData.length < minCapacity)
{
什么含义,它在判断原先长度的够不够,然后把一堆代码写进去,让我们使用截取这一段
条件表达式,然后使用guard Clauses:
代码:
protected void ensureCapacity(int expectedCapacity) {
    if (isLongEnough(expectedCapacity))
    return;
    。。。。
    。。。。
}
长度够不够呀?够的话,走人。我在smiling的UMLChina上讲了很多关于多出口和单出
口的问题,这里显然有一个return更好。
代码:
protected boolean isLongEnough(int expectedCapacity) {
    return elementData.length >=expectedCapacity;
}
往下看:
代码:
int newLength = elementData.length;
if (capacityIncrement == 0) {
    if (newLength == 0) newLength = 1;
    while (newLength < minCapacity) {
        newLength *= 2;
    }
} else
 {
    while(newLength < minCapacity) {
        newLength += capacityIncrement;
    }
}
这一段代码好像在计算如果长度不够的话,那么计算所需的新长度,我们变成如下:
代码:
protected void ensureCapacity(int expectedCapacity) {
    if (isLongEnough(expectedCapacity))
        return;
    int newLength = getNewLength(expectedCapacity);
    。。。。
    。。。
}
这里的问题是涉及到原先代码中的局部变量newLength,在原来的代码中,newLength被重
新赋值,所以我们把它作为返回值,原来那一大段代码现在被移入了:
代码:
protected int getNewLength(int expectedCapacity) {
    int newLength = elementData.length;
    if (capacityIncrement == 0) {
        if (newLength == 0) newLength = 1;
        while (newLength < minCapacity) {
            newLength *= 2;
        }
    } else
 {
        while(newLength < minCapacity) {
            newLength += capacityIncrement;
        }
    }
    return newLength;
}
这一段代码还是棘手的事情,其实在我们刚开始的构建函数中传入的capacityIncrement
具有双重的含义,当它为0的时候,表示每次不够的时候增加一倍的空间,不然就有用户
来指定所需的增长,我们知道采用每次倍增的策略,象add这样的操作时间复杂度为O(n)
,而每次只增加一个元素的时间复杂度则为O(n*n).
到这里,我们明白了capacityIncrement == 0这个0的含义,像这样的条件,你必定要给
它一个独立的方法。逐步进化的最后结果使得getNewLength变为如下(我省去了具体步
骤,在我的javablock有详细的源代码,用于测试Block):
代码:
protected int getNewLength(int expectedCapacity) {
    if (shouldDoubleSpace()) {
        returndo
ubleIt();
    } else
 {
    returndo
Increment(expectedCapacity);
}
}
再往下看:
代码:
Object newElementData[] = new Object[newLength];
int i;
for(i = 0;
i < elementCount;
i++) {
    newElementData[i] = elementData[i];
}
elementData = newElementData;
}
这段代码的含义是增大元素数组,我们最后把ensureCapacity变为:
代码:
protected void ensureCapacity(int expectedCapacity) {
    if (isLongEnough(expectedCapacity))
       return;
    int newLength = getNewLength(expectedCapacity);
    enlargeElementArrayTo(newLength);
}
而enlargeElementArrayTo(newLength)则为:
代码:
protected void enlargeElementArrayTo(int newLength) {
   Object newElementData[] = new Object[newLength];
   int i;
   for(i = 0;
i < elementCount;
i++) {
       newElementData[i] = elementData[i];
   }
   elementData = newElementData;
}
这里,我要讲到另外一个问题,很多人可能会觉得refactoring会降低系统的效率,确实
在细节看上是这样的。但是性能良好的程序只能从结构良好的程序优化而来,从来就不
会是一次写好的。细节性能是芝麻,总体性能是西瓜。要得到西瓜,首先就是让你的结
构更加容易让性能监测工具细致深入地度量。
在上面这段代码中,把数据从一个数组复制到另外一个数组是整个性能的关键,如果这
段代码埋藏在其他一大堆代码之间,你很难进行tune。但是一旦成为一个独立的方法,
那么我们可以很容易地对它进行benchmark,然后我们优化如下:
代码:
protected void enlargeElementArrayTo(int newLength) {
    Object[] newElementData = new Object[newLength];
    System.arraycopy(elementData,0,newElementData,0,elementData.length);
    elementData = newElementData;
}
参考:
Ralph Johnson’s some words about “Once And Only Once“
Kent Beck:Smalltalk Best Pratice Pattern
Martin Fowler:Refactoring
Bertrand Meyer: Object-Oriented Software Construction
Duane A. Bailey: Java Data Struture: Data Structures in Java for the Principled Programming
Shiyiying: JavaBlock 0.12 Source Code
Shiyiying: Duplicate Code
 
我KAO 你真有心!
 
to 曹晓钢大侠:
你能否教我用VC6.0 的IDE,我不熟悉VC的编译环境,[:(]
我给你300分,如何?[:D]
 
这个...你应该买几本入门的书,并且自己写一些程序才能熟悉。别人教,有口无心也是没有用的。[:D]
 
好好学习
 
思考中、、、
请继续!
 
坐下来精心学学!^_^
 
其实有些理论性的东西真应该好好看看了……
 
听君一席话,胜编10年程序
 
大侠就是大侠。
 
yysun:
I have to add "&amp;skin=2" after the URL everytime...[:(][:(][:(][:(]
 
曹大侠您在南京吗?我想毕业后到您那里工作,可以吗?
这是我的简历:http://ydjjld.myetang.com/resume.doc
 
能关注到这个层面,
才算是用JAVA用出了点味道。
 
劉勝利:
先累死你這個人渣!
你要是有道理,說出來大家聽聽!別在這裡亂吠!
 
楼上的你搞错了。我不知道什么华宇。
关于你的言论我会要求管理员严肃处理。
 
xalndy,你搞错了。我不知道什么华宇。
关于你的言论我会要求管理员严肃处理。
 
很抱歉,我今天在贵论坛上对“曹晓钢(南京华宇的)”说了很多不该说的话。
我对我的过激言论表示道歉!希望你们能谅解,能谅解一个刚刚毕业就被人在心口
狠狠的扎上一刀的人,见到如此对待自己的那个人的愤慨和痛苦!真的很对不起!请原谅!
 
后退
顶部