【读书笔记】代码不朽——编写可维护软件的十大要则

瞎写一时爽,维护火葬场

天天写尸堆,自己都看不下去了!什么时候开始自己的代码变得那么丑陋了呢?经过一番回想,我发现凡是没有约束的代码,变得丑陋只是时间问题。
综上所述,我拜读了Joost Visser的Building Maintainable Software这本书(中文译名为“代码不朽”),受益匪浅,在此做一下笔记以防遗忘。

代码不朽

——编写可维护软件的十大要则


①编写较小的代码单元

原则:

  • 代码单元的长度应该限制在15行代码以内。
  • 为此首先不要编写超过15行代码的单元,或者将长的单元分解成多个更短的单元,直到每个单元都不超过15行代码。
  • 该原则能提高可维护性的原因在于,短小的代码但愿易于理解,测试及重用。

如何应用本原则?

——使用重构技巧来应用原则

㈠重构技巧:提取方法

将内容代码较多的单元提取为几个代码较少的单元,此时代码总量将增加,但是我们总要在代码总量与后期可维护性之间做出权衡,不止这里,后面讲的方法也或多或少地增加了代码总量,但却能大大提高后期可维护性。
Example——吃豆人游戏中的一段代码:

public void start()
{
    if(inProgress){
        return;
    }
    inProgress=true;
    //如果玩家死亡则更新观察者
    if(!isAnyPlayerAlive()){
        for(LevelOvserver o:observers){
            o.levelLost();
        }
    }
    //如果所有的豆都被吃光则更新观察者
    if(remainingPellets()==0)
    {
        for(LevelObserver o:observers){
            o.levelWon();
        }
    }
}

原来有1行代码,现在我们提取方法,可将其分解成三个小方法:

public void start()
{
    if(inProgress){
        return;
    }
    inProgress=true;
    updateObservers();
}
private void updateObservers(){
    updateObserversPlayerDied();
    updateObserversPelletsEaten();
}
private void updateOberversPlayerDied(){
    if(!isAnyPlayerAlive()){
        for(LevelObserver o:observers){
            o.levelLost();
        }
    }
}
private void updateObserversPelletsEaten(){
    if(remainingPellets()==0){
        for(LevelObserver o:observers){
            o.levelWon();
        }
    }
}
㈡重构技巧:将方法替换为方法对象

在传入参数较少时使用提取方法较为方便,上面的例子就提取出了三个无参数的函数。但是若是要提取的方法调用了较多临时变量,提取方法时就要传入大量参数,造成代码冗余,对于这种情况可以尝试将方法替换为方法对象
Example:

public Board CreatBoard(Square[][] grid){
    assert grid!=null;
    Board board=new Board(grid);
    int width=board.getWidth();
    int height=board.getHeight();
    for(int x=0;x<width;x++){
        for(int y=0;y<height;y++){
            Square square=grid[x][y];
            for(Direction dir:Direction.velues()){
                int dirX=(width+x+dir.getDeltaX())%width;
                int dirY=(height+y+dir.getDeltaY())%height;
                Square neighbour=grid[dirX][dirY];
                square.link(neighbour,dir);
            }
        }
    }
}

若要提取上面循环中的方法,则需要传递七个参数,分别是width,height,x,y,dir,square,grid。拥有很长参数的方法显得丑陋无比,下面使用方法对象来将其替换之:

class BoardCreator{
    private Square[][] grid;
    private Board borad;
    private int width;
    private int height;
    BoardCreator(Square[][] grid){
        assert grid!=null;
        this.grid=grid;
        this.board=new Board(grid);
        this.width=board.getWidth();
        this.height=board.getHeight();
    }
    Board create(){
        for(int x=0;x<width;x++){
            for(int y=0;y<height;y++){
                Square square =grid[x][y];
                for(Direction dir:Direction.values()){
                    setLink(Square,dir,x,y);
                }
            }
        }
        return this.board;
    }
    private void setLink(Square square,Direction dir,int x,int y){
        int dirX=(width+x+dir.getDeltaX())%width;
        int dirY=(height+y+dir.getDeltaY())%height;
        Square neighbour=grid[dirX][dirY];
        square.link(neighbour,dir);
    }
}

如此,有了以上方法类,createBoard方法即可重构为:

public Board createBoard(Square[][] grid){
    return new BoardCreator(grid).create();
}

本章警告:

⑴不要牺牲可维护性来优化性能,除非有可靠的性能测试能够证明确实存在性能问题,并且你的性能优化措施也真的有效果。

⑵为你的继任者(也为将来的自己)编写易于阅读和理解的代码。

⑶当似乎可以重构但是并没有什么意义时,请重新思考系统的架构。

⑷精挑细选可以描述功能的方法名,并将代码放在短小的代码单元中(最多15行代码)。

②编写简单的代码单元

原则:

  • 限制每个代码单元分支点的数量不超过4个。
  • 你应该将复杂的代码单元拆分成多个简单的单元,避免多个复杂的单元在一起。
  • 该原则能提高可维护性的原因在于,分支点越少,代码单元越容易被修改和测试。

概念:

  • 代码单元分支点: 能够覆盖分支点所有分支的路径数量。 在Java中,这些语句均被认为是分支点:
    1. if
    2. case
    3. ?
    4. &&,||
    5. while
    6. for
    7. catch
  • McCabe复杂度:分支点数量加一 ,又称圈复杂度或循环复杂度。因此这个原则相当于“限制McCabe复杂度不超过5”。

如何使用本原则?

Ⅰ.处理链式条件语句

以以下代码为例。对于一个国家来说,getFlagColors方法会返回正确的国旗颜色:

public List<Color> getFlagColors(Nationality notionality){
    List<Color> result;
    switch(nationality){
        case DUTCH:
            result=Arrays.asList(Color.RED,Color.WHITE,Color.BLUE);
            break;
        case GERMAN:
            result=Arrays.asList(Color.BLACK,Color.RED,Color.YELLOW);
            break;
        case BELGIAN:
            result=Arrays.asList(Color.BLACK,Color.YELLOW,Color.RED);
            break;
        case FRENCH:
            result=Arrays.asList(Color.BLUE,Color.WHITE,Color.RED);
            break;
        case ITALIAN:
            result=Arrays.asList(Color.GREEN,Color.WHITE,Color.RED);
            break;
        case UNCLASSIFIED;
        default:
            result=Arrays.asList(Color.GRAY);
            break;
    }
    return result;
}

显然,该方法的复杂度达到了6+1=7,我们需要对其进行优化。
方法一:引入一个Map数据结构:
这种方法的核心观念是:创建一个字典,以便使用一值直接映射到另一个值,从而将判断语句省略。

private static Map<Nationality,List<Color>> FLAGS=new HashMap<Nationality,List<Color>>();
static{
    Flags.put(DUTCH,Arrays.asList(Color.RED,Color.WHITE,Color.BLUE));
    Flags.put(GERMAN,Arrays.asList(Color.BLACK,Color.RED,Color.YELLOW));
    Flags.put(BELGIAN,Arrays.asList(Color.BLACK,Color.YELLOW,Color.RED));
    Flags.put(FRENCH,Arrays.asList(Color.BLUE,Color.WHITE,Color.RED));
    Flags.put(ITALIAN,Arrays.asList(Color.GREEN,Color.WHITE,Color.RED));
}
public List<Color> getFlagColors(Nationality nationality){
    List<Color> color=FLAGS.get(nationality);
    return colors!=null?colors:Arrays.asList(Color.GRAY);8
}

方法二:使用多态来代替条件判断
这种方法的核心观念是:让每个国旗都拥有一个自己的类型,并实现同一个接口。Java语言的多态性会保证在运行时调用到正确的类型。

//为了进行这次重构,我们首先定义一个共用的Flag接口
public interface Flag{
    List<Color> getColors();
}
//为不同的国家定义不同的国旗类型,例如荷兰国旗:
public class DutchFlag implements Flag{
    public List<Color> getColors(){
        return Arrays.asList(Color.RED,Color.WHITE,Color.BLUE);
    }
}
//以及意大利国旗:
public class ItalianFlag implements Flag{
    public List<color> getColors(){
        return Arrays.asList(Color.GREEN,Color.WHITE,Color.RED);
    }
}
//中间的国旗类定义不予赘述

private static final Map(Nationality nationality){}
static{
    Flags.put(DUTCH,new DutchFlag());
    Flags.put(GERMAN,new GermanFlag());
    Flags.put(BELGIAN,new BelgianFlag());
    Flags.put(FRENCH,new FrenchFlag());
    Flags.put(ITALIAN,new ItalianFlag());
}
public List<Color> getFlagColors(Nationality nationality){
    Flag flag=FLAGS.get(nationality);
    flag=flag!=null?flag:new DefaultFlag();
    return flag.getColors();
}

这种重构技巧提供了最灵活的实现方式,例如,你只需要实现新的国旗类型,就可以不断增加所支持的国旗种类,并且可以对它进行独立测试。不过这种方式的不好之处是引入了更多的类和代码。开发人员必须在可扩展性和简洁性之间做出选择。

Ⅱ.嵌套条件语句

如下例所示,我们给定一个二分查找树的根节点和一个整数,calculateDepth方法会找出整数在树中的位置。如果找到,该方法会返回整数在树中的深度,否则抛出一个TreeException异常:

 public int calculateDepth(BinaryTreeNode<Integer>t,int n){
     int depth=0;
     if(t.getValue()==n){
         retrun depth;
     }else{
         if(n<t.getValue()){
             BinaryTreeNode<Inter> left=t.getLeft();
             if(left=null){
                 throw new TreeException("Value not found in tree!");
             }else{
                 return 1+calculateDepth(Left,n);
             }
         }else{
             BinaryTreeNode<Inter> right=t.getRight();
             if(right==null){
                 throw new TreeException("Value not found in tree!");
             }else{
                 return 1+calculateDepth(right,n);
             }
         }
     }
 }

为了提高可读性,我们可以标识出各种独立的情况,并插入return语句来代替嵌套式的条件语句,这种做法在重构中被称为使用卫语句来代替嵌套的条件语句

 public static int calculateDepth(BinaryTreeNode<Integer> t,int n){
     int depth=0;
     if(t.getValue()==n)return depth;
     if(n<getValue()&&t.getLeft()!=null)return 1+calculateDepth(t.getLeft(),n);
     if(n<getValue()&&t.getRight()!=null)return 1+calculateDepth(t.getRight(),n);
     throw new TreeException("Value not found in tree!");
 }

这样一来,代码就变得更容易理解了,但是其复杂度并没有降低,为了降低复杂度,应该将嵌套的条件语句提取到其他方法中

 public static int calculateDepth(BinaryTreeNode<Integer> t int n){
     int depth=0;
     if(t.getValue()==n)return depth;
     else return traverseByValue(t,n);
 }

 public static int traverseByValue(BinaryTreeNode<Integer> t,int n){
     BinaryTreeNode<Integer> childNode=getChildNode(t,n);
     if(childNode==Null){
         throw new TreeException("Value not found in tree!");
     }else{
         return 1+calculateDepth(childNode,n);
     }
 }

 private static BinaryTreeNode<Integer> getChildNode(BinaryTreeNode<Integer> t,int n)
 {
     if(n<t.getValue())return t.getLeft();
     else return t.getRight();
 }

<====To be continue====>

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据