DDD perspective
I'd like to start this example from DDD perspective. A good place to start is to look for a consistency boundaries during command (as opposed to query) execution. As a result I come up with an aggregates, main DDD building blocks.
In your first use case, checkin, a command is to reserve a suitable slot. A consistency boundary is that a slot capacity should not be exceeded. Hence an obvious aggregate is a Slot. And it's important that at least in this use case we don't need a Parking lot aggregate, since its state isn't changed. So here is an application service for current use case. It serves as an environment staging place. All repositories, external api client, etc are injected here:
class CheckInApplication
{
private $slotRepository;
private $locker;
public function act(Vehicle $vehicle)
{
$this->locker->lock();
if (is_null($slot = $this->slotRepository->getSuitable($vehicle))) {
$this->locker->unlock();
return new NoVacantSpaceResponse();
}
$this->slotRepository->save($slot->reserve($vehicle));
$this->locker->unlock();
return SlotReservedResponse();
}
}
And here is how a slot reserving itself could look like:
class Slot
{
private $id;
private $capacity;
private $stays;
public function __construct(SlotId $id, Capacity $capacity, Stays $stays)
{
$this->id = $id;
$this->capacity = $capacity;
$this->stays = $stays;
}
public function reserve(Vehicle $vehicle)
{
if ($this->capacity < $vehicle->capacity()) {
throw new Exception('Vacant space is not enough');
}
$this->capacity = $this->capacity - $vehicle->capacity();
$this->stays->add(new Stay($this->vehicle, new DateTime('now')));
}
}
Slot check its integrity rules, that it can accommodate a vehicle, and passes on to Stays object. It is in turn is responsible for holding domain knowledge of Slot being able to accommodate no more than two vehicles:
class Stays
{
private $stays;
public function __construct(array $stays)
{
$this->stays = $stays;
}
public function add(Stay $stay)
{
if (sizeof($this->stays) > 1) {
throw new Exception('Only two cars per slot allowed');
}
$this->stays[] = $stay;
}
}
You can also add a new Stay
to Stays
, which holds the info about what vehicle occupied the slot and when. It will come in handy a bit later.
I've factored two separate use cases from your checkout scenario. So my second one is charging. After all, no one's gonna let you go until you get charged. Here is an application service:
class ChargeVehicleApplicationService
{
private $repository;
public function __construct(SlotRepository $repository)
{
$this->repository = $repository;
}
public function act(Vehicle $vehicle, SlotId $slotId)
{
try {
return new ChargeResponse($this->repository->getById($slotId)->charge($vehicle));
} catch (Exception $e) {
return ErrorResponse($e->getMessage());
}
}
}
Slot
class is updated with new method:
public function charge(Vehicle $vehicle)
{
$this->stays->get($vehicle)->charge();
}
I think it's a good idea that Stay
class calculates the charge itself. After all, it is a data owner for an algorithm logic. Here is it is (Stay
class):
public function charge()
{
return (DateTime('now') - $this->arrivedAt) * $this->vehicle->capacity();
}
And finally the third use case, releasing the vehicle (in real life I would stick to a single term, but now since I'm writing code on the fly I might be a bit fickle). As always, here is an application service:
class CheckoutVehicleApplicationService
{
private $repository;
public function __construct(SlotRepository $repository)
{
$this->repository = $repository;
}
public function act(Vehicle $vehicle, SlotId $slotId)
{
return
$this->repository
->save(
$this->repository
->getById($slotId)
->releaseFrom($vehicle)
)
;
}
}
Here is a Slot
's new functionality:
public function releaseFrom(Vehicle $vehicle)
{
$this->capacity = $this->capacity + $vehicle->capacity();
$this->stays->releaseFrom($vehicle);
}
And Stay
's method just removes the corresponding Stay
:
public function releaseFrom(Vehicle $vehicle)
{
$this->stays =
array_filter(
$this->stays,
function (Stay $stay) use ($vehicle) {
return !$stay->containsVehicle($vehicle);
}
);
}
So although this model and code are not very well thought out, but it shows that with aggregate concept in mind and object-thinking in heart one can come up with quite even responsibilities spreading among domain objects. And this is good, since every object is quite simple.
RDD perspective
This approach implies a bit different accents, though has the same fundamental OO principles. And it can be combined with DDD in any way you'd like. So when employing this technique, I usually start with a design story. What is your app is all about? You're modelling a parking lot. Your software is required to define (by whom?) if there is some vacant room for a vehicle, and if there is, find a proper slot on checkin. On checkout a vehicle should be charged (by whom?) a fee based on vehicle size and duration of stay.
The next step I take is identifying candidate objects and their responsibilities. Let's consider a checkin user case. I have a parking lot -- it holds data about slots. Hence it's a good fit for answering the question about a vacant space and for finding a suitable slot. Would it implement this task for itself? I don't think so. More probably than not parking lot will collaborate with slots, who actually are aware of the fact if they are vacant or not, and if they are, then how much space left. If there is some vacant slot, it can accept a vehicle. So I have two objects with one responsibility each.
Technically, there are usually no repositories, and each object is in charge of its responsibility. For example, saving an object is considered to be that object's responsibility. The same goes for displaying. So the code usually look really odd for most OO programmers. Since there are already plenty of code here, I just leave a link of how this could look like.