I implement a working version of the Lift Kata
Can you provide me some tips to improve my code?
I am not very proud of the handleLiftWithOpenDoors
method that have a side effect on calls
: as I am in a stream().map()
function, I want the lambda to be pure, with no side effects. Do you think of something elegant to solve this problem?
public class LiftSystem {
private final List<Integer> floors;
private final Map<Integer, List<Call>> calls;
private List<Lift> lifts;
public LiftSystem(List<Integer> floors, List<Lift> lifts, List<Call> calls) {
this.floors = floors;
this.calls = calls.stream().collect(Collectors.groupingBy(Call::fromFloor));
this.lifts = lifts;
}
public List<Integer> getFloorsInDescendingOrder() {
List<Integer> shallowCopy = new ArrayList<>(floors);
Collections.reverse(shallowCopy);
return shallowCopy;
}
public List<Call> getCallsForFloor(int floor) {
return calls.getOrDefault(floor, Collections.emptyList());
}
public List<Lift> getLifts() {
return lifts;
}
public void tick() {
lifts = lifts.stream()
.map(lift -> {
List<Call> callsAtThisFloor = calls.getOrDefault(lift.floor(), Collections.emptyList());
if (lift.doorsOpen()) {
// FIXME handleLiftWithOpenDoors have a side effect on callsAtThisFloor
return handleLiftWithOpenDoors(lift, callsAtThisFloor);
} else {
return handleLiftWithClosedDoors(lift, callsAtThisFloor);
}
})
.collect(Collectors.toList());
}
private Lift handleLiftWithOpenDoors(Lift lift, List<Call> callsAtThisFloor) {
List<Integer> remainingRequests = lift.filterRequests(lift.floor());
if (!callsAtThisFloor.isEmpty()) {
Direction liftDirection = liftDirection(lift, remainingRequests).orElse(callsAtThisFloor.get(0).direction());
Map<Boolean, List<Call>> callsInLiftDirection = callsInLiftDirection(callsAtThisFloor, liftDirection);
remainingRequests.addAll(callsInLiftDirection.get(true).stream().map(Call::destinationFloor).toList());
// FIXME side effect : update the calls map ?
calls.put(lift.floor(), callsInLiftDirection.get(false));
}
return lift.closeDoors(remainingRequests);
}
private Lift handleLiftWithClosedDoors(Lift lift, List<Call> callsAtThisFloor) {
if (!lift.requests().isEmpty()) {
return handleLiftWithRequests(lift, callsAtThisFloor);
} else {
if (!callsAtThisFloor.isEmpty()) {
return lift.openDoors(lift.requests());
} else {
return handleWaitingCallsIfAny(lift);
}
}
}
private Lift handleWaitingCallsIfAny(Lift lift) {
// find the closest call if any
int floor = lift.floor();
for (int i = 1; i < Math.max(lift.floor(), floors.size() - 1 - lift.floor()); i++) {
if (isNotEmpty(calls.get(floor + i))) {
return lift.moveUp();
}
if (isNotEmpty(calls.get(floor - i))) {
return lift.moveDown();
}
}
return lift;
}
private Lift handleLiftWithRequests(Lift lift, List<Call> callsForFloor) {
if (lift.hasRequestForFloor(lift.floor())) {
return lift.openDoors(lift.filterRequests(lift.floor()));
} else {
Direction liftDirection = liftDirection(lift, lift.requests()).get();
if (hasCallsInLiftDirection(liftDirection, callsForFloor)) {
return lift.openDoors(lift.requests());
} else {
return liftDirection.equals(Direction.UP) ? lift.moveUp() : lift.moveDown();
}
}
}
private boolean hasCallsInLiftDirection(Direction liftDirection, List<Call> callsForFloor) {
return callsForFloor.stream().anyMatch(call -> call.direction().equals(liftDirection));
}
private static Map<Boolean, List<Call>> callsInLiftDirection(List<Call> callsForFloor, Direction liftDirection) {
return callsForFloor.stream()
.collect(Collectors.partitioningBy(call -> call.direction().equals(liftDirection)));
}
public boolean stillMoving() {
return calls.values().stream().anyMatch(Predicate.not(Collection::isEmpty))
|| lifts.stream().anyMatch(lift -> lift.doorsOpen() || !lift.requests().isEmpty());
}
private Optional<Direction> liftDirection(Lift lift, List<Integer> remainingRequests) {
if (remainingRequests.isEmpty()) {
return Optional.empty();
} else {
int destination = lift.closestDestination();
return Optional.of(destination > lift.floor() ? Direction.UP : Direction.DOWN);
}
}
private boolean isNotEmpty(Collection<?> collection) {
return collection != null && !collection.isEmpty();
}
}
public record Lift(String id, int floor, List<Integer> requests, boolean doorsOpen) {
public boolean hasRequestForFloor(int floor) {
return this.requests.contains(floor);
}
public Lift openDoors(List<Integer> requests) {
return new Lift(id, floor, requests, true);
}
public Lift closeDoors(List<Integer> requests) {
return new Lift(id, floor, requests, false);
}
public int closestDestination() {
return requests
.stream()
.min(Comparator.comparingInt(i -> Math.abs(i - floor)))
.orElse(floor);
}
public Lift moveUp() {
return new Lift(id, floor + 1, requests, doorsOpen);
}
public Lift moveDown() {
return new Lift(id, floor - 1, requests, doorsOpen);
}
public List<Integer> filterRequests(int unwantedFloor) {
return requests.stream()
.filter(request -> request != unwantedFloor)
.collect(Collectors.toList());
}
}
public enum Direction {
UP, DOWN;
}
public record Call (int fromFloor, int destinationFloor, Direction direction) {
}