技术开发 频道

敏捷开发的必要技巧(三)

【IT168 技术文章】    异味这个词可能有点抽象,所以我们先看下面的例子。这是一个CAD系统。现在,它可以画三种形状:线条、长方形和圆。
    认真地看一下代码:

class Shape { final static int TYPELINE = 0; final static int TYPERECTANGLE = 1; final static int TYPECIRCLE = 2; int shapeType; //线条的开始点 //长方形左下角的点 //圆心 Point p1; //线条的结束点 //长方形右上角的点 //如果是圆,这个属性不用 Point p2; int radius; } class CADApp { void drawShapes(Graphics graphics, Shape shapes[]) { for (int i = 0; i < shapes.length; i++) { switch (shapes[i].getType()) { case Shape.TYPELINE: graphics.drawLine(shapes[i].getP1(), shapes[i].getP2()); break; case Shape.TYPERECTANGLE: //画四条边 graphics.drawLine(...); graphics.drawLine(...); graphics.drawLine(...); graphics.drawLine(...); break; case Shape.TYPECIRCLE: graphics.drawCircle(shapes[i].getP1(), shapes[i].getRadius()); break; } } } }

    代码都是一直在改变的,这也是上面的代码会碰到的一个问题。如果我们需要支持更多的形状(比如三角形),那么肯定要改动Shape这个类,CADApp里的drawShapes方法也要改动。
    可以修改如下:

class Shape { final static int TYPELINE = 0; final static int TYPERECTANGLE = 1; final static int TYPECIRCLE = 2; final static int TYPETRIANGLE = 3; int shapeType; Point p1; Point p2; //三角形的第三个点. Point p3; int radius; } class CADApp { void drawShapes(Graphics graphics, Shape shapes[]) { for (int i = 0; i < shapes.length; i++) { switch (shapes[i].getType()) { case Shape.TYPELINE: graphics.drawLine(shapes[i].getP1(), shapes[i].getP2()); break; case Shape.TYPERECTANGLE: //画四条边. graphics.drawLine(...); graphics.drawLine(...); graphics.drawLine(...); graphics.drawLine(...); break; case Shape.TYPECIRCLE: graphics.drawCircle(shapes[i].getP1(), shapes[i].getRadius()); break; case Shape.TYPETRIANGLE: graphics.drawLine(shapes[i].getP1(), shapes[i].getP2()); graphics.drawLine(shapes[i].getP2(), shapes[i].getP3()); graphics.drawLine(shapes[i].getP3(), shapes[i].getP1()); break; } } } }

    如果以后要支持更多的形状,这些类和方法又要改动,这可不是什么好事情。理想情况下,我们希望当一个类、一个方法或其他的代码设计完成后,就不用再做修改了。它们应该稳定到不用修改就可以重用。现在的情况恰好相反。每当我们增加新的形状,都得修改Shape类,以及CADApp里的drawShapes方法。
    怎么才能让代码稳定(也就是无需修改)呢?这是一个好问题。不过老规矩,先不说,我们以行动回答。我们先看另外一个方法,当给你一段代码,你怎么知道它是稳定的?

怎么判断代码的稳定性?

    要判断代码的稳定性,我们可能会这样来判定:先假设一些具体的情况或需求变动,然后看要满足这些新的需求代码是否需要被修改?可惜,这也是一件很麻烦的事。因为有那么多的可能性,我们怎么知道哪些可能性要考虑,哪些不用考虑呢?
    有个更简单的方法。如果发现,我们已经第三次修改这些代码,那么可以认定这些代码是不稳定的。这样的方法很“懒惰”,而且“被动”。因为这是在我们被伤到以后,才开始处理问题。虽然这种方法还算一个很有效的方法。
    此外,还有一个简单而且“主动”的方法:如果这段代码是不稳定或者有一些潜在问题的,那么代码往往会包含一些明显的痕迹。正如食物要腐坏之前,经常会发出一些异味一样(当然,食物如果有了异味了,再怎么处理我们都不想吃,但是代码可不行)。我们管这些痕迹叫做“代码异味”。并不是所有的代码异味都是坏事,但大多数情况下,它们确实是坏事情。因此,当我们感觉出有代码异味时,我们必须小心谨慎地检查了。
    现在,我们来看看示例例子中的代码异味。
    第一种异味:代码用了类别代码(type code)。

class Shape { final int TYPELINE = 0; final int TYPERECTANGLE = 1; final int TYPECIRCLE = 2; int shapeType; ... }

    这样的异味是一种严肃的警告:我们的代码可能有许多问题。
    第二种异味:Shape这个类有很多属性有时候是不用的。例如,radius这个属性只有在Shape是圆时才用到。

class Shape { ... Point p1; Point p2; int radius; //有时候不用 }

    第三种异味:我们想给p1、p2取个好一点的变量名都做不到,因为不同情况下它们有不同的含义。

class Shape { ... Point p1; //要取作“起始点”、“左下点”还是“圆心”? Point p2; }

    第四种异味:drawShapes这个方法里有一个switch表达式。当我们用到switch(或者一大串的if-then-else-if)时,要小心了。switch表达式经常是跟类别代码(type code)同时出现的。
    现在,让我们将这个示例中的代码异味消除吧。

消除代码异味:怎么去掉类别代码(type code)

    大多数情况下,要想去掉一个类别代码,我们会为每一种类别建立一个子类(当然,并不是每次去掉一个类别代码都要增加一个新类,下面的一个例子会讲到另一种解决方法)。比如:

class Shape { } class Line extends Shape { Point startPoint; Point endPoint; } class Rectangle extends Shape { Point lowerLeftCorner; Point upperRightCorner; } class Circle extends Shape { Point center; int radius; }

    因为现在没有了类别代码,drawShapes方法里面就要用instanceof来判断对象是哪一种形状。因此,我们不能用switch,而要改用if-then-else:

class CADApp { void drawShapes(Graphics graphics, Shape shapes[]) { for (int i = 0; i < shapes.length; i++) { if (shapes[i] instanceof Line) { Line line = (Line)shapes[i]; graphics.drawLine(line.getStartPoint(),line.getEndPoint()); } else if (shapes[i] instanceof Rectangle) { Rectangle rect = (Rectangle)shapes[i]; graphics.drawLine(...); graphics.drawLine(...); graphics.drawLine(...); graphics.drawLine(...); } else if (shapes[i] instanceof Circle) { Circle circle = (Circle)shapes[i]; graphics.drawCircle(circle.getCenter(), circle.getRadius()); } } } }

    因为没有了类别代码,现在每个类(Shape、Line、Rectangle、Circle)里的所有属性就可以保证任何情况都是必需的。现在可以给它们取一些好听的名字了,比如Line里面,p1属性可以改名为startPoint。
    现在四种异味只剩一种了,那就是在drawShapes里还有一大串if-then-else-if。下一步,就是要去掉这长长的一串。

消除代码异味:如何去掉长串if-then-else-if

    经常地,为了去掉if-then-else-if或者switch,我们需要先保证在每个条件分支下的代码是一样的。在drawShapes方法里,我们先以一个较抽象的方法(伪码)来写吧。

class CADApp { void drawShapes(Graphics graphics, Shape shapes[]) { for (int i = 0; i < shapes.length; i++) { if (shapes[i] instanceof Line) { 画线条; } else if (shapes[i] instanceof Rectangle) { 画长方形; } else if (shapes[i] instanceof Circle) { 画圆; } } } }

    条件分支下的代码还是不怎么一样,不如再抽象一点:

class CADApp { void drawShapes(Graphics graphics, Shape shapes[]) { for (int i = 0; i < shapes.length; i++) { if (shapes[i] instanceof Line) { 画出形状; } else if (shapes[i] instanceof Rectangle) { 画出形状; } else if (shapes[i] instanceof Circle) { 画出形状; } } } }

    好,现在三个分支下的代码都一样,我们也就不需要条件分支了:

class CADApp { void drawShapes(Graphics graphics, Shape shapes[]) { for (int i = 0; i < shapes.length; i++) { 画出形状; } } }

    最后,将“画出形状”这个伪码写成代码:

class CADApp { void drawShapes(Graphics graphics, Shape shapes[]) { for (int i = 0; i < shapes.length; i++) { shapes[i].draw(graphics); } } }

    当然,我们还需要在每种Shape的类里面提供draw这个方法:

abstract class Shape { abstract void draw(Graphics graphics); } class Line extends Shape { Point startPoint; Point endPoint; void draw(Graphics graphics) { graphics.drawLine(getStartPoint(), getEndPoint()); } } class Rectangle extends Shape { Point lowerLeftCorner; Point upperRightCorner; void draw(Graphics graphics) { graphics.drawLine(...); graphics.drawLine(...); graphics.drawLine(...); graphics.drawLine(...); } } class Circle extends Shape { Point center; int radius; void draw(Graphics graphics) { graphics.drawCircle(getCenter(), getRadius()); } }
0
相关文章