`
kent0113
  • 浏览: 4015 次
  • 性别: Icon_minigender_1
  • 来自: 北京
最近访客 更多访客>>
社区版块
存档分类
最新评论

MarRover代码重构

阅读更多

上周末参与公司的招聘面试,跟其中的一个候选人pair编程,但由于面试时间有限,实现了新需求后,只重构了代码的一小部分,所以在面试之后,我就继续将剩余的部分重构完成。重构的整个过程可以clone一份看看(git@github.com:wjh-thoughtworks/MarRoversRefactor.git)

题目大概的意思是这样的,有一个机器人在一个平原里,我们用坐标轴给机器人定位。这个平原的大小是有限的,(5,5)代表x的上限为5,y的上限为5。机器人有初始位置和朝向,例如(1,2,N),在坐标轴的点(1,2)上,面朝北。我们可以给它下指令(M: 前进,B: 后退,L: 左转,R: 右转),执行一系列指令之后,得出机器人的位置和朝向。例如初始状态为(1,2,N),执行指令LMLMLMLMM后状态变为(1,3,N)。

原始代码如下

 

public class Client {
	//输入指令时请输入大写的字母,小写
	public static void main(String[] args) throws Exception {
		int initx=0;
		int inity=0;
		int boundaryX=0;
		int boundaryY=0;
		Robot r=new Robot();
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); 
        String strBoundary = null; 
        System.out.println("Enter your Boundary x and y:"); 
        strBoundary = br.readLine();
        String[] boundaryArr=strBoundary.split(" ");
        if(!idDigit(boundaryArr[0]) || !idDigit(boundaryArr[1])){
        	System.out.println("Boundary x and y are must be digit,and input them again:"); 
                //重新输入的代码
        }
        //初始化边界点
        Robot.X=Integer.parseInt(boundaryArr[0]);
        Robot.Y=Integer.parseInt(boundaryArr[1]);
        String strInitPos = null; 
        System.out.println("Enter your robot initial x,y and direstion:"); 
        strInitPos = br.readLine();
        String[] initPos=strInitPos.split(" ");
        //加入检查输入合法性的代码
        
        //初始化当前Robot的位置
        r.x=Integer.parseInt(initPos[0]);
        r.y=Integer.parseInt(initPos[1]);
        r.dir=Character.toUpperCase(initPos[2].charAt(0));
        //获得控制动作
        String strOrder = null; 
        while(true){
        	
            System.out.println("Enter your Robot orders:"); 
            strOrder = br.readLine();
            //加入控制命令合法性检查
            String result=r.control(strOrder);
            System.out.println(result);
            if("RIP".equals(result)){
            	System.exit(0);
            }
        }
        
	}

	public static boolean idDigit(String str){
		Pattern pattern = Pattern.compile("[0-9]*");
	    return pattern.matcher(str).matches();
	}
}
public class Robot {
	public static int X;
	public static int Y;
	//position
	public int x;
	public int y;
	//direction
	public char dir;
	
	public String control(String order){
		char[] orders=order.toCharArray();
		for(char tmp : orders){
			tmp=Character.toUpperCase(tmp);
			if(tmp == 'L' || tmp == 'R'){
				this.setDir(tmp);
			}else{
				this.move(tmp);
				if(this.checkError()){
					return "RIP";
				}
			}
		}
		//for end
		return this.x+" "+this.y+" "+this.dir;
	}

	private void setDir(char change){
		if(change=='L'){
			if(this.dir=='N'){
				this.dir='W';
			}else if(this.dir=='S'){
				this.dir='E';
			}else if(this.dir=='W'){
				this.dir='S';
			}else if(this.dir=='E'){
				this.dir='N';
			}
		}else{
			if(this.dir=='N'){
				this.dir='E';
			}else if(this.dir=='S'){
				this.dir='W';
			}else if(this.dir=='W'){
				this.dir='N';
			}else if(this.dir=='E'){
				this.dir='S';
			}
		}
	}

	private void move(char des){
		if(des == 'M'){
			if (this.dir == 'N') {
				this.y += 1;
			} else if (this.dir == 'S') {
				this.y -= 1;
			} else if (this.dir == 'W') {
				this.x -= 1;
			} else if (this.dir == 'E') {
				this.x += 1;
			}
		}else{
			if (this.dir == 'N') {
				this.y -= 1;
			} else if (this.dir == 'S') {
				this.y += 1;
			} else if (this.dir == 'W') {
				this.x += 1;
			} else if (this.dir == 'E') {
				this.x -= 1;
			}
		}
	}

	private boolean checkError(){
		if(this.x>X || this.x<0 || this.y>Y || this.y<0){
			return true;
		}
		return false;
	}
}

在重构之前,一定要有测试,不然重构的过程中可能会带入bug。这里就不展示测试代码了,有兴趣的可以到github去clone看看,注释其实不是特别提倡,因为注释的维护成本比较大,很容易导致方法的实际意图与注释不一致的情况。所以我个人认为,与其写注释,不如让方法和变量的命名更加清晰,这样更好。在《代码整洁之道》一书中有对注释的好坏进行详细的阐述,有兴趣可以去看看。
而这份代码的注释其实也没有什么很大的作用,所以我先把注释去掉。

这份代码主要有两个类,Client类和Robot类。我们先从Client类开始重构。
首先,Client类里面又四个无用的变量,initx,inity,boundaryX,boundaryY,所以先删掉。

这个main方法很长很复杂,所以第一个重构对象就是main方法。
第一步,将main中的代码按照功能,抽取成三个方法,init,initRobot和outputResult。
第二步,再对抽取出来的三个方法进一步重构。
重构后的效果如下:

 

public class Client {
	public static void main(String[] args) throws Exception {
		Robot robot=new Robot();
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        init(br);
        initRobot(robot, br);
        while(true){
            outputResult(robot, br);
        }
        
	}

    private static void outputResult(Robot robot, BufferedReader br) throws IOException {
        System.out.println("Enter your Robot orders:");

        String result=robot.control(br.readLine());

        System.out.println(result);
    }

    private static void initRobot(Robot robot, BufferedReader br) throws IOException {
        System.out.println("Enter your robot initial x,y and direstion:");
        String[] initPos= br.readLine().split(" ");

        robot.x=Integer.parseInt(initPos[0]);
        robot.y=Integer.parseInt(initPos[1]);
        robot.dir=Character.toUpperCase(initPos[2].charAt(0));
    }

    private static void init(BufferedReader br) throws IOException {
        System.out.println("Enter your Boundary x and y:");
        String[] boundaryArr= br.readLine().split(" ");

        if(!idDigit(boundaryArr[0]) || !idDigit(boundaryArr[1])){
        	System.out.println("Boundary x and y are must be digit,and input them again:");
        }

        Robot.X=Integer.parseInt(boundaryArr[0]);
        Robot.Y=Integer.parseInt(boundaryArr[1]);
    }

    public static boolean idDigit(String str){
		Pattern pattern = Pattern.compile("[0-9]*");
	    return pattern.matcher(str).matches();
	}
}

运行测试,全部通过。
接下来,就是重构Robot类。
首先,我们看到就是变量的命名不是很好,我们稍微修改一下。
接下来,我们看到,整一个Robot类,几乎都是if-else,所以接下来就是重构if-else。
第一步,我们将setCurrentDirection和move方法里面的if-else抽取出四个方法,让代码意图更清晰。

 

public class Robot {
    public static int x_bound;
    public static int y_bound;
    public int x;
    public int y;
    public char currentDirection;

    public String control(String order) {
        char[] orders = order.toCharArray();
        for (char tmp : orders) {
            tmp = Character.toUpperCase(tmp);
            if (tmp == 'L' || tmp == 'R') {
                this.setCurrentDirection(tmp);
            } else {
                this.move(tmp);
                if (this.checkError()) {
                    return "RIP";
                }
            }
        }
        return this.x + " " + this.y + " " + this.currentDirection;
    }

    private void setCurrentDirection(char change) {
        if (change == 'L') {
            turnLeft();
        } else {
            turnRight();
        }
    }

    private void move(char des) {
        if (des == 'M') {
            moveForward();
        } else {
            moveBack();
        }
    }

    private void turnRight() {
        if (this.currentDirection == 'N') {
            this.currentDirection = 'E';
        } else if (this.currentDirection == 'S') {
            this.currentDirection = 'W';
        } else if (this.currentDirection == 'W') {
            this.currentDirection = 'N';
        } else if (this.currentDirection == 'E') {
            this.currentDirection = 'S';
        }
    }

    private void turnLeft() {
        if (this.currentDirection == 'N') {
            this.currentDirection = 'W';
        } else if (this.currentDirection == 'S') {
            this.currentDirection = 'E';
        } else if (this.currentDirection == 'W') {
            this.currentDirection = 'S';
        } else if (this.currentDirection == 'E') {
            this.currentDirection = 'N';
        }
    }

    private void moveBack() {
        if (this.currentDirection == 'N') {
            this.y -= 1;
        } else if (this.currentDirection == 'S') {
            this.y += 1;
        } else if (this.currentDirection == 'W') {
            this.x += 1;
        } else if (this.currentDirection == 'E') {
            this.x -= 1;
        }
    }

    private void moveForward() {
        if (this.currentDirection == 'N') {
            this.y += 1;
        } else if (this.currentDirection == 'S') {
            this.y -= 1;
        } else if (this.currentDirection == 'W') {
            this.x -= 1;
        } else if (this.currentDirection == 'E') {
            this.x += 1;
        }
    }

    private boolean checkError() {
        if (this.x > x_bound || this.x < 0 || this.y > y_bound || this.y < 0) {
            return true;
        }
        return false;
    }
}


第二步,我们看到turnLeft,turnRight,moveForward和moveBack都是高度相似的,这四个方法重复率很高。
这四个方法之所以重复,就是因为需要判断’N’,’S’,’E’,’W’,所以我们就需要将用字符转换成用数字代表方位。
建立char direction = new char[]{'N','E','S','W’},按照顺时针排放方位;
建立一个Map directionMap = new HashMap()来将用字符转换为用数字来代表方位。
这样我们就可以0代表N,1代表E,2代表,3代表W,所以之前的currentDirection的类型和setCurrentDirection方法也需要有些改变。
重构的原则是可控的地小步重构,直接修改currentDirection的类型会导致一大片代码需要修改,所以我们先引入一个变量currentDirectionInt。
旧的代码可以被新的代码替换掉,而且新的代码都通过测试,我们再把旧代码删除,然后再将currentDirectionInt重命名为currentDirection即可。
引入新变量currentDirectionInt和setCurrentDirectionInt方法,要跑测试保证没有影响原有功能。

 

第三步,之前turnLeft需要先判断是现在是什么朝向,然后再按照朝向判断左转后是什么方向。而我们现在用数字表示朝向,所以左转其实就是相当于-1。
不过当朝向为N的时候,0减1就成了-1了,为了避免这种情况,左转应该是(currentDirectionInt+3)%4
所以turnleft可以简化成一行代码currentDirectionInt = (currentDirectionInt + 3) % 4;
类似地turnRight就可以简化成currentDirectionInt = (currentDirectionInt + 1) % 4;
同样地,我们先不在原有的方法修改,我们先新加方法turnLeftInt和turnRightInt

第四步,重构moveForward和moveBack。
还有一样的原因,需要判断朝向才能知道x轴和y轴是加1还是减1。
其实如果朝向为N或者S的时候,前进或者后退,x轴的数值都是不会变的,变得只是y轴的值,而每次都是加或者减1。
因为我们以数字表示方位,所以当处于不同朝前进,y轴的变化可以这样表示y_moves = {1,0,-1,0},同理,x轴的变化可以表示为x_moves = {0,1,0,-1}。
因此,moveForward可以简化为
x += x_moves[currentDirection];
y += y_moves[currentDirection];
moveBack简化为
x -= x_moves[currentDirection];
y -= y_moves[currentDirection];

同样,两个新方法moveForwardInt和moveBackInt。

第五步,重构control方法。
还是老思路,先复制一个control方法,名为controlRefactor。
将原来调用turnLeft、turnRight、moveForward和moveBack的,改为调用turnLeftInt、turnRightInt、moveForwardInt和moveBackInt。

第六步,接下来,就是将原来Client和RobotTest里面调用Robot.control的改为调用Robot.controlRefactor
然后跑一下测试,验证是否重构是否正确。

第七步,测试通过了,说明我们的重构完成,现在就需要把旧的方法变量删除掉。
删除之后,再跑测试保证没用删错。

第八步,将重构时候的新方法和变量重命名,之后再跑测试。

至此,Robot类的重构也基本完成了

 

public class Robot {
    public static int x_bound;
    public static int y_bound;
    public int x;
    public int y;
    private int currentDirection;
    private Map directionMap;
    private char[] direction = new char[]{'N', 'E', 'S', 'W'};
    private int[] x_moves = new int[]{0, 1, 0, -1};
    private int[] y_moves = new int[]{1, 0, -1, 0};

    public Robot() {
        directionMap = new HashMap();
        directionMap.put('N',0);
        directionMap.put('E',1);
        directionMap.put('S',2);
        directionMap.put('W',3);
    }

    public String control(String orderString) {
        char[] orders = orderString.toCharArray();
        for (char order : orders) {
            order = Character.toUpperCase(order);
            switch (order) {
                case 'L':
                    turnLeft();
                    break;
                case 'R':
                    turnRight();
                    break;
                case 'M':
                    moveForward();
                    break;
                case 'B':
                    moveBack();
                    break;
            }
            if (this.checkError()) {
                return "RIP";
            }

        }
        return x + " " + y + " " + direction[currentDirection];
    }

    public void setCurrentDirection(char currentDirection) {
        this.currentDirection = (Integer) directionMap.get(currentDirection);
    }

    public void turnLeft() {
        currentDirection = (currentDirection + 3) % 4;
    }

    public void turnRight() {
        currentDirection = (currentDirection + 1) % 4;
    }

    public void moveForward() {
        x += x_moves[currentDirection];
        y += y_moves[currentDirection];
    }

    public void moveBack() {
        x -= x_moves[currentDirection];
        y -= y_moves[currentDirection];
    }

    private boolean checkError() {
        if (this.x > x_bound || this.x < 0 || this.y > y_bound || this.y < 0) {
            return true;
        }
        return false;
    }
}

 

 

重构后的Robot类还是不是很完美,因为有个非常不好看的switch-case。可以使用多态来消失这个switch-case。
我在这里就不再展示了怎么重构了(因为这篇博客已经够长了,再长一点的话就没有人看了微笑),有兴趣可以阅读《重构:改善既有代码的设计》这书中关于使用多态将switch-case去除的例子。
如果Robot中得switch-case用多态去掉后,对设计模式比较熟悉的同学可能看出来,代码长得有点命令模式的味道。所以,这份代码可以更进一步重构成使用命令模式,重构所使用的步骤也不是很复杂,有待同学们去亲手实践一下。

详细的重构过程,可以在github上面clone一份,然后看每一个commit。(git@github.com:wjh-thoughtworks/MarRoversRefactor.git)

最后,总结一下重构的过程:

1、如果没有测试,补充测试

2、去掉一下无用的方法或者变量

3、将比较大的方法通过抽取方法的重构手法,让方法的意图清晰

4、小步快跑地重构原来的方法并在调用的地方做相应的替换

 

 

 

 

 

 

分享到:
评论

相关推荐

    改善既有的代码重构(ppt)课件

    改善既有的代码重构(ppt),改善既有的代码重构,改善既有的代码重构PPT

    代码重构.pdf

    《代码重构》一书由Martin Fowler编写,是软件开发领域中关于代码质量提升的经典之作。书中详细阐述了重构代码的必要性、重构的时机以及如何安全地重构代码。重构指的是在不改变软件外部行为的前提下,改进其内部...

    代码重构PDF

    **代码重构:提升软件设计与代码质量的关键技术** 在软件开发过程中,代码重构是一个至关重要的环节,它关乎到代码的可读性、可维护性和整体性能。代码重构并不意味着添加新功能或修复错误,而是对现有代码结构进行...

    .java代码重构

    《.java代码重构》 代码重构是软件开发过程中的一个重要环节,它涉及到对现有代码的改进,以提高代码的可读性、可维护性,同时并不改变其外在行为。在Java编程中,代码重构是一种常见的实践,尤其在大型项目中,...

    代码重构&模式

    **代码重构** 代码重构是软件开发过程中的一个重要环节,它是指在不改变代码外在行为的前提下,对代码结构、设计和实现进行改进,以提高代码的可读性、可维护性和内部结构。重构的主要目标是使软件更容易理解和修改...

    代码重构 (C# & ASP.NET) 英文原版

    **代码重构:C#与ASP.NET的实践指南** 在软件开发过程中,代码重构是一个至关重要的环节,它旨在改进代码的内部结构,不改变其外部行为。《代码重构 (C# & ASP.NET)》英文原版提供了针对这两种技术的重构策略和技巧...

    测试驱动开发及代码重构

    3. **重构**:一旦测试通过,开发者可以对新添加的代码进行重构,以保持代码简洁、清晰,并符合设计原则,同时确保所有测试仍然通过。 代码重构是TDD的重要组成部分,它是在不改变代码外在行为的前提下,改善代码的...

    java代码重构经验分享

    Java 代码重构经验分享 Java 代码重构是指在不改变外部行为的情况下,修改代码的内部结构,以提高代码的可维护性、可读性和可扩展性。本文总结了 Java 代码重构的经验和技术规范,包括重构要求、重构的工作、代码的...

    Python代码重构的艺术:探索自动化重构工具

    ### Python 代码重构的艺术:探索自动化重构工具 #### 一、引言 Python 作为一门高级编程语言,自1991年首次发布以来,便以其简洁易读的语法、强大的标准库支持以及广泛的跨平台特性赢得了众多开发者的青睐。随着...

    一本介绍如何将现有的 Java 代码重构为 Kotlin 代码的书籍

    书中不仅提供了丰富的重构技巧,还包括了实用的示例和最佳实践,旨在帮助开发者更高效地完成代码转换,并提升重构后代码的质量和性能。 #### 二、重构技巧与方法 **1. 语法转换** - **基础语法差异**:Kotlin在很...

    java代码重构经验总结

    ### Java代码重构经验总结 在软件开发过程中,代码重构是一项重要的技能,它旨在不改变代码外部行为的前提下,改进其内部结构,从而提升代码质量和可维护性。本文将深入探讨Java代码重构的关键点,涵盖重构原则、...

    软件工程中的代码重构技术.pptx

    ### 软件工程中的代码重构技术 #### 第1章:简介 **1.1 什么是代码重构** 代码重构是一种软件工程活动,旨在不改变软件的外部行为的情况下,改进其内部结构和设计。这一过程有助于增强代码的质量和可维护性。 **...

    一本代码重构的书让代码更简洁

    《重构:改善既有代码的设计》是一本由Martin Fowler所著的经典书籍,专注于讲解如何通过重构技术来提升代码质量,使其更具可读性、可维护性和扩展性。在Java编程领域,重构是提升软件开发效率和降低维护成本的重要...

    代码重构源码(包含重构前后代码)

    代码重构是一种重要的软件开发实践,旨在改进代码的结构和可读性,而不改变其外部行为。这个压缩包文件“代码重构源码(包含重构前后代码)”提供了面向对象编程领域中的一个实例,具体是对影片出租店租赁程序的重构...

    Python代码重构:提升代码质量的艺术

    在软件开发中,代码重构是一个不可或缺的过程,旨在不改变外部行为的前提下,改善代码的内部结构。Python作为一种动态类型、解释型的高级脚本语言,其代码的可读性和可维护性尤为重要。本文将详细探讨在Python中进行...

    代码重构总结实例

    代码重构是一种改善软件设计、提升代码可读性和可维护性的技术。它并不改变代码的外在行为,而是通过改进代码结构来提高代码质量。在这个"代码重构总结实例"中,我们将探讨一系列相关知识点,包括重构的原因、原则、...

    swift-高仿微信iOSAppTemplate代码重构

    这个项目名为"swift-高仿微信iOSAppTemplate代码重构",旨在通过重构代码来提升应用的可读性、可维护性和性能。在这个过程中,开发者将深入理解Swift的面向对象编程、MVVM设计模式、网络请求处理、UI布局以及数据...

    代码重构技术

    代码重构技术

Global site tag (gtag.js) - Google Analytics