最近在学习设计模式,比较巧合的是,昨天在看其他人博客的是,发现了一道比较有意思的面试题目,想用比较好的方法来设计他,一路思考,今天抽出午休时间完成了他,基于之前学习的设计模式系列之装饰模式(DECORATOR PATTERN),我发现这个题目非常适合使用装饰器模式,顺便标注以前原博主的文章链接:最近的一次面试.
代码下载地址:设计模式的学习 src/main/com/zhoutao123/design目录
我们首先看题目
题目要求
一队机器人漫游车将被美国宇航局降落在火星高原上。漫游车将在这个奇怪的长方形高原上巡游,以便他们的机载摄像头可以获得周围地形的完整视图,并将其发送回地球。漫游者的坐标和位置由x和y坐标的组合以及代表四个方向(E, S, W, N)的字母表示。高原划分为网格以简化导航。比如位置0,0,N,表示漫游车位于左下角并面向北。为了控制漫游车,美国宇航局发送一串简单的字母。指令字母是’L’,’R’和’M’。 ‘L’和’R’使漫游车分别向左或向右旋转90度,而不会从当前地点移动。 ‘M’表示前进一个网格点,并保持相同的方向。
假设从(x,y)直接向北移动,就到了(x,y + 1)。
INPUT:
- 第一行输入是平台的右上角坐标,左下角坐标被假定为0,0。
其余的输入是有关已部署的漫游车的信息。每个漫游车都有两行输入。第一行给出了漫游车的位置,第二行是告诉漫游车如何探索高原的一系列指令。位置由两个整数和一个由空格分隔的字母组成,对应于x和y坐标以及漫游车当前的方向。
每个漫游车将按顺序完成,这意味着第二个漫游车在第一个漫游车完成移动之前不会开始移动。
OUTPUT:
- 每个漫游车的输出应该是其最终的坐标和位置。
效果示例
- 输入:
- 5 5
- 1 2 N
- LMLMLMLMM
- 3 3 E
- MMRMMRMRRM
- 预期产出:
- 1 3 N
- 5 1 E
由于这里只考虑怎么实现,所以就不写那些简单的输入提示了,我直接使用变量代替.
问题分析
从装饰模式考虑,我们有一个原点位置,然后经过移动,左右转向到达一个新的位置,因此我们可以定义移动,左转,右转操作,来装饰原点,当装饰然后之后我们只要调用修饰完成的点的方法之后,他就会一层一层的向深处执行,然后返回,类似于入栈和出栈(之前也考虑到使用栈来实现,有时间可以尝试使用栈来实现).入栈的时候定义操作,出站的时候执行操作,一步一步按照我们设定的方法执行.
具体代码
操作抽象类
package com.zhoutao123.design.example;
public abstract class Operate {
// 声明为public,主要是懒得写get/set方法
public int x;
public int y;
// 方向使用枚举类
public Direction direction;
// 抽象方法,执行操作,不同的操作不同的实现
abstract Operate exec();
// 主要是方便打印数据
@Override
public String toString() {
return "坐标{" +
"x=" + x +
", y=" + y +
", 方向=" + direction +
'}';
}
// 方向枚举类
enum Direction {
N, S, W, E
}
}
位置类
位置location只要在个个操作之间传递,用于定义初始化的点,因此继承了Operate
package com.zhoutao123.design.example;
public class Localtion extends Operate {
public Localtion(int x,int y,Direction direction) {
this.x = x;
this.y = y;
this.direction = direction;
}
@Override
Operate exec() {
return this;
}
}
操作实现类
这里主要是实现不同的对点的操作,声明如下:
public class TurnLeft extends Operate {} 向左转
public class TurnRight extends Operate {} 向右转
public class TurnMove extends Operate {} 移动一个单位
下面看主要代码
向左转向
package com.zhoutao123.design.example;
public class TurnLeft extends Operate {
// 持有下一个动作,构造的时候传入
private Operate operate;
public TurnLeft(Operate operate) {
this.operate = operate;
}
@Override
public Operate exec() {
// 调用下一个动作exec方法,获得位置新
this.operate = operate.exec();
// 根据自己的实现的功能实现转向
switch (operate.direction) {
case E:
operate.direction = Direction.N;
break;
case S:
operate.direction = Direction.E;
break;
case W:
operate.direction = Direction.S;
break;
case N:
operate.direction = Direction.W;
}
// 向上一层返回操作后的点的位置信息
return this.operate;
}
}
向右转向
和向左转非常类似,不再赘述
package com.zhoutao123.design.example;
public class TurnRight extends Operate {
private Operate operate;
public TurnRight(Operate operate) {
this.operate = operate;
}
@Override
public Operate exec() {
this.operate = operate.exec();
switch (operate.direction) {
case E:
operate.direction = Direction.S;
break;
case S:
operate.direction = Direction.W;
break;
case W:
operate.direction = Direction.N;
break;
case N:
operate.direction = Direction.E;
}
return this.operate;
}
}
移动命令
package com.zhoutao123.design.example;
public class TurnMove extends Operate {
private Operate operate;
public TurnMove(Operate operate) {
this.operate = operate;
}
@Override
public Operate exec() {
this.operate = operate.exec();
switch (operate.direction) {
case E:
operate.x++;
break;
case S:
operate.y--;
break;
case W:
operate.x--;
break;
case N:
operate.y++;
}
if (operate.x < 0 || operate.y < 0 || operate.x > 5 || operate.y > 5) {
throw new IllegalStateException("命令有误,小车已经掉下悬崖");
}
return this.operate;
}
}
测试代码
完成了三个操作,以及初始化点之后,我们尝试测试下.
需要说明的是,这里我并没有按照题目要求实现输入,直接定义了一个String来测试的,也就是说我只是实现了核心功能.
package com.zhoutao123.design.example;
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
// 定义命令
String command1 = "LMLMLMLMM";
// 定义初始化的点
Operate initOpera1 = new Localtion(1, 2, Operate.Direction.N);
// 输出结果
System.out.println(String.format("经过命令%s后移动到%s",command1,packageCommand(command1,initOpera1).exec()));
String command2 = "MMRMMRMRRM";
Operate initOpera2 = new Localtion(3, 3, Operate.Direction.E);
System.out.println(String.format("经过命令%s后移动到%s",command2,packageCommand(command2,initOpera2).exec()));
String command3 = "MLMRMLMRMLMRMLMRMLM";
Operate initOpera3 = new Localtion(0, 0, Operate.Direction.E);
System.out.println(String.format("经过命令%s后移动到%s",command3,packageCommand(command3,initOpera3).exec()));
}
// 开始装饰疯转操作
public static Operate packageCommand(String commandStr,Operate operate){
for (char command : commandStr.toCharArray()) {
switch (command) {
case 'L':
operate = new TurnLeft(operate);
break;
case 'R':
operate = new TurnRight(operate);
break;
case 'M':
operate = new TurnMove(operate);
break;
default:
throw new IllegalArgumentException("非法的命令,程序退出");
}
}
return operate;
}
}
测试结果
经过命令LMLMLMLMM后移动到坐标{x=1, y=3, 方向=N}
经过命令MMRMMRMRRM后移动到坐标{x=5, y=1, 方向=E}
经过命令MLMRMLMRMLMRMLMRMLM后移动到坐标{x=5, y=5, 方向=N}
总结
可以看到装饰模式的使用让代码看起来非常的整洁,逻辑清晰,而且易于拓展,假设有一天新的需求过来,要求实现45°转弯,这时候我们之要是实现向左转45°和向右转45°(实现方法和TurnLeft以及Left类似),然后Move操作在向左或者向右转45°的之后,合理的处理xy即可.甚至说,Move移动两步,我们使用装饰模式都可以非常轻松的实现,并且代码逻辑清晰.
设计模式的核心理念:
- 对接口编程而不是对实现编程
- 少用继承多用组合
- 代码要对修改关闭,对拓展开放