一个机器人降落在火星上,恰好是一个笛卡尔网格;假设我们把这些指令交给机器人,比如LFFFRFFFRRFFF,其中"L“是”向左转90度“,"R”是“向右转90度”,"F“是”前进一个空格“,请为机器人编写控制代码,以便它最终到达正确的目的地,并包括单元测试。
下面是命令“FF”的示例输出:
0,2
class Robot {
private int x;
private int y;
private int currentDirection;
Robot() {
this(0, 0);
}
Robot(int x, int y) {
this.x = x;
this.y = y;
currentDirection = 0;
}
public void move(String moves) {
for (char ch : moves.toCharArray()) {
if (ch == 'R') currentDirection += 1;
if (ch == 'L') currentDirection -= 1;
currentDirection = currentDirection % 4;
if (ch != 'F') continue;
System.out.println(currentDirection);
if (currentDirection == 0) {
y += 1;
} if (currentDirection == 1) {
x += 1;
} if (currentDirection == 2) {
y -= 1;
} if (currentDirection == 3) {
x -= 1;
}
}
}
public void reset() {
x = 0;
y = 0;
currentDirection = 0;
}
public String position() {
return x + ":" + y;
}
}
class Main {
public static void main(String[] args) {
Robot robot = new Robot();
robot.move("FF");
System.out.println(robot.position()); // 0,2
robot.reset();
System.out.println(robot.position()); // 0,0
robot.move("FFRF");
System.out.println(robot.position()); // 1,2
robot.reset();
robot.move("FFRRRFF");
System.out.println(robot.position()); // -2,2
}
}如何使这段代码更面向对象?
发布于 2019-03-03 22:59:12
我会考虑将方向重构成一个枚举,而不是一个神奇的数字。这将允许在这种类型中封装相关的逻辑。
看起来可能是这样的:
enum Direction {
UP(0, 1),
RIGHT(1, 0),
DOWN(0, -1),
LEFT(-1, 0);
final int xVector;
final int yVector;
private final Direction FIRST = Direction.values()[0];
private final Direction LAST = Direction.values()[Direction.values().length];
Direction(int xVector, int yVector) {
this.xVector = xVector;
this.yVector = yVector;
}
Direction rotatedLeft() {
return this == FIRST
? LAST // cycle complete
: Direction.values()[ordinal() - 1]; // previous value
}
Direction rotatedRight() {
return this == LAST
? FIRST // cycle complete
: Direction.values()[ordinal() + 1]; // next value
}
}然后Robot代码变得(包括一些与面向对象方面本身无关的主观清理):
class Robot {
private int x;
private int y;
private Direction currentDirection;
Robot() {
this(0, 0);
}
Robot(int x, int y) {
this.x = x;
this.y = y;
currentDirection = Direction.UP;
}
public void move(String moves) {
for (char code : moves.toCharArray()) {
// a matter of taste, I like to extract logic
// out of looping constructs for clarity
move(code);
}
}
private void move(char code) {
switch (code) {
case 'R': {
currentDirection = currentDirection.rotatedRight();
break;
}
case 'L': {
currentDirection = currentDirection.rotatedLeft();
break;
}
case 'F': {
// you'd call the println thing here.
// as it's not part of the requirements I assumed it to be a debugging artifact
this.x += currentDirection.xVector;
this.y += currentDirection.yVector;
break;
}
}
}
public void reset() {
x = 0;
y = 0;
currentDirection = Direction.UP;
}
public String position() {
return x + ":" + y;
}
}我认为这更多的是面向对象的,也是一个改进。
您可以更进一步,将x和y坐标封装在一个小的独立类(Position)中。就像这样:
class Position {
static final Position DEFAULT = new Position(0, 0);
final int x;
final int y;
public Position(int x, int y) {
this.x = x;
this.y = y;
}
public Position movedInto(Direction direction) {
return new Position(x + direction.xVector, y + direction.yVector);
}
@Override
public String toString() {
return x + ":" + y;
}
}然后,在Robot类中,用Position position替换x和y字段,不再重新计算Robot类中的位置,因为您可以简单地这样做:
case 'F': {
position = position.movedInto(currentDirection);
break;
}在move方法中。
加:
public void reset() {
position = Position.DEFAULT;
currentDirection = Direction.UP;
}
public String position() {
return position.toString();
}另一个想法是将移动代码封装到一个或多个类中。有时很难说出在哪里划分界限,因为代码越来越多的OO是以更多的样板为代价的,并且可能是一种过度工程的形式。
我承认我还没有测试我的重构版本,我只是从代码设计的角度来探讨这个问题。
发布于 2019-03-03 23:06:52
正如所写的,你的机器人着陆器可以降落在任何X,Y坐标在你的网格,但将永远面对正的Y轴方向。这似乎不合理。如果风、湍流会导致位置不确定,这需要在一个特定的X,Y坐标上初始化机器人,那么假设它也可能降落在任何一个平面上似乎是合理的。
Robot() {
this(0, 0, 0);
}
Robot(int x, int y) {
this(x, y, 0);
}
Robot(int x, int y, int initial_facing) {
// ...
}您的指令是一系列单字母命令,但实际上不能向机器人发送一个命令。您应该将单独的命令与一系列的说明分开。类似于:
void turn_left() { ... }
void turn_right() { ... }
void move_forward() { ... }
public void command(char command_letter) {
switch (command_letter) {
case 'L': turn_left(); break;
case 'R': turn_right(); break;
case 'F': move_forward(); break;
default: throw new IllegalArgumentException("Invalid command: "+command_letter);
}
}
public void instructions(String moves) {
for (char command_letter : moves.toCharArray()) {
command(command_letter);
}
}position()返回一个包含机器人坐标的String。如果控制程序想要查询机器人的位置,为了确定应该发送哪些命令将其发送到所需的位置,则需要将该字符串解析为整数值。
考虑返回实际整数位置,可能如下:
public int[] position() {
return new int[]{ x, y };
}或者,您可能希望创建一个class Position,它可以存储机器人的x,y位置。或者你可以使用java.awt.Point
public Point position() {
return new Point(x, y);
}也许可以重写toString()方法来返回对机器人的友好描述,包括它的位置。或者是一种position_as_string()方法。
机器人面向哪边?不能直接说出来!目前您必须查询position(),然后是move("F"),然后是position(),然后比较位置以确定机器人面临的方向!添加一个facing()方法怎么样?
学习如何编写适当的单元测试。例如,使用JUnit5,您可以编写:
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class RobotTests {
private final Robot robot = new Robot();
@Test
void starts_at_origin() {
assertEquals("0:0", robot.position());
}
@Test
void move_forward_twice() {
robot.move("FF");
assertEquals("0:2", robot.position());
}
@Test
void move_and_turn_right() {
robot.move("FFRF");
assertEquals("1:2", robot.position());
}
@Test
void three_rights_make_a_left() {
robot.move("FFRRRFF");
assertEquals("-2:2", robot.position());
}
@Test
void but_one_left_does_not() {
robot.move("FFLFF");
assertEquals("-2:2", robot.position());
}
}注意,每个测试都在一个全新的RobotTests实例中运行,因此不需要在每个实例之间调用robot.reset()。
如果你运行这个单元测试,你会发现5个测试中有4个通过了,一个测试失败了。我让你自己找出原因。
currentDirection采用0、1、2和3值来表示四个基本方向是有限的。如果以后要添加对角线移动(NW、SW、SE或NE),您会使用值4及以上来表示它们吗?还是将原来的4个方向重新编号为0、2、4和6,并使用1、3、5和7作为对角线方向?
您可能会倾向于使用enum作为方向值,但我认为这不是一个好主意。我将使用0、90、180和270作为方向值。这些都有物理意义。如果以后你的机器人被允许使用一个更真实的double x, y;坐标系,你也可以改变为使用double currentDirection;,并允许一个零度的旋转。使用enum,您将失去这种未来可能的灵活性。
作为另一种选择,您还可以考虑使用定向向量:
int dx=0, dy=1; // currentDirection = 0°move_forward简单地变成:
x += dx;
y += dy;然后右转就会变成:
int old_dx = dx;
dx = dy;
dy = -old_dx;https://codereview.stackexchange.com/questions/214669
复制相似问题