0
\$\begingroup\$

The following code is my solution to a code challenge I submitted a few days ago. I got rejected straight away with no feedback and I've wondered why.

Here is the full code repo.

Requirements

The application is a simulation of a toy robot moving on a square tabletop, of dimensions 5 x 5 units. There are no other obstructions on the table surface. The robot is free to roam around the surface of the table, but must be prevented from falling to destruction. Any movement that would result in the robot falling from the table must be prevented, however further valid movement commands must still be allowed. Create an application that can read in commands of the following form:

PLACE X,Y,F

MOVE

LEFT

RIGHT

REPORT

PLACE will put the toy robot on the table in position X,Y and facing NORTH, SOUTH, EAST or WEST.

The origin (0,0) can be considered to be the SOUTH WEST most corner.

MOVE will move the toy robot one unit forward in the direction it is currently facing.

LEFT and RIGHT will rotate the robot 90 degrees in the specified direction without changing the position of the robot.

REPORT will announce the X,Y and F of the robot.

Constraints:

  • The application must be a Spring-Boot-Application
  • Input must be realised over the REST-API, take care when designing the REST-API
  • The robot that is not on the table can choose the ignore the MOVE, LEFT, RIGHT and REPORT commands.
  • The robot must not fall off the table during movement. This also includes the initial placement of the toy robot.
  • Any move that would cause the robot to fall must be ignored.
  • It is not required to provide any graphical output showing the movement of the toy robot.

Plain input Examples:

PLACE 0,0,NORTH MOVE REPORT

Output: 0,1,NORTH

PLACE 0,0,NORTH LEFT REPORT

Output: 0,0,WEST

PLACE 1,2,EAST MOVE MOVE LEFT MOVE REPORT

Output: 3,3,NORTH

MOVE REPORT

Output: ROBOT MISSING

Solution

ToyRobotApplication.java

package com.puzzle.toyrobot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ToyRobotApplication {

    public static void main(String[] args) {
        SpringApplication.run(ToyRobotApplication.class, args);
    }
}

RobotSimulationController.java

package com.puzzle.toyrobot.controller;

import com.puzzle.toyrobot.model.Report;
import com.puzzle.toyrobot.model.SimulationRound;
import com.puzzle.toyrobot.service.RobotSimulationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RobotSimulationController {

    private RobotSimulationService simulationService;

    @Autowired
    public RobotSimulationController(RobotSimulationService simulationService) {
        this.simulationService = simulationService;
    }

    @PostMapping
    @RequestMapping("/robot/simulation")
    public ResponseEntity<Report> newSimulationRound(@RequestBody SimulationRound simulationRound) {
        Report report = simulationService.start(simulationRound);
        return ResponseEntity.ok(report);
    }
}

RobotSimulationService.java

package com.puzzle.toyrobot.service;

import com.puzzle.toyrobot.model.Report;
import com.puzzle.toyrobot.model.Robot;
import com.puzzle.toyrobot.model.SimulationRound;
import com.puzzle.toyrobot.model.command.Command;
import com.puzzle.toyrobot.model.command.CommandFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class RobotSimulationService {

    private final Logger log = LoggerFactory.getLogger(RobotSimulationService.class);

    public Report start(SimulationRound simulationRound) {

        Robot robot = new Robot();

        Report report = new Report();

        for (String commandString : simulationRound.getCommands()) {

            Command command = CommandFactory.getCommand(commandString);

            if (command != null) {

                command.execute(robot, report);

            } else {

                log.debug("Wrong command: " + commandString);
            }
        }

        return report;
    }

}

SimulationRound.java

package com.puzzle.toyrobot.model;

import java.util.ArrayList;
import java.util.List;

public class SimulationRound {

    private List<String> commands = new ArrayList<>();

    public List<String> getCommands() {
        return commands;
    }

    public void setCommands(List<String> commands) {
        this.commands = commands;
    }

    public void addCommand(String command) {
        commands.add(command);
    }
}

Robot.java

package com.puzzle.toyrobot.model;

public class Robot {

    public static final Integer MAX_POSITION = 4;
    public static final Integer MIN_POSITION = 0;

    private Integer xPosition;

    private Integer yPosition;

    private CardinalDirection cardinalDirection;

    public Robot() {
    }

    public Robot(Integer xPosition, Integer yPosition, CardinalDirection cardinalDirection) {
        this.xPosition = xPosition;
        this.yPosition = yPosition;
        this.cardinalDirection = cardinalDirection;
    }

    public Integer getXPosition() {
        return xPosition;
    }

    public void setXPosition(Integer xPosition) {
        this.xPosition = xPosition;
    }

    public Integer getYPosition() {
        return yPosition;
    }

    public void setYPosition(Integer yPosition) {
        this.yPosition = yPosition;
    }

    public CardinalDirection getCardinalDirection() {
        return cardinalDirection;
    }

    public void setCardinalDirection(CardinalDirection cardinalDirection) {
        this.cardinalDirection = cardinalDirection;
    }

    public boolean isOnTable() {
        return xPosition != null && yPosition != null && cardinalDirection != null;
    }

    public String getCurrentStatus() {
        return String.join(",", xPosition.toString(), yPosition.toString(), cardinalDirection.toString());
    }


    public void increaseYPosition() {
        yPosition++;
    }

    public void decreaseYPosition() {
        yPosition--;
    }

    public void increaseXPosition() {
        xPosition++;
    }

    public void decreaseXPosition() {
        yPosition++;
    }
}

Report.java package com.puzzle.toyrobot.model;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

public class Report {

    private List<String> output = new ArrayList<>();

    public List<String> getOutput() {
        return output;
    }

    public void setOutput(List<String> output) {
        this.output = output;
    }

    public void addOutput(String outupt) {
        this.output.add(outupt);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Report report = (Report) o;
        return Objects.equals(output, report.output);
    }

    @Override
    public int hashCode() {
        return Objects.hash(output);
    }
}

CardinalDirection.java

package com.puzzle.toyrobot.model;

public enum CardinalDirection {
    EAST, WEST, SOUTH, NORTH
}

Command.java

package com.puzzle.toyrobot.model.command;


import com.puzzle.toyrobot.model.Report;
import com.puzzle.toyrobot.model.Robot;

public abstract class Command {

    public abstract void execute(Robot robot, Report report);
}

CommandFactory.java

package com.puzzle.toyrobot.model.command;

public class CommandFactory {

    private static final String PLACE_COMMAND_REGEX = "^(PLACE)\\s\\d+,\\d+,(NORTH|WEST|EAST|SOUTH)$";

    public static Command getCommand(String commandString) {

        if (commandString.matches(PLACE_COMMAND_REGEX)) {
            return new PlaceCommand(commandString);

        } else if (commandString.equals(CommandType.LEFT.name())) {
            return new LeftCommand();

        } else if (commandString.equals(CommandType.RIGHT.name())) {
            return new RightCommand();

        } else if (commandString.equals(CommandType.REPORT.name())) {
            return new ReportCommand();

        } else if (commandString.equals(CommandType.MOVE.name())) {
            return new MoveCommand();

        }
        return null;
    }
}

CommandType,java

package com.puzzle.toyrobot.model.command;

public enum CommandType {
    PLACE, MOVE, LEFT, RIGHT, REPORT
}

LeftCommand.java

package com.puzzle.toyrobot.model.command;

import com.puzzle.toyrobot.model.CardinalDirection;
import com.puzzle.toyrobot.model.Report;
import com.puzzle.toyrobot.model.Robot;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LeftCommand extends Command {

    private final Logger log = LoggerFactory.getLogger(LeftCommand.class);

    public void execute(Robot robot, Report report) {

        if (!robot.isOnTable()) {
            log.debug("Left command ignored");
        } else {
            switch (robot.getCardinalDirection()) {
                case NORTH:
                    robot.setCardinalDirection(CardinalDirection.WEST);
                    break;
                case SOUTH:
                    robot.setCardinalDirection(CardinalDirection.EAST);
                    break;
                case EAST:
                    robot.setCardinalDirection(CardinalDirection.NORTH);
                    break;
                case WEST:
                    robot.setCardinalDirection(CardinalDirection.SOUTH);
                    break;
            }
            log.debug("The robot is rotating 90 degree to " + robot.getCardinalDirection());
        }

    }
}

MoveCommand.java

package com.puzzle.toyrobot.model.command;

import com.puzzle.toyrobot.model.Report;
import com.puzzle.toyrobot.model.Robot;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MoveCommand extends Command {
    private final Logger log = LoggerFactory.getLogger(MoveCommand.class);

    @Override
    public void execute(Robot robot, Report report) {
        if (!robot.isOnTable()) {
            log.debug("Move command ignored");
        } else {
            switch (robot.getCardinalDirection()) {
                case NORTH:
                    if (robot.getYPosition() < Robot.MAX_POSITION) {
                        robot.increaseYPosition();
                        log.debug("The robot is moving");

                    } else {
                        log.debug("Move command ignored");
                    }
                    break;

                case SOUTH:
                    if (robot.getYPosition() > Robot.MIN_POSITION) {
                        robot.decreaseYPosition();
                        log.debug("The robot is moving");
                    } else {
                        log.debug("Move command ignored");
                    }
                    break;

                case EAST:
                    if (robot.getXPosition() < Robot.MAX_POSITION) {
                        robot.increaseXPosition();
                        log.debug("The robot is moving");
                    } else {
                        log.debug("Move command ignored");
                    }
                    break;

                case WEST:
                    if (robot.getXPosition() > Robot.MIN_POSITION) {
                        robot.decreaseXPosition();
                        log.debug("The robot is moving");
                    } else {
                        log.debug("Move command ignored");
                    }
                    break;
            }

        }
    }
}

PlaceCommand.java

package com.puzzle.toyrobot.model.command;

import com.puzzle.toyrobot.model.CardinalDirection;
import com.puzzle.toyrobot.model.Report;
import com.puzzle.toyrobot.model.Robot;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PlaceCommand extends Command {
    private final Logger log = LoggerFactory.getLogger(PlaceCommand.class);

    private String commandString;

    PlaceCommand(String commandString) {
        this.commandString = commandString;
    }

    @Override
    public void execute(Robot robot, Report report) {
        String placementArgs = StringUtils.substringAfter(commandString, CommandType.PLACE.name());
        String[] args = StringUtils.split(placementArgs, ",");

        Integer initialX = Integer.parseInt(args[0].trim());
        Integer initialY = Integer.parseInt(args[1].trim());

        if (initialX <= Robot.MAX_POSITION && initialX >= Robot.MIN_POSITION
                && initialY <= Robot.MAX_POSITION && initialY >= Robot.MIN_POSITION) {
            robot.setXPosition(initialX);
            robot.setYPosition(initialY);
            robot.setCardinalDirection(CardinalDirection.valueOf(args[2].trim()));
            log.debug("Robot is placed at " + robot.getCurrentStatus());
        } else {
            log.debug("Place command ignored");
        }
    }
}

ReportCommand.java

package com.puzzle.toyrobot.model.command;

import com.puzzle.toyrobot.model.Report;
import com.puzzle.toyrobot.model.Robot;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ReportCommand extends Command {
    private final Logger log = LoggerFactory.getLogger(ReportCommand.class);

    @Override
    public void execute(Robot robot, Report report) {
        if (!robot.isOnTable()) {
            log.debug("Missing Robot");
            report.addOutput("ROBOT MISSING");
        } else {
            report.addOutput(robot.getCurrentStatus());
        }
    }
}

RightCommand.java

package com.puzzle.toyrobot.model.command;

import com.puzzle.toyrobot.model.CardinalDirection;
import com.puzzle.toyrobot.model.Report;
import com.puzzle.toyrobot.model.Robot;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RightCommand extends Command {
    private final Logger log = LoggerFactory.getLogger(RightCommand.class);

    @Override
    public void execute(Robot robot, Report report) {
        if (!robot.isOnTable()) {
            log.debug("Right command ignored");
        } else {
            switch (robot.getCardinalDirection()) {
                case NORTH:
                    robot.setCardinalDirection(CardinalDirection.EAST);
                    break;
                case SOUTH:
                    robot.setCardinalDirection(CardinalDirection.WEST);
                    break;
                case EAST:
                    robot.setCardinalDirection(CardinalDirection.SOUTH);
                    break;
                case WEST:
                    robot.setCardinalDirection(CardinalDirection.NORTH);
                    break;
            }
            log.debug("The robot is rotating 90 degree to " + robot.getCardinalDirection());

        }
    }
}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.puzzle</groupId>
    <artifactId>toy-robot</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>toy-robot</name>
    <description>Toy Robot coding puzzle</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.7</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

and Finally a controller test

package com.puzzle.toyrobot.controller;

import com.puzzle.toyrobot.model.Report;
import com.puzzle.toyrobot.model.SimulationRound;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class RobotSimulationControllerTest {

    @Autowired
    private TestRestTemplate template;

    @Test
    public void simulationReportAsExpectedTest() {

        Report report = new Report();
        report.addOutput("0,1,NORTH");

        SimulationRound round = new SimulationRound();
        round.addCommand("PLACE 0,0,NORTH");
        round.addCommand("LEFT");
        round.addCommand("RIGHT");
        round.addCommand("MOVE");
        round.addCommand("REPORT");

        HttpEntity<Object> simulationRound = getHttpEntity(round);

        ResponseEntity<Report> resultAsset = template.postForEntity("/robot/simulation", simulationRound, Report.class);
        Assert.assertEquals(resultAsset.getBody(), report);
    }

    @Test
    public void missingRobotTest() {

        Report report = new Report();
        report.addOutput("ROBOT MISSING");

        SimulationRound round = new SimulationRound();
        round.addCommand("MOVE");
        round.addCommand("REPORT");

        HttpEntity<Object> simulationRound = getHttpEntity(round);
        ResponseEntity<Report> resultAsset = template.postForEntity("/robot/simulation", simulationRound, Report.class);
        Assert.assertEquals(resultAsset.getBody(), report);
    }


    @Test
    public void ignoringWrongCommandTest() {

        Report report = new Report();
        report.addOutput("0,0,WEST");

        SimulationRound round = new SimulationRound();
        round.addCommand("PLACE 0,0,WEST");
        round.addCommand("MOVEEEE");
        round.addCommand("REPORT");

        HttpEntity<Object> simulationRound = getHttpEntity(round);

        ResponseEntity<Report> resultAsset = template.postForEntity("/robot/simulation", simulationRound, Report.class);
        Assert.assertEquals(resultAsset.getBody(), report);
    }


    @Test
    public void ignoringCommandThatCausesFailTest() {

        Report report = new Report();
        report.addOutput("0,0,SOUTH");

        SimulationRound round = new SimulationRound();
        round.addCommand("PLACE 0,0,SOUTH");
        round.addCommand("MOVE");
        round.addCommand("REPORT");

        HttpEntity<Object> simulationRound = getHttpEntity(round);

        ResponseEntity<Report> resultAsset = template.postForEntity("/robot/simulation", simulationRound, Report.class);
        Assert.assertEquals(resultAsset.getBody(), report);
    }


    @Test
    public void simulationRoundWithoutReportTest() {

        Report report = new Report();

        SimulationRound round = new SimulationRound();
        round.addCommand("PLACE 1,2,EAST");
        round.addCommand("MOVE");
        round.addCommand("MOVE");
        round.addCommand("LEFT");

        HttpEntity<Object> simulationRound = getHttpEntity(round);

        ResponseEntity<Report> resultAsset = template.postForEntity("/robot/simulation", simulationRound, Report.class);
        Assert.assertEquals(resultAsset.getBody(), report);
    }



    private HttpEntity<Object> getHttpEntity(Object body) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        return new HttpEntity<>(body, headers);
    }


}

Notes on solution:

  • All commands will be ignored until a valid PLACE command.
  • The robot can be re-PLACEd at any time.
  • Any number of REPORT commands are allowed.
  • The REST-API request body is a Simulation Round object that contains a list of commands.
  • The REST-API response object is a Report object that contains a list of reports, which is the output of the REPORT command if any.
  • Many Integration tests are added.
\$\endgroup\$
0
\$\begingroup\$

I didn't examine all the classes. Here is my (partial) analysis:

REST API

Your API consists of one call. This is clearly not what was intended by the question (they did state that they regard the design of the API as important...)

First thing when designing a REST API is to decide what is(are) the entities that are represented by the API, and then decide what are the actions that can be done on these entities.

Although your API is superficially desgined around a "Simulation Round" entity, in essence it is designed to take and do commands. This is more inline with a design of RPC (remote proc call) API. It also returns OK status code no matter what's the input.

The entity should be the Robot. Placing the Robot is analog to creating a new entity. Moving and rotating the robot is updating its state and reporting is, naturally GETting its state . So

POST /robot should accept X,Y,F as body and return a unique Robot-id
PUT /robot/{id} should accept move and rotation commands and return either OK or 404 Not Found or some error indicating the command was ignored (perhaps 409 Conflict)
GET /robot/{id} should return the state of the Robot (or 404)
DELETE /robot/{id} will delete the specified Robot (or 404)
All non GET calls should return 400 (bad request) for invalid input. I mean input that cannot be parsed at all

... and just like that, you designed a service that supports multiple robots...

ENUM

You should harness the power of arguments to enum to eliminate need for switch statements:

  1. CommandType.java
    I would add an instance of Command as argument. This saves the switch statement in the command factory

  2. CardinalDirection.java
    I would add the result CardinalDirection of right and left rotation as parameters to the enum. This saves the switch statements in the commands. Better still, why not order the enum values by right rotation: NORTH, EAST, SOUTH, WEST and then you use the enum's ordinal() value to determine the result of right (+1) and left (-1) rotations (with wrapping of course)

\$\endgroup\$
  • \$\begingroup\$ First of all, Thank you for your answer and time! for the REST API part, I thought they want one API to simulate the movement. Anyway thanks again. \$\endgroup\$ – Aya Azzam Jul 3 '18 at 9:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.