Mars Rover, Landing event 20/07/2016
In this series we're going to build the software of a Mars Rover, according to the following specifications. It will allow us to practice the followings:
- Monolithic Repositories (MonoRepo)
- Command / Query Responsibility Segregation (CQRS)
- Event Sourcing (ES)
- Test Driven Development (TDD)
Previously we've created a
navigation package, and in it
a LandRover
class that
validates input parameters for our
first use case:
Mars Rovers need first to be landed at a given position. A position is composed of coordinates (
x
andy
, which are both integers) and an orientation (a string being one ofnorth
,east
,west
orsouth
).
We've then refactored it to extract coordinates and orientation in their own classes.
In this article we're going to create the actual landing logic, using Event Sourcing:
cd packages/navigation
git checkout 2-landing
LandRoverHandler
Following our Command Bus twist,
we're now going to create the LandRoverHandler
class that's going to take
care of the actual logic associated to the LandRover
use case. We're starting
by bootstraping the test class:
vendor/bin/phpspec describe 'MarsRover\Navigation\LandRoverHandler'
this should create the following
spec/MarsRover/Navigation/LandRoverHandlerSpec.php
file:
<?php
namespace spec\MarsRover\Navigation;
use MarsRover\Navigation\LandRoverHandler;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class LandRoverHandlerSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType(LandRoverHandler::class);
}
}
Event Sourcing is
all about recording significant actions. Landing a rover seems significant
enough, so that's what LandRoverHandler
should do:
<?php
namespace spec\MarsRover\Navigation;
use MarsRover\Navigation\AnEventHappened;
use MarsRover\Navigation\Event;
use MarsRover\Navigation\EventStore;
use MarsRover\Navigation\LandRover;
use MarsRover\Navigation\Orientation;
use PhpSpec\ObjectBehavior;
class LandRoverHandlerSpec extends ObjectBehavior
{
const X = 23;
const Y = 42;
const ORIENTATION = Orientation::NORTH;
const EVENT_NAME = 'rover_landed';
const EVENT_DATA = [
'x' => self::X,
'y' => self::Y,
'orientation' => self::ORIENTATION,
];
function it_lands_a_rover_at_given_location(
AnEventHappened $anEventHappened,
Event $roverLanded,
EventStore $eventStore
) {
$this->beConstructedWith($anEventHappened, $eventStore);
$landRover = new LandRover(
self::X,
self::Y,
self::ORIENTATION
);
$anEventHappened->justNow(
self::EVENT_NAME,
self::EVENT_DATA
)->willReturn($roverLanded);
$eventStore->log($roverLanded)->shouldBeCalled();
$this->handle($landRover);
}
}
In this test, we rely on:
Event
, a Data Transfer Object (DTO) that contains the name and the dataAnEventHappened
, which is actually anEvent
factoryEventStore
, a service responsible for "logging"Event
s
We could have done without the factory and create directly Event
in
LandRoverHandler
, but then we'd have no way to check in our tests that it
was constructed with the right data.
Those classes don't exist yet, we've made them up to build a coherent story. Let's run the tests:
vendor/bin/phpspec run
They fail because of the missing classes... But phpspec bootstrapped them for us!
It created the src/MarsRover/Navigation/AnEventHappened.php
file:
<?php
namespace MarsRover\Navigation;
interface AnEventHappened
{
public function justNow($argument1, $argument2);
}
It also created the src/MarsRover/Navigation/Event.php
file:
<?php
namespace MarsRover\Navigation;
interface Event
{
}
And it created the src/MarsRover/Navigation/EventStore.php
file:
<?php
namespace MarsRover\Navigation;
interface EventStore
{
public function log($argument1);
}
As we can see, when we reference a non existence class, phpspec generates an interface for us. It also generates methods, if we've described method calls in our test.
For now we'll leave them like this, and have a look at the generated
src/MarsRover/Navigation/LandRoverHandler.php
file:
<?php
namespace MarsRover\Navigation;
class LandRoverHandler
{
private $anEventHappened;
private $eventStore;
public function __construct(AnEventHappened $anEventHappened, EventStore $eventStore)
{
$this->anEventHappened = $anEventHappened;
$this->eventStore = $eventStore;
}
public function handle(LandRover $landRover)
{
}
}
Thanks to the SpecGen extension, phpspec was able to detect Dependency Injection, and bootstrapped a constructor with an attribute initialization for us. How nice!
We'll just need to complete the handle
method:
<?php
namespace MarsRover\Navigation;
class LandRoverHandler
{
const EVENT_NAME = 'rover_landed';
private $anEventHappened;
private $eventStore;
public function __construct(
AnEventHappened $anEventHappened,
EventStore $eventStore
) {
$this->anEventHappened = $anEventHappened;
$this->eventStore = $eventStore;
}
public function handle(LandRover $landRover)
{
$roverLanded = $this->anEventHappened->justNow(self::EVENT_NAME, [
'x' => $landRover->getCoordinates()->getX(),
'y' => $landRover->getCoordinates()->getY(),
'orientation' => $landRover->getOrientation()->get(),
]);
$this->eventStore->log($roverLanded);
}
}
Overall, the code looks very similar to the test. Let's run them:
vendor/bin/phpspec run
All green! We might want to use the same event name in both the code and the
test, so let's create a src/MarsRover/Navigation/Events.php
file:
<?php
namespace MarsRover\Navigation;
class Events
{
const ROVER_LANDED = 'rover_landed';
}
Then use it in the code:
<?php
namespace MarsRover\Navigation;
class LandRoverHandler
{
private $anEventHappened;
private $eventStore;
public function __construct(
AnEventHappened $anEventHappened,
EventStore $eventStore
) {
$this->anEventHappened = $anEventHappened;
$this->eventStore = $eventStore;
}
public function handle(LandRover $landRover)
{
$roverLanded = $this->anEventHappened->justNow(Events::ROVER_LANDED, [
'x' => $landRover->getCoordinates()->getX(),
'y' => $landRover->getCoordinates()->getY(),
'orientation' => $landRover->getOrientation()->get(),
]);
$this->eventStore->log($roverLanded);
}
}
and finally in the test:
<?php
namespace spec\MarsRover\Navigation;
use MarsRover\Navigation\AnEventHappened;
use MarsRover\Navigation\Event;
use MarsRover\Navigation\Events;
use MarsRover\Navigation\EventStore;
use MarsRover\Navigation\LandRover;
use MarsRover\Navigation\Orientation;
use PhpSpec\ObjectBehavior;
class LandRoverHandlerSpec extends ObjectBehavior
{
const X = 23;
const Y = 42;
const ORIENTATION = Orientation::NORTH;
const EVENT_NAME = Events::ROVER_LANDED;
const EVENT_DATA = [
'x' => self::X,
'y' => self::Y,
'orientation' => self::ORIENTATION,
];
function it_lands_a_rover_at_given_location(
AnEventHappened $anEventHappened,
Event $roverLanded,
EventStore $eventStore
) {
$this->beConstructedWith($anEventHappened, $eventStore);
$landRover = new LandRover(
self::X,
self::Y,
self::ORIENTATION
);
$anEventHappened->justNow(
self::EVENT_NAME,
self::EVENT_DATA
)->willReturn($roverLanded);
$eventStore->log($roverLanded)->shouldBeCalled();
$this->handle($landRover);
}
}
We can run the tests one last time:
vendor/bin/phpspec run
Still green! We can commit our work:
git add -A
git commit -m '2: Created LandRoverHandler'
git checkout master
git merge --no-ff 2-landing
Conclusion
With Event Sourcing, the logic associated to our "Landing a Rover on Mars" use case is quite simple: we just record it as an event.
What's next?
In the next article, we'll extract Event Sourcing logic from the navigation
package and put it in its own event-sourcing
one.