瞎写一时爽,维护火葬场
天天写尸堆,自己都看不下去了!什么时候开始自己的代码变得那么丑陋了呢?经过一番回想,我发现凡是没有约束的代码,变得丑陋只是时间问题。 综上所述,我拜读了Joost Visser的Building Maintainable Software这本书(中文译名为“代码不朽”),受益匪浅,在此做一下笔记以防遗忘。
代码不朽 ——编写可维护软件的十大要则
①编写较小的代码单元 原则:
代码单元的长度应该限制在15行代码以内。
为此首先不要编写超过15行代码的单元,或者将长的单元分解成多个更短的单元 ,直到每个单元都不超过15行代码。
该原则能提高可维护性的原因在于,短小的代码但愿易于理解,测试及重用。
如何应用本原则? ——使用重构技巧来应用原则
㈠重构技巧:提取方法 将内容代码较多的单元提取为几个代码较少的单元,此时代码总量将增加,但是我们总要在代码总量与后期可维护性之间做出权衡,不止这里,后面讲的方法也或多或少地增加了代码总量,但却能大大提高后期可维护性。 Example——吃豆人游戏中的一段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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行代码,现在我们提取方法,可将其分解成三个小方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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
。拥有很长参数的方法显得丑陋无比,下面使用方法对象来将其替换之:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 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
方法即可重构为:
1 2 3 public Board createBoard (Square[][] grid) { return new BoardCreator(grid).create(); }
本章警告:
⑴不要牺牲可维护性来优化性能,除非有可靠的性能测试能够证明确实存在性能问题,并且你的性能优化措施也真的有效果。 ⑵为你的继任者(也为将来的自己)编写易于阅读和理解的代码。 ⑶当似乎可以重构但是并没有什么意义时,请重新思考系统的架构。 ⑷精挑细选可以描述功能的方法名,并将代码放在短小的代码单元中(最多15行代码)。
②编写简单的代码单元 原则:
限制每个代码单元分支点的数量不超过4个。
你应该将复杂的代码单元拆分成多个简单的单元 ,避免多个复杂的单元在一起。
该原则能提高可维护性的原因在于,分支点越少,代码单元越容易被修改和测试。
概念:
代码单元分支点: 能够覆盖分支点所有分支的路径数量。 在Java中,这些语句均被认为是分支点:
if
case
?
&&,
while
for
catch
McCabe复杂度:分支点数量加一 ,又称圈复杂度或循环复杂度。因此这个原则相当于“限制McCabe复杂度不超过5”。
如何使用本原则? Ⅰ.处理链式条件语句 以以下代码为例。对于一个国家来说,getFlagColors
方法会返回正确的国旗颜色:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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数据结构: 这种方法的核心观念是:创建一个字典,以便使用一值直接映射到另一个值,从而将判断语句省略。
1 2 3 4 5 6 7 8 9 10 11 12 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语言的多态性会保证在运行时调用到正确的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 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异常:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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
语句来代替嵌套式的条件语句,这种做法在重构中被称为使用卫语句来代替嵌套的条件语句 :
1 2 3 4 5 6 7 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!" ); }
这样一来,代码就变得更容易理解了,但是其复杂度并没有降低 ,为了降低复杂度,应该将嵌套的条件语句提取到其他方法中 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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====>