In this post, I will elaborate the code structure a bit. This may help in getting hold of the design I have chosen.
To help us get the context, here’s a summary of the Use-Case we are targeting:
A hotel employs a number of lifts/elevators. A Controller coordinates their movement. A lift’s carriage gets its inputs (press on a button) either from a passenger waiting at floor’s lobby or from a passenger inside it, who chooses the floor she wants to go to, using the panel mounted inside.
Here’s is a block diagram showing for components that our model is using.
Implementation
We will form the right solution for the Use-Case (outlined at the beginning), step-by-step.
A carriage is represented as an Actor:
class LiftCarriageWithMovingState (val movementHWIndicator: ActorRef) extends Actor
with LoggingFSM[LiftState,LiftData]
with ActorLogging
{ // …
}
|
The construction parameter movementHWIndicator is the actor which represents the hardware circuit associated the carriage which indicates end of movement (i.e., arrival at a floor). We are representing that as a trait:
trait MovingStateSimulator extends Actor with ActorLogging {
// ..
}
|
So, at the time of construction, LiftCarriageWithMovingState receives an ActorRef which refers to an instance of MovingStateSimulator.
The states that the carriage can be in, are enumerated as follows:
sealed trait LiftState
object PoweredOff extends LiftState
object PoweredOn extends LiftState
object Ready extends LiftState
object Waiting extends LiftState
object Moving extends LiftState
object Stopped extends LiftState
|
A vector named pendingPassengerRequests holds the request for movement that have reached the carriage so far. The head of pendingPassengerRequests always represents the next floor to stop.
private var pendingPassengerRequests: Vector[NextStop] = Vector.empty
|
Every stop is represented as a tuple of floor’s number and the reason why it is moving to it:
case class NextStop(floorID: Int, purposeOfMovement: PurposeOfMovement)
object PurposeOfMovement extends Enumeration {
type PurposeOfMovement = Value
val ToWelcomeInAnWaitingPassenger, ToAllowATransportedPassengerAlight = Value
}
|
After it is switched on, every carriage is at the ground floor and is in Ready state.
While in the Moving state, the carriage responds to three events:
when (Moving) {
case Event(ReachedFloor(currentStop),_) =>
currentFloorID = pendingPassengerRequests.head.floorID
pendingPassengerRequests = pendingPassengerRequests.tail
goto (Stopped)
case Event(PassengerIsWaitingAt(floorID),_) =>
this.pendingPassengerRequests = accumulateWaitingRequest(floorID)
stay
case Event(PassengerRequestsATransportTo(floorIDs),_) =>
this.pendingPassengerRequests = accumulateTransportRequest(floorIDs)
stay
}
|
While in the Stopped state, the carriage waits for some time for the door to open, to let the passengers come in or go out, and then for it to close. If there is no pending requests, it goes back to Ready state:
private val actWhenStoppedForLongEnough: StateFunction = {
case Event(StateTimeout, _) =>
if (this.pendingPassengerRequests isEmpty) {
log.debug("Stopped.timeout, No pending passenger requests")
goto (Ready)
}
else {
log.debug( s"Stopped.timeout, moving to floor:( ${this.pendingPassengerRequests.head} )")
movementHWIndicator ! InformMeOnReaching(
this.currentFloorID,
this.pendingPassengerRequests.head)
goto(Moving)
}
}
|
Whenever pendingPassengerRequests is empty, the carriage is in the Stopped or Ready state and is stationary at the last floor it has reached.
Simulation of consumption of time while Moving
When the carriage is moving, it must expend time doing that. Moreover, while it is expending time, asynchronous events are possible to arrive (from the Controller or the Button Panel: both actors in their own rights). Because movementHWIndicator is an Actor, it is easy to model the asynchronicity by generating its signal using a Scheduler, after a delay.
Because the Scheduler runs on ActorSystem’s scheduler thread, it becomes messy when we want to test behaviour of LiftCarriageWithMovingState together with a MovingStateSimulator. I had to mock the behaviour of MovingStateSimulator. However, Instead of mocking it using a framework, I have decided to use a do-nothing Actor to construct a LiftCarriageWithMovingState. Its behaviour is much like that of an echo Actor:
trait MovingStateSimulator extends Actor with ActorLogging {
case class SpentTimeToReach(nextStop: NextStop, carriageToBeInformed: ActorRef)
val timeToReachNextFloor: FiniteDuration = 1000 millis // up or down, same time taken
def simulateMovementTo(
fromFloorID: Int,
nextStop: NextStop): Unit
override def receive: Receive = {
case InformMeOnReaching(fromFloorID,nextStop) =>
if (fromFloorID == nextStop.floorID) // Just a regular edge-case check
sender ! ReachedFloor(nextStop)
else
simulateMovementTo(fromFloorID,nextStop)
case SpentTimeToReach(nextStop,carriageToBeInformed) =>
log.debug(s"Informing ${carriageToBeInformed} after reaching the floor.")
carriageToBeInformed ! ReachedFloor(nextStop)
}
}
object DefaultMovingStateSimulatorActor extends MovingStateSimulator
with ActorLogging {
override def simulateMovementTo(
fromFloorID: Int,
toNextStop: NextStop) = {
// Immediate response, no time consumption in moving
sender ! ReachedFloor(toNextStop)
}
}
|
Finally, we have a Controller and a InlaidButtonPanel to complete the story:
class InlaidButtonPanel (
panelID: Int, attachedToCarriage: ActorRef, val noOfFloors: Int = 10)
extends Actor
with ActorLogging{ // ..
}
class LiftController (carriages: Vector[ActorRef]) extends Actor
with LoggingFSM[LiftState,LiftData]
with ActorLogging { // ..
}
|
Remember that a Carriage receives the instruction to pick a passenger from the Controller and to drop a passenger, from the button panel.
The following is a sample output log of a hotel having two lifts (enumerated 0 and 1) available. The Driver code emulates a instruction sequence of
A passenger presses a button at the lobby of floor 2
A passenger presses a button at the lobby of floor 4
Controller checks with Carriage(1), at which floor it is now
// Carriages reach floor 2 and 4 after some time...
Passenger inside Carriage[0]wants to go to floor 5, presses button
2 Passengers inside Carriage[1]; one wants to go to 6 and the other, to 2; press buttons
Controller checks with Carriage(0), at which floor it is now
Controller checks with Carriage(1), at which floor it is now
Controller checks with Carriage(0), at which floor it is now
Controller checks with Carriage(1), at which floor it is now
|
The output (pruned for space and readability):
While writing this blog, I have read a number of blogs on the same / similar topic. Some of them which I enjoyed reading were:
- A detailed description of an Akka-based application, highlighting Unit and Integration testing by Felipe Fernandez at Codurance
No comments:
Post a Comment