Mars Rover, Locating refactoring 28/09/2016
In this series we're building the software of a Mars Rover, according to the following specifications. It allows us to practice the followings:
- Monolithic Repositories (MonoRepo)
- Command / Query Responsibility Segregation (CQRS)
- Event Sourcing (ES)
- Test Driven Development (TDD)
We've already developed the first use case about landing the rover on mars, and the second one about driving it. We're now developing the last one, requesting its location:
Mars rover will be requested to give its current location (
x
andy
coordinates and the orientation).
In this article we're going to create the locating logic:
git checkout 5-location
Location
Our LocateRover
command object relies on a FindLatestLocation
service. They
both currently return an array containing the coordinates and orientation of
our rover. Since FindLatestLocation
is an interface, we can't control what's
being actually returned... This could be fixed by specifying a Location
object as a return type, and it would make things more explicit.
Since Our Location
object will contain Coordinates
and Orientation
, we
might want to create it in the navigation
packages, where those two other
objects are alreay:
cd packages/navigation
We can now start writing Location
's test:
vendor/bin/phpspec describe 'MarsRover\Navigation\Location'
This should have bootstrapped the following
spec/MarsRover/Navigation/LocationSpec.php
file:
<?php
namespace spec\MarsRover\Navigation;
use MarsRover\Navigation\Location;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class LocationSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType(Location::class);
}
}
We can then edit it to specify that it should contain Coordinates
and
Orientation
:
<?php
namespace spec\MarsRover\Navigation;
use MarsRover\Navigation\Coordinates;
use MarsRover\Navigation\Orientation;
use PhpSpec\ObjectBehavior;
class LocationSpec extends ObjectBehavior
{
const X = 23;
const Y = 42;
const ORIENTATION = Orientation::NORTH;
function it_has_coordinates()
{
$coordinates = new Coordinates(self::X, self::Y);
$orientation = new Orientation(self::ORIENTATION);
$this->beConstructedWith($coordinates, $orientation);
$this->getCoordinates()->shouldBe($coordinates);
}
function it_has_orientation()
{
$coordinates = new Coordinates(self::X, self::Y);
$orientation = new Orientation(self::ORIENTATION);
$this->beConstructedWith($coordinates, $orientation);
$this->getOrientation()->shouldBe($orientation);
}
}
That sounds simple enough, we can run the tests:
vendor/bin/phpspec run
And of course they fail because Location
doesn't exist yet. to help us write
it, phpspec bootstrapped the following src/MarsRover/Navigation/Location.php
file:
<?php
namespace MarsRover\Navigation;
class Location
{
private $coordinates;
private $orientation;
public function __construct(Coordinates $coordinates, Orientation $orientation)
{
$this->coordinates = $coordinates;
$this->orientation = $orientation;
}
public function getCoordinates()
{
}
public function getOrientation()
{
}
}
Let's complete it:
<?php
namespace MarsRover\Navigation;
class Location
{
private $coordinates;
private $orientation;
public function __construct(
Coordinates $coordinates,
Orientation $orientation
) {
$this->coordinates = $coordinates;
$this->orientation = $orientation;
}
public function getCoordinates() : Coordinates
{
return $this->coordinates;
}
public function getOrientation() : Orientation
{
return $this->orientation;
}
}
This should be enough to make our tests pass:
vendor/bin/phpspec run
All green! We can now commit our work:
git add -A
git commit -m '5: Created Location'
Refactoring LandRover
This Location
value object looks great! Why didn't we create it in the first
place? That'll be pragmatism for you: don't create something you might need in
the future, create something you need now. But now that's it's here, we can
refactor LocateRover
to use it.
First let's update its test:
<?php
namespace spec\MarsRover\Navigation;
use MarsRover\Navigation\Location;
use MarsRover\Navigation\Orientation;
use PhpSpec\ObjectBehavior;
class LandRoverSpec extends ObjectBehavior
{
const X = 23;
const Y = 42;
const ORIENTATION = Orientation::NORTH;
function it_has_location()
{
$this->beConstructedWith(
self::X,
self::Y,
self::ORIENTATION
);
$location = $this->getLocation();
$location->shouldHaveType(Location::class);
$coordinates = $location->getCoordinates();
$coordinates->getX()->shouldBe(self::X);
$coordinates->getY()->shouldBe(self::Y);
$location->getOrientation()->get()->shouldBe(self::ORIENTATION);
}
}
Then its code:
<?php
namespace MarsRover\Navigation;
class LandRover
{
private $location;
public function __construct($x, $y, $orientation)
{
$this->location = new Location(
new Coordinates($x, $y),
new Orientation($orientation)
);
}
public function getLocation() : Location
{
return $this->location;
}
}
And finally LandRoverHandler
:
<?php
namespace MarsRover\Navigation;
use MarsRover\EventSourcing\{
AnEventHappened,
EventStore
};
class LandRoverHandler
{
private $anEventHappened;
private $eventStore;
public function __construct(
AnEventHappened $anEventHappened,
EventStore $eventStore
) {
$this->anEventHappened = $anEventHappened;
$this->eventStore = $eventStore;
}
public function handle(LandRover $landRover)
{
$location = $landRover->getLocation();
$coordinates = $location->getCoordinates();
$orientation = $location->getOrientation();
$roverLanded = $this->anEventHappened->justNow(Events::ROVER_LANDED, [
'x' => $coordinates->getX(),
'y' => $coordinates->getY(),
'orientation' => $orientation->get(),
]);
$this->eventStore->log($roverLanded);
}
}
Let's check the tests:
vendor/bin/phpspec run
All green! That should be enough to commit:
git add -A
git commit -m '5: Used Location in LandRover'
Conclusion
While we've been playing with the notion of Location
since the very first
use case, it's only now that we really need it that we created it.
It encapsulates X and Y coordinates as well as an orientation.
What's next?
Location
is currently in the navigation
package, but we also need it in
the location
package... To fix this we have the following solutions:
- add
navigation
as a dependency oflocation
- merge together
navigation
andlocation
- create a new
geolocation
package, withLocation
,Coordinates
andOrientation
Since we want to keep navigation
and location
separate, we'll opt for the
third option and create this new package in the next article.