Low-Level Design (Object-Oriented Design) Module 8 – Code quality under time pressure
Weak LLD answers: how to spot and fix them
Over-engineering, under-modelling, pattern stuffing, hidden global state and ignored failure paths in LLD answers, measured on three elevator answers.
What you will learn
- Recognise over-engineering, under-modelling and pattern stuffing in an LLD answer
- Explain how each weakness hurts correctness, the next change or the tests
- Measure what a follow-up requirement costs an answer
- Find hidden global state and ignored failure paths in a design
- Rewrite an over-engineered answer into a balanced one in safe steps
- Evaluate an answer against a five-criterion rubric
Before you start
What you’ll learn · Before you start
What you will learn
- Recognise over-engineering, under-modelling and pattern stuffing in an LLD answer
- Explain how each weakness hurts correctness, the next change or the tests
- Measure what a follow-up requirement costs an answer
- Find hidden global state and ignored failure paths in a design
- Rewrite an over-engineered answer into a balanced one in safe steps
- Evaluate an answer against a five-criterion rubric
Before you start
On this page
A weak low-level design (LLD) answer usually runs. It fails when the interviewer asks for one more feature: the change spreads through the code, breaks a test that passed, or cannot be made without starting again. Five weaknesses cause most of that: over-engineering, under-modelling, pattern stuffing, hidden global state and ignored failure paths. Each hurts correctness, the next change or the tests, and each is fixed by making the answer clearer, often smaller. This lesson names the five, then measures three answers to one elevator prompt, over-engineered, under-modelled and balanced, against the same follow-up. The balanced answer absorbed it in 13 lines of code; the under-modelled one changed five of its seven functions.
Five weaknesses, and what each one costs
- Over-engineering is structure the prompt never asked for: an interface with one implementation, a factory for a plain object, a layer that only passes calls on. Every follow-up must then travel through each layer. Google’s engineering-practices guide calls it code made more generic than it needs to be.
- Under-modelling is too little structure: one class with dictionaries of dictionaries, states as strings, and input parsing mixed with the rules. The data cannot say what it means, so a new idea changes the code wherever the data is read.
- Pattern stuffing is naming design patterns for their own sake, such as a Singleton “because a building has one elevator system”. A pattern earns its place only by solving a problem the prompt states.
- Hidden global state is a singleton or a module-level mutable object that any code can reach. A test leaves state behind, and whether the next test passes depends on the order in which they run.
- Ignored failure paths means only the happy path was designed. A call from a floor that does not exist, a button pressed twice or a payment that fails after a seat was held leaves a state nobody chose.
A balanced answer gives each idea of the prompt a type and each rule one home, adds a seam only where a stated change will land, refuses impossible requests with a reason, and builds its objects where a test can build them too. Three refactorings from Martin Fowler’s catalogue shrink an over-engineered answer. Collapse Hierarchy merges an interface into its only implementation. Inline Class folds a class that does too little into its user. Remove Middle Man lets callers skip a class that only forwards.
Three answers to one elevator prompt
The prompt: floors 0 to 9 and three cars, A, B and C. People on a floor press up or down (a hall call); people
inside a car press a floor (a car call). Each tick, a car moves one floor towards its next stop or stops there. All
three answers read commands such as CALL 5 UP, PRESS A 8, TICK 3 and STATUS, and pass the same three base
scenarios.
The follow-up is the same for all three: CANCEL 6 UP means that the person who made that hall call took the
stairs, but a passenger inside the car who pressed 6 must still get out at 6. compare.py runs the base scenarios
on each answer and on its follow-up version, and the follow-up scenarios on the follow-up versions, loading each
answer afresh for every scenario:
answers/compare.py
"""Runs the three answers on the same scenarios, before and after the CANCEL follow-up, and measures what the
follow-up changed in each. Each scenario loads its answer afresh, as if it were a run of its own."""
import ast
import difflib
import importlib
from pathlib import Path
import scenarios
HERE = Path(__file__).resolve().parent
ANSWERS = {"A": "answer_a", "B": "answer_b", "C": "answer_c"}
def passed(module_name, scenario_set):
count = 0
for commands, expected in scenario_set.values():
module = importlib.reload(importlib.import_module(module_name))
count += module.run(commands) == expected
return f"{count}/{len(scenario_set)}"
def code_lines(module_name):
"""The lines of a file that are code: no blank lines, comments or docstrings."""
text = (HERE / f"{module_name}.py").read_text()
docstrings = set()
for node in ast.walk(ast.parse(text)):
if isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef)) and ast.get_docstring(node):
docstrings.update(range(node.body[0].lineno, node.body[0].end_lineno + 1))
return [line for number, line in enumerate(text.splitlines(), 1)
if line.strip() and not line.strip().startswith("#") and number not in docstrings]
def changed_lines(before, after):
"""Code lines added or removed between two versions."""
diff = difflib.unified_diff(code_lines(before), code_lines(after), lineterm="", n=0)
return sum(1 for line in diff if line[:1] in "+-" and not line.startswith(("+++", "---")))
def functions(module_name):
"""Every function and method, by its qualified name, with its code."""
text = (HERE / f"{module_name}.py").read_text()
found = {}
def visit(node, prefix):
for child in ast.iter_child_nodes(node):
if isinstance(child, ast.ClassDef):
visit(child, f"{prefix}{child.name}.")
elif isinstance(child, ast.FunctionDef):
found[prefix + child.name] = ast.get_source_segment(text, child)
visit(ast.parse(text), "")
return found
def classes(module_name):
tree = ast.parse((HERE / f"{module_name}.py").read_text())
return sum(isinstance(node, ast.ClassDef) for node in ast.walk(tree))
def row(label, values):
print(f" {label:<28}" + "".join(f"{value:>8}" for value in values))
def report():
print("A over-engineered, B under-modelled, C balanced")
print()
print(f"{'As written':<30}" + "".join(f"{key:>8}" for key in ANSWERS))
row("classes", [classes(m) for m in ANSWERS.values()])
row("lines of code", [len(code_lines(m)) for m in ANSWERS.values()])
row("base scenarios passed", [passed(m, scenarios.BASE) for m in ANSWERS.values()])
changes = {}
for key, before in ANSWERS.items():
after = before + "_cancel"
old, new = functions(before), functions(after)
changes[key] = {
"base": passed(after, scenarios.BASE),
"follow-up": passed(after, scenarios.FOLLOW_UP),
"lines": changed_lines(before, after),
"changed": sorted(name for name in old if name in new and new[name] != old[name]),
"new": sorted(name for name in new if name not in old),
"existing": len(old),
}
print()
print(f"{'After the CANCEL follow-up':<30}" + "".join(f"{key:>8}" for key in ANSWERS))
row("base scenarios passed", [c["base"] for c in changes.values()])
row("follow-up scenarios passed", [c["follow-up"] for c in changes.values()])
row("code lines added or removed", [c["lines"] for c in changes.values()])
row("existing functions changed", [f"{len(c['changed'])}/{c['existing']}" for c in changes.values()])
row("new functions", [len(c["new"]) for c in changes.values()])
row("classes touched", [len({name.rpartition(".")[0] for name in c["changed"] + c["new"] if "." in name})
for c in changes.values()])
for key, c in changes.items():
print()
print(f"What the follow-up changed in {key}")
for name in sorted(c["changed"] + c["new"]):
print(f" {name:<34}{'new' if name in c['new'] else 'changed'}")
if __name__ == "__main__":
report() Output
A over-engineered, B under-modelled, C balanced As written A B C classes 25 1 5 lines of code 237 74 110 base scenarios passed 3/3 3/3 3/3 After the CANCEL follow-up A B C base scenarios passed 3/3 3/3 3/3 follow-up scenarios passed 3/3 3/3 3/3 code lines added or removed 22 37 13 existing functions changed 1/56 5/7 1/13 new functions 5 1 2 classes touched 6 1 2 What the follow-up changed in A CancelCommand.execute new CommandFactory.__init__ changed Elevator.remove_request new ElevatorController.cancel new ElevatorService.cancel_hall_call new IElevator.remove_request new What the follow-up changed in B Elevators.__init__ changed Elevators.call changed Elevators.cancel new Elevators.handle changed Elevators.status changed Elevators.tick changed What the follow-up changed in C Car.drop new ElevatorBank.cancel new run changed
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 compare.py
answers/scenarios.py
"""The scenarios every answer runs: command lines, each followed by the lines (marked >) it must print."""
def parse(transcript):
"""Turns a transcript into (commands, expected output lines)."""
commands, expected = [], []
for line in transcript.strip().splitlines():
line = line.strip()
if line.startswith(">"):
expected.append(line[1:].strip())
else:
commands.append(line)
return commands, expected
BASE = {
"one call, then a car call": parse("""
CALL 5 UP
> call 5 UP -> A
TICK 4
STATUS
> A at 4, stops [5]
> B at 0, idle
> C at 0, idle
TICK
> tick 5: A stops at 5
PRESS A 8
> press 8 in A
TICK 3
> tick 8: A stops at 8
STATUS
> A at 8, idle
> B at 0, idle
> C at 0, idle
"""),
"the direction rule picks the car": parse("""
PRESS A 9
> press 9 in A
TICK 2
CALL 6 UP
> call 6 UP -> A
CALL 4 DOWN
> call 4 DOWN -> B
CALL 1 UP
> call 1 UP -> B
TICK 3
> tick 3: B stops at 1
STATUS
> A at 5, stops [6, 9]
> B at 3, stops [4]
> C at 0, idle
TICK 4
> tick 6: A stops at 6
> tick 6: B stops at 4
> tick 9: A stops at 9
STATUS
> A at 9, idle
> B at 4, idle
> C at 0, idle
"""),
"a car turns round": parse("""
PRESS C 6
> press 6 in C
TICK 3
PRESS C 1
> press 1 in C
CALL 2 DOWN
> call 2 DOWN -> A
TICK 3
> tick 5: A stops at 2
> tick 6: C stops at 6
STATUS
> A at 2, idle
> B at 0, idle
> C at 6, stops [1]
TICK 5
> tick 11: C stops at 1
CALL 2 UP
> call 2 UP -> A
TICK
> tick 12: A stops at 2
STATUS
> A at 2, idle
> B at 0, idle
> C at 1, idle
"""),
}
# The follow-up: "CANCEL 6 UP: the person who made that hall call took the stairs. Remove the call from the car
# it was given to. A passenger inside that car who pressed the same floor must still get out there."
FOLLOW_UP = {
"cancel the only call": parse("""
CALL 7 DOWN
> call 7 DOWN -> A
TICK 3
CANCEL 7 DOWN
> cancel 7 DOWN (was A)
TICK 2
STATUS
> A at 3, idle
> B at 0, idle
> C at 0, idle
"""),
"a passenger still gets out": parse("""
PRESS B 6
> press 6 in B
TICK 2
CALL 6 UP
> call 6 UP -> B
CANCEL 6 UP
> cancel 6 UP (was B)
TICK 4
> tick 6: B stops at 6
STATUS
> A at 0, idle
> B at 6, idle
> C at 0, idle
"""),
"a call that is not there": parse("""
CANCEL 3 UP
> no call 3 UP to cancel
CALL 3 UP
> call 3 UP -> A
TICK 3
> tick 3: A stops at 3
CANCEL 3 UP
> no call 3 UP to cancel
STATUS
> A at 3, idle
> B at 0, idle
> C at 0, idle
"""),
} A · answers/answer_a.py
"""Answer A, over-engineered: an interface for each class, a factory for each object, layers that pass calls
on, a class for each command word and a singleton for the system. It works."""
from abc import ABC, abstractmethod
from enum import Enum
class Direction(Enum):
UP = "up"
DOWN = "down"
class ElevatorException(Exception):
pass
class InvalidRequestException(ElevatorException):
pass
class IRequest(ABC):
@abstractmethod
def get_floor(self): ...
class HallRequest(IRequest):
def __init__(self, floor, direction):
self._floor = floor
self._direction = direction
def get_floor(self):
return self._floor
def get_direction(self):
return self._direction
class CarRequest(IRequest):
def __init__(self, floor):
self._floor = floor
def get_floor(self):
return self._floor
class RequestFactory:
def create_hall_request(self, floor, direction):
return HallRequest(floor, direction)
def create_car_request(self, floor):
return CarRequest(floor)
class RequestValidator:
def __init__(self, floors):
self._floors = floors
def validate_floor(self, floor):
if not 0 <= floor < self._floors:
raise InvalidRequestException(f"no floor {floor}")
def validate_hall_request(self, request):
floor, direction = request.get_floor(), request.get_direction()
self.validate_floor(floor)
if (floor, direction) in ((0, Direction.DOWN), (self._floors - 1, Direction.UP)):
raise InvalidRequestException(f"no {direction.name} call from floor {floor}")
class IElevator(ABC):
@abstractmethod
def get_name(self): ...
@abstractmethod
def get_floor(self): ...
@abstractmethod
def get_direction(self): ...
@abstractmethod
def add_request(self, request): ...
@abstractmethod
def get_requests(self): ...
@abstractmethod
def step(self): ...
class Elevator(IElevator):
def __init__(self, name):
self._name = name
self._floor = 0
self._direction = None
self._requests = []
def get_name(self):
return self._name
def get_floor(self):
return self._floor
def get_direction(self):
return self._direction
def add_request(self, request):
self._requests.append(request)
def get_requests(self):
return list(self._requests)
def step(self):
stops = {request.get_floor() for request in self._requests}
if not stops:
self._direction = None
return None
if self._floor not in stops:
if self._direction is Direction.UP and max(stops) < self._floor:
self._direction = None
if self._direction is Direction.DOWN and min(stops) > self._floor:
self._direction = None
if self._direction is None:
nearest = min(stops, key=lambda stop: (abs(stop - self._floor), stop))
self._direction = Direction.UP if nearest > self._floor else Direction.DOWN
self._floor += 1 if self._direction is Direction.UP else -1
if self._floor not in stops:
return None
self._requests = [request for request in self._requests if request.get_floor() != self._floor]
if not self._requests:
self._direction = None
return f"{self._name} stops at {self._floor}"
class ElevatorFactory:
def create_elevator(self, name):
return Elevator(name)
class IDispatchStrategy(ABC):
@abstractmethod
def select_elevator(self, elevators, request): ...
class NearestElevatorStrategy(IDispatchStrategy):
def __init__(self, floors):
self._floors = floors
def select_elevator(self, elevators, request):
return min(elevators, key=lambda elevator: (self._cost(elevator, request), elevator.get_name()))
def _cost(self, elevator, request):
distance = abs(elevator.get_floor() - request.get_floor())
moving, wanted = elevator.get_direction(), request.get_direction()
if moving is None:
return distance
here, there = elevator.get_floor(), request.get_floor()
if moving is Direction.UP and wanted is Direction.UP and here <= there:
return distance
if moving is Direction.DOWN and wanted is Direction.DOWN and here >= there:
return distance
return distance + 2 * self._floors
class IElevatorObserver(ABC):
@abstractmethod
def notify(self, message): ...
class ConsoleElevatorObserver(IElevatorObserver):
def __init__(self):
self.messages = []
def notify(self, message):
self.messages.append(message)
class ElevatorRepository:
def __init__(self):
self._elevators = {}
def save(self, elevator):
self._elevators[elevator.get_name()] = elevator
def find_by_name(self, name):
if name not in self._elevators:
raise InvalidRequestException(f"no car {name}")
return self._elevators[name]
def find_all(self):
return [self._elevators[name] for name in sorted(self._elevators)]
class ElevatorService:
def __init__(self, repository, strategy, request_factory, validator, observer):
self._repository = repository
self._strategy = strategy
self._request_factory = request_factory
self._validator = validator
self._observer = observer
self._ticks = 0
def handle_hall_call(self, floor, direction):
request = self._request_factory.create_hall_request(floor, direction)
self._validator.validate_hall_request(request)
label = f"{floor} {direction.name}"
for elevator in self._repository.find_all():
for waiting in elevator.get_requests():
if isinstance(waiting, HallRequest) and waiting.get_floor() == floor \
and waiting.get_direction() is direction:
self._observer.notify(f"call {label} -> {elevator.get_name()} (already waiting)")
return
elevator = self._strategy.select_elevator(self._repository.find_all(), request)
elevator.add_request(request)
self._observer.notify(f"call {label} -> {elevator.get_name()}")
def handle_car_call(self, name, floor):
elevator = self._repository.find_by_name(name)
self._validator.validate_floor(floor)
elevator.add_request(self._request_factory.create_car_request(floor))
self._observer.notify(f"press {floor} in {name}")
def advance(self, count):
for _ in range(count):
self._ticks += 1
for elevator in self._repository.find_all():
event = elevator.step()
if event:
self._observer.notify(f"tick {self._ticks}: {event}")
def report_status(self):
for elevator in self._repository.find_all():
stops = sorted({request.get_floor() for request in elevator.get_requests()})
where = f"{elevator.get_name()} at {elevator.get_floor()}"
self._observer.notify(f"{where}, stops {stops}" if stops else f"{where}, idle")
class ElevatorController:
def __init__(self, service):
self._service = service
def call(self, floor, direction):
self._service.handle_hall_call(floor, direction)
def press(self, name, floor):
self._service.handle_car_call(name, floor)
def tick(self, count):
self._service.advance(count)
def status(self):
self._service.report_status()
class ICommand(ABC):
@abstractmethod
def execute(self, controller, args): ...
class CallCommand(ICommand):
def execute(self, controller, args):
controller.call(int(args[0]), Direction[args[1]])
class PressCommand(ICommand):
def execute(self, controller, args):
controller.press(args[0], int(args[1]))
class TickCommand(ICommand):
def execute(self, controller, args):
controller.tick(int(args[0]) if args else 1)
class StatusCommand(ICommand):
def execute(self, controller, args):
controller.status()
class CommandFactory:
def __init__(self):
self._commands = {
"CALL": CallCommand(),
"PRESS": PressCommand(),
"TICK": TickCommand(),
"STATUS": StatusCommand(),
}
def get_command(self, word):
return self._commands.get(word)
class ElevatorSystem:
"""A building has one elevator system, so this is a singleton."""
_instance = None
def __init__(self):
self.observer = ConsoleElevatorObserver()
repository = ElevatorRepository()
factory = ElevatorFactory()
for name in "ABC":
repository.save(factory.create_elevator(name))
strategy = NearestElevatorStrategy(10)
service = ElevatorService(repository, strategy, RequestFactory(), RequestValidator(10), self.observer)
self.controller = ElevatorController(service)
self.commands = CommandFactory()
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = ElevatorSystem()
return cls._instance
def run(lines):
system = ElevatorSystem.get_instance()
for line in lines:
word, *args = line.split()
command = system.commands.get_command(word)
if command is None:
system.observer.notify(f"unknown command {word}")
continue
try:
command.execute(system.controller, args)
except InvalidRequestException as error:
system.observer.notify(f"refused: {error}")
out = list(system.observer.messages)
system.observer.messages.clear()
return out A + CANCEL · answers/answer_a_cancel.py
"""Answer A after the CANCEL follow-up: a new method in the interface, the elevator, the service and the
controller, and a new command class with its registration."""
from abc import ABC, abstractmethod
from enum import Enum
class Direction(Enum):
UP = "up"
DOWN = "down"
class ElevatorException(Exception):
pass
class InvalidRequestException(ElevatorException):
pass
class IRequest(ABC):
@abstractmethod
def get_floor(self): ...
class HallRequest(IRequest):
def __init__(self, floor, direction):
self._floor = floor
self._direction = direction
def get_floor(self):
return self._floor
def get_direction(self):
return self._direction
class CarRequest(IRequest):
def __init__(self, floor):
self._floor = floor
def get_floor(self):
return self._floor
class RequestFactory:
def create_hall_request(self, floor, direction):
return HallRequest(floor, direction)
def create_car_request(self, floor):
return CarRequest(floor)
class RequestValidator:
def __init__(self, floors):
self._floors = floors
def validate_floor(self, floor):
if not 0 <= floor < self._floors:
raise InvalidRequestException(f"no floor {floor}")
def validate_hall_request(self, request):
floor, direction = request.get_floor(), request.get_direction()
self.validate_floor(floor)
if (floor, direction) in ((0, Direction.DOWN), (self._floors - 1, Direction.UP)):
raise InvalidRequestException(f"no {direction.name} call from floor {floor}")
class IElevator(ABC):
@abstractmethod
def get_name(self): ...
@abstractmethod
def get_floor(self): ...
@abstractmethod
def get_direction(self): ...
@abstractmethod
def add_request(self, request): ...
@abstractmethod
def get_requests(self): ...
@abstractmethod
def remove_request(self, request): ...
@abstractmethod
def step(self): ...
class Elevator(IElevator):
def __init__(self, name):
self._name = name
self._floor = 0
self._direction = None
self._requests = []
def get_name(self):
return self._name
def get_floor(self):
return self._floor
def get_direction(self):
return self._direction
def add_request(self, request):
self._requests.append(request)
def get_requests(self):
return list(self._requests)
def remove_request(self, request):
self._requests.remove(request)
if not self._requests:
self._direction = None
def step(self):
stops = {request.get_floor() for request in self._requests}
if not stops:
self._direction = None
return None
if self._floor not in stops:
if self._direction is Direction.UP and max(stops) < self._floor:
self._direction = None
if self._direction is Direction.DOWN and min(stops) > self._floor:
self._direction = None
if self._direction is None:
nearest = min(stops, key=lambda stop: (abs(stop - self._floor), stop))
self._direction = Direction.UP if nearest > self._floor else Direction.DOWN
self._floor += 1 if self._direction is Direction.UP else -1
if self._floor not in stops:
return None
self._requests = [request for request in self._requests if request.get_floor() != self._floor]
if not self._requests:
self._direction = None
return f"{self._name} stops at {self._floor}"
class ElevatorFactory:
def create_elevator(self, name):
return Elevator(name)
class IDispatchStrategy(ABC):
@abstractmethod
def select_elevator(self, elevators, request): ...
class NearestElevatorStrategy(IDispatchStrategy):
def __init__(self, floors):
self._floors = floors
def select_elevator(self, elevators, request):
return min(elevators, key=lambda elevator: (self._cost(elevator, request), elevator.get_name()))
def _cost(self, elevator, request):
distance = abs(elevator.get_floor() - request.get_floor())
moving, wanted = elevator.get_direction(), request.get_direction()
if moving is None:
return distance
here, there = elevator.get_floor(), request.get_floor()
if moving is Direction.UP and wanted is Direction.UP and here <= there:
return distance
if moving is Direction.DOWN and wanted is Direction.DOWN and here >= there:
return distance
return distance + 2 * self._floors
class IElevatorObserver(ABC):
@abstractmethod
def notify(self, message): ...
class ConsoleElevatorObserver(IElevatorObserver):
def __init__(self):
self.messages = []
def notify(self, message):
self.messages.append(message)
class ElevatorRepository:
def __init__(self):
self._elevators = {}
def save(self, elevator):
self._elevators[elevator.get_name()] = elevator
def find_by_name(self, name):
if name not in self._elevators:
raise InvalidRequestException(f"no car {name}")
return self._elevators[name]
def find_all(self):
return [self._elevators[name] for name in sorted(self._elevators)]
class ElevatorService:
def __init__(self, repository, strategy, request_factory, validator, observer):
self._repository = repository
self._strategy = strategy
self._request_factory = request_factory
self._validator = validator
self._observer = observer
self._ticks = 0
def handle_hall_call(self, floor, direction):
request = self._request_factory.create_hall_request(floor, direction)
self._validator.validate_hall_request(request)
label = f"{floor} {direction.name}"
for elevator in self._repository.find_all():
for waiting in elevator.get_requests():
if isinstance(waiting, HallRequest) and waiting.get_floor() == floor \
and waiting.get_direction() is direction:
self._observer.notify(f"call {label} -> {elevator.get_name()} (already waiting)")
return
elevator = self._strategy.select_elevator(self._repository.find_all(), request)
elevator.add_request(request)
self._observer.notify(f"call {label} -> {elevator.get_name()}")
def cancel_hall_call(self, floor, direction):
label = f"{floor} {direction.name}"
for elevator in self._repository.find_all():
for waiting in elevator.get_requests():
if isinstance(waiting, HallRequest) and waiting.get_floor() == floor \
and waiting.get_direction() is direction:
elevator.remove_request(waiting)
self._observer.notify(f"cancel {label} (was {elevator.get_name()})")
return
self._observer.notify(f"no call {label} to cancel")
def handle_car_call(self, name, floor):
elevator = self._repository.find_by_name(name)
self._validator.validate_floor(floor)
elevator.add_request(self._request_factory.create_car_request(floor))
self._observer.notify(f"press {floor} in {name}")
def advance(self, count):
for _ in range(count):
self._ticks += 1
for elevator in self._repository.find_all():
event = elevator.step()
if event:
self._observer.notify(f"tick {self._ticks}: {event}")
def report_status(self):
for elevator in self._repository.find_all():
stops = sorted({request.get_floor() for request in elevator.get_requests()})
where = f"{elevator.get_name()} at {elevator.get_floor()}"
self._observer.notify(f"{where}, stops {stops}" if stops else f"{where}, idle")
class ElevatorController:
def __init__(self, service):
self._service = service
def call(self, floor, direction):
self._service.handle_hall_call(floor, direction)
def press(self, name, floor):
self._service.handle_car_call(name, floor)
def cancel(self, floor, direction):
self._service.cancel_hall_call(floor, direction)
def tick(self, count):
self._service.advance(count)
def status(self):
self._service.report_status()
class ICommand(ABC):
@abstractmethod
def execute(self, controller, args): ...
class CallCommand(ICommand):
def execute(self, controller, args):
controller.call(int(args[0]), Direction[args[1]])
class PressCommand(ICommand):
def execute(self, controller, args):
controller.press(args[0], int(args[1]))
class TickCommand(ICommand):
def execute(self, controller, args):
controller.tick(int(args[0]) if args else 1)
class StatusCommand(ICommand):
def execute(self, controller, args):
controller.status()
class CancelCommand(ICommand):
def execute(self, controller, args):
controller.cancel(int(args[0]), Direction[args[1]])
class CommandFactory:
def __init__(self):
self._commands = {
"CALL": CallCommand(),
"PRESS": PressCommand(),
"TICK": TickCommand(),
"STATUS": StatusCommand(),
"CANCEL": CancelCommand(),
}
def get_command(self, word):
return self._commands.get(word)
class ElevatorSystem:
"""A building has one elevator system, so this is a singleton."""
_instance = None
def __init__(self):
self.observer = ConsoleElevatorObserver()
repository = ElevatorRepository()
factory = ElevatorFactory()
for name in "ABC":
repository.save(factory.create_elevator(name))
strategy = NearestElevatorStrategy(10)
service = ElevatorService(repository, strategy, RequestFactory(), RequestValidator(10), self.observer)
self.controller = ElevatorController(service)
self.commands = CommandFactory()
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = ElevatorSystem()
return cls._instance
def run(lines):
system = ElevatorSystem.get_instance()
for line in lines:
word, *args = line.split()
command = system.commands.get_command(word)
if command is None:
system.observer.notify(f"unknown command {word}")
continue
try:
command.execute(system.controller, args)
except InvalidRequestException as error:
system.observer.notify(f"refused: {error}")
out = list(system.observer.messages)
system.observer.messages.clear()
return out B · answers/answer_b.py
"""Answer B, under-modelled: one class keeps each car as a dictionary, its direction as a string and its
stops as plain floor numbers, and each method reads its own command words."""
class Elevators:
def __init__(self):
self.cars = {}
for name in "ABC":
self.cars[name] = {"floor": 0, "dir": "IDLE", "stops": set()}
self.t = 0
def handle(self, line):
p = line.split()
if p[0] == "CALL":
return [self.call(p)]
if p[0] == "PRESS":
return [self.press(p)]
if p[0] == "TICK":
return self.tick(p)
if p[0] == "STATUS":
return self.status()
return [f"unknown command {p[0]}"]
def call(self, p):
f = int(p[1])
best = None
for name in sorted(self.cars):
c = self.cars[name]
cost = abs(c["floor"] - f)
if c["dir"] == "UP" and (p[2] != "UP" or c["floor"] > f):
cost += 20
if c["dir"] == "DOWN" and (p[2] != "DOWN" or c["floor"] < f):
cost += 20
if best is None or cost < best[0]:
best = (cost, name)
self.cars[best[1]]["stops"].add(f)
return f"call {f} {p[2]} -> {best[1]}"
def press(self, p):
self.cars[p[1]]["stops"].add(int(p[2]))
return f"press {p[2]} in {p[1]}"
def tick(self, p):
out = []
for _ in range(int(p[1]) if len(p) > 1 else 1):
self.t += 1
for name in sorted(self.cars):
c = self.cars[name]
if not c["stops"]:
c["dir"] = "IDLE"
continue
if c["floor"] not in c["stops"]:
if c["dir"] == "UP" and max(c["stops"]) < c["floor"]:
c["dir"] = "IDLE"
if c["dir"] == "DOWN" and min(c["stops"]) > c["floor"]:
c["dir"] = "IDLE"
if c["dir"] == "IDLE":
n = min(c["stops"], key=lambda s: (abs(s - c["floor"]), s))
c["dir"] = "UP" if n > c["floor"] else "DOWN"
c["floor"] += 1 if c["dir"] == "UP" else -1
if c["floor"] not in c["stops"]:
continue
c["stops"].discard(c["floor"])
if not c["stops"]:
c["dir"] = "IDLE"
out.append(f"tick {self.t}: {name} stops at {c['floor']}")
return out
def status(self):
out = []
for name in sorted(self.cars):
c = self.cars[name]
if c["stops"]:
out.append(f"{name} at {c['floor']}, stops {sorted(c['stops'])}")
else:
out.append(f"{name} at {c['floor']}, idle")
return out
def run(lines):
e = Elevators()
out = []
for line in lines:
out.extend(e.handle(line))
return out B + CANCEL · answers/answer_b_cancel.py
"""Answer B after the CANCEL follow-up: hall calls had to move out of "stops" into a set of their own, so
every method that reads the stops changed."""
class Elevators:
def __init__(self):
self.cars = {}
for name in "ABC":
self.cars[name] = {"floor": 0, "dir": "IDLE", "stops": set(), "calls": set()}
self.t = 0
def handle(self, line):
p = line.split()
if p[0] == "CALL":
return [self.call(p)]
if p[0] == "PRESS":
return [self.press(p)]
if p[0] == "TICK":
return self.tick(p)
if p[0] == "STATUS":
return self.status()
if p[0] == "CANCEL":
return [self.cancel(p)]
return [f"unknown command {p[0]}"]
def call(self, p):
f = int(p[1])
best = None
for name in sorted(self.cars):
c = self.cars[name]
cost = abs(c["floor"] - f)
if c["dir"] == "UP" and (p[2] != "UP" or c["floor"] > f):
cost += 20
if c["dir"] == "DOWN" and (p[2] != "DOWN" or c["floor"] < f):
cost += 20
if best is None or cost < best[0]:
best = (cost, name)
self.cars[best[1]]["calls"].add((f, p[2]))
return f"call {f} {p[2]} -> {best[1]}"
def press(self, p):
self.cars[p[1]]["stops"].add(int(p[2]))
return f"press {p[2]} in {p[1]}"
def cancel(self, p):
f = int(p[1])
for name in sorted(self.cars):
c = self.cars[name]
if (f, p[2]) in c["calls"]:
c["calls"].discard((f, p[2]))
if not c["stops"] and not c["calls"]:
c["dir"] = "IDLE"
return f"cancel {f} {p[2]} (was {name})"
return f"no call {f} {p[2]} to cancel"
def tick(self, p):
out = []
for _ in range(int(p[1]) if len(p) > 1 else 1):
self.t += 1
for name in sorted(self.cars):
c = self.cars[name]
todo = c["stops"] | {f for f, d in c["calls"]}
if not todo:
c["dir"] = "IDLE"
continue
if c["floor"] not in todo:
if c["dir"] == "UP" and max(todo) < c["floor"]:
c["dir"] = "IDLE"
if c["dir"] == "DOWN" and min(todo) > c["floor"]:
c["dir"] = "IDLE"
if c["dir"] == "IDLE":
n = min(todo, key=lambda s: (abs(s - c["floor"]), s))
c["dir"] = "UP" if n > c["floor"] else "DOWN"
c["floor"] += 1 if c["dir"] == "UP" else -1
if c["floor"] not in todo:
continue
c["stops"].discard(c["floor"])
c["calls"] = {(f, d) for f, d in c["calls"] if f != c["floor"]}
if not c["stops"] and not c["calls"]:
c["dir"] = "IDLE"
out.append(f"tick {self.t}: {name} stops at {c['floor']}")
return out
def status(self):
out = []
for name in sorted(self.cars):
c = self.cars[name]
todo = c["stops"] | {f for f, d in c["calls"]}
if todo:
out.append(f"{name} at {c['floor']}, stops {sorted(todo)}")
else:
out.append(f"{name} at {c['floor']}, idle")
return out
def run(lines):
e = Elevators()
out = []
for line in lines:
out.extend(e.handle(line))
return out C · answers/answer_c.py
"""Answer C, balanced: a type for each thing the prompt names, one home for each rule, refusals for requests
the building cannot serve, and nothing the prompt does not ask for."""
from dataclasses import dataclass, field
from enum import Enum
class Direction(Enum):
UP = "up"
DOWN = "down"
class Refused(Exception):
"""A request the building cannot serve; run() prints the reason."""
@dataclass(frozen=True)
class HallCall:
floor: int
direction: Direction
def __str__(self):
return f"{self.floor} {self.direction.name}"
@dataclass
class Car:
name: str
floor: int = 0
moving: Direction | None = None
car_calls: set[int] = field(default_factory=set)
hall_calls: set[HallCall] = field(default_factory=set)
def stops(self):
return self.car_calls | {call.floor for call in self.hall_calls}
def heads_for(self, call):
"""Moving the way the call wants to go, with the call's floor still ahead."""
if self.moving is Direction.UP:
return call.direction is Direction.UP and self.floor <= call.floor
if self.moving is Direction.DOWN:
return call.direction is Direction.DOWN and self.floor >= call.floor
return False
def step(self):
"""One tick: serve this floor, or move one floor towards the next stop and serve that floor."""
stops = self.stops()
if not stops:
self.moving = None
return None
if self.floor not in stops:
if self.moving is Direction.UP and max(stops) < self.floor:
self.moving = None
if self.moving is Direction.DOWN and min(stops) > self.floor:
self.moving = None
if self.moving is None:
nearest = min(stops, key=lambda stop: (abs(stop - self.floor), stop))
self.moving = Direction.UP if nearest > self.floor else Direction.DOWN
self.floor += 1 if self.moving is Direction.UP else -1
if self.floor not in stops:
return None
self.car_calls.discard(self.floor)
self.hall_calls = {call for call in self.hall_calls if call.floor != self.floor}
if not self.stops():
self.moving = None
return f"{self.name} stops at {self.floor}"
def cost(car, call, floors):
"""The dispatch rule: the distance, plus twice the building's height if the car must turn round first."""
distance = abs(car.floor - call.floor)
if car.moving is None or car.heads_for(call):
return distance
return distance + 2 * floors
class ElevatorBank:
def __init__(self, floors=10, names="ABC"):
self.floors = floors
self.cars = {name: Car(name) for name in names}
self.ticks = 0
def call(self, floor, direction):
call = HallCall(self._floor(floor), direction)
if (floor, direction) in ((0, Direction.DOWN), (self.floors - 1, Direction.UP)):
raise Refused(f"no {direction.name} call from floor {floor}")
for car in self.cars.values():
if call in car.hall_calls:
return f"call {call} -> {car.name} (already waiting)"
car = min(self.cars.values(), key=lambda car: (cost(car, call, self.floors), car.name))
car.hall_calls.add(call)
return f"call {call} -> {car.name}"
def press(self, name, floor):
self._car(name).car_calls.add(self._floor(floor))
return f"press {floor} in {name}"
def tick(self, count=1):
events = []
for _ in range(count):
self.ticks += 1
for car in self.cars.values():
event = car.step()
if event:
events.append(f"tick {self.ticks}: {event}")
return events
def status(self):
return [f"{car.name} at {car.floor}, " + (f"stops {sorted(car.stops())}" if car.stops() else "idle")
for car in self.cars.values()]
def _floor(self, floor):
if not 0 <= floor < self.floors:
raise Refused(f"no floor {floor}")
return floor
def _car(self, name):
if name not in self.cars:
raise Refused(f"no car {name}")
return self.cars[name]
def run(lines):
"""The boundary: turns command lines into calls and collects what the bank prints."""
bank = ElevatorBank()
out = []
for line in lines:
word, *args = line.split()
try:
if word == "CALL":
out.append(bank.call(int(args[0]), Direction[args[1]]))
elif word == "PRESS":
out.append(bank.press(args[0], int(args[1])))
elif word == "TICK":
out.extend(bank.tick(int(args[0]) if args else 1))
elif word == "STATUS":
out.extend(bank.status())
else:
out.append(f"unknown command {word}")
except Refused as reason:
out.append(f"refused: {reason}")
return out C + CANCEL · answers/answer_c_cancel.py
"""Answer C after the CANCEL follow-up: Car.drop() and ElevatorBank.cancel() are new, and run() reads one
more command word."""
from dataclasses import dataclass, field
from enum import Enum
class Direction(Enum):
UP = "up"
DOWN = "down"
class Refused(Exception):
"""A request the building cannot serve; run() prints the reason."""
@dataclass(frozen=True)
class HallCall:
floor: int
direction: Direction
def __str__(self):
return f"{self.floor} {self.direction.name}"
@dataclass
class Car:
name: str
floor: int = 0
moving: Direction | None = None
car_calls: set[int] = field(default_factory=set)
hall_calls: set[HallCall] = field(default_factory=set)
def stops(self):
return self.car_calls | {call.floor for call in self.hall_calls}
def heads_for(self, call):
"""Moving the way the call wants to go, with the call's floor still ahead."""
if self.moving is Direction.UP:
return call.direction is Direction.UP and self.floor <= call.floor
if self.moving is Direction.DOWN:
return call.direction is Direction.DOWN and self.floor >= call.floor
return False
def step(self):
"""One tick: serve this floor, or move one floor towards the next stop and serve that floor."""
stops = self.stops()
if not stops:
self.moving = None
return None
if self.floor not in stops:
if self.moving is Direction.UP and max(stops) < self.floor:
self.moving = None
if self.moving is Direction.DOWN and min(stops) > self.floor:
self.moving = None
if self.moving is None:
nearest = min(stops, key=lambda stop: (abs(stop - self.floor), stop))
self.moving = Direction.UP if nearest > self.floor else Direction.DOWN
self.floor += 1 if self.moving is Direction.UP else -1
if self.floor not in stops:
return None
self.car_calls.discard(self.floor)
self.hall_calls = {call for call in self.hall_calls if call.floor != self.floor}
if not self.stops():
self.moving = None
return f"{self.name} stops at {self.floor}"
def drop(self, call):
self.hall_calls.discard(call)
if not self.stops():
self.moving = None
def cost(car, call, floors):
"""The dispatch rule: the distance, plus twice the building's height if the car must turn round first."""
distance = abs(car.floor - call.floor)
if car.moving is None or car.heads_for(call):
return distance
return distance + 2 * floors
class ElevatorBank:
def __init__(self, floors=10, names="ABC"):
self.floors = floors
self.cars = {name: Car(name) for name in names}
self.ticks = 0
def call(self, floor, direction):
call = HallCall(self._floor(floor), direction)
if (floor, direction) in ((0, Direction.DOWN), (self.floors - 1, Direction.UP)):
raise Refused(f"no {direction.name} call from floor {floor}")
for car in self.cars.values():
if call in car.hall_calls:
return f"call {call} -> {car.name} (already waiting)"
car = min(self.cars.values(), key=lambda car: (cost(car, call, self.floors), car.name))
car.hall_calls.add(call)
return f"call {call} -> {car.name}"
def press(self, name, floor):
self._car(name).car_calls.add(self._floor(floor))
return f"press {floor} in {name}"
def cancel(self, floor, direction):
call = HallCall(floor, direction)
for car in self.cars.values():
if call in car.hall_calls:
car.drop(call)
return f"cancel {call} (was {car.name})"
return f"no call {call} to cancel"
def tick(self, count=1):
events = []
for _ in range(count):
self.ticks += 1
for car in self.cars.values():
event = car.step()
if event:
events.append(f"tick {self.ticks}: {event}")
return events
def status(self):
return [f"{car.name} at {car.floor}, " + (f"stops {sorted(car.stops())}" if car.stops() else "idle")
for car in self.cars.values()]
def _floor(self, floor):
if not 0 <= floor < self.floors:
raise Refused(f"no floor {floor}")
return floor
def _car(self, name):
if name not in self.cars:
raise Refused(f"no car {name}")
return self.cars[name]
def run(lines):
"""The boundary: turns command lines into calls and collects what the bank prints."""
bank = ElevatorBank()
out = []
for line in lines:
word, *args = line.split()
try:
if word == "CALL":
out.append(bank.call(int(args[0]), Direction[args[1]]))
elif word == "PRESS":
out.append(bank.press(args[0], int(args[1])))
elif word == "TICK":
out.extend(bank.tick(int(args[0]) if args else 1))
elif word == "STATUS":
out.extend(bank.status())
elif word == "CANCEL":
out.append(bank.cancel(int(args[0]), Direction[args[1]]))
else:
out.append(f"unknown command {word}")
except Refused as reason:
out.append(f"refused: {reason}")
return out Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Every version passes everything it is given, so the difference is the cost of the change. The balanced answer C
needed 13 code lines: Car.drop(), ElevatorBank.cancel() and one command word in run(). The under-modelled
answer B needed 37 and changed five of its seven functions: its stops were bare floor numbers, so hall calls had to
be split from passengers’ car calls wherever the stops were read. The over-engineered answer A changed almost
nothing that existed, but the change ran through six classes, four of which only declare, register or pass it on.
Where the CANCEL follow-up landed in each answer
Text description of the diagram
The diagram has three panels, one above the other, one for each answer. Each shows what the CANCEL follow-up added or changed, in the order a CANCEL line reaches it, and the counts that compare.py measured.
- Answer A, over-engineered: 22 code lines in 6 classes. CommandFactory changed, to register CANCEL. It creates the new CancelCommand, whose execute method reads the line and passes it on to the new ElevatorController.cancel, which passes it on to the new ElevatorService.cancel_hall_call, which finds the call. That calls IElevator.remove_request, a new method the interface only declares, implemented by the new Elevator.remove_request, which removes the call.
- Answer B, under-modelled: 37 code lines, and 5 of its 7 functions changed. Each car's dictionary keeps its stops as floor numbers only, as before, and gains calls, a new set of floor and direction pairs. The functions that read that data changed: __init__, handle, call, tick and status; cancel is new; press and run did not change.
- Answer C, balanced: 13 code lines in 2 classes and run(). run() changed, to read CANCEL, and calls the new ElevatorBank.cancel, which finds the call and calls the new Car.drop, which drops it and leaves the car idle when no stops are left.
| Criterion | A | B | C |
|---|---|---|---|
| Classes | 25 | 1 | 5 |
| Lines | 237 | 74 | 110 |
| CANCEL: lines | 22 | 37 | 13 |
| CANCEL: functions | 6 | 6 | 3 |
| CANCEL: classes | 6 | 1 | 2 |
Five questions to ask of your own answer
Ask your answer five questions before the interviewer asks for a change. They are the five criteria of the rubric used to score answers:
- Fit. Does every noun and state of the prompt have a type, with nothing the prompt does not need?
- One home per rule. Can you point at the one place where each rule lives?
- Change. Would the likeliest follow-up land in one or two places?
- Failure paths. What happens to a request the system cannot serve?
- Tests. Can a test build the system, drive it and throw it away, sharing nothing?
Practise: cars that do not stop at every floor
Add a follow-up to the balanced answer: floors 7 to 9 are let to one company, and only car C stops there. The sample tests check that nothing else changes, where hall calls go, and the two new refusals.
Exercise · Medium · Python
Cars that do not stop at every floor
elevators.py is the balanced answer to the elevator prompt. A follow-up arrives: floors 7 to 9 are let to one company, and only car C stops there. Add it so that a building without such rules prints exactly what it printed before:
ElevatorBank(floors=10, names="ABC", serves=None):servesmaps a car's name to the floors it stops at, such as{"A": range(7), "B": range(7)}. A car that is not inservesstops at every floor.Car.serves(floor)answers whether the car stops at that floor.- A hall call goes only to a car that stops at its floor, chosen among those cars by the same cost rule. If no car stops there,
call()raisesRefused("no car stops at 8"). press()raisesRefused("A does not stop at 8")when the car skips that floor. A floor that does not exist is still refused first, asno floor 12.run(lines, serves=None)passesserveson to the bank.
The sample tests run a building without rules, calls that only one car can take, the two refusals and Car.serves().
Starter code · elevators.py
"""The balanced answer to the elevator prompt. Add the follow-up of the task: some cars do not stop at every
floor."""
from dataclasses import dataclass, field
from enum import Enum
class Direction(Enum):
UP = "up"
DOWN = "down"
class Refused(Exception):
"""A request the building cannot serve; run() prints the reason."""
@dataclass(frozen=True)
class HallCall:
floor: int
direction: Direction
def __str__(self):
return f"{self.floor} {self.direction.name}"
@dataclass
class Car:
name: str
floor: int = 0
moving: Direction | None = None
car_calls: set[int] = field(default_factory=set)
hall_calls: set[HallCall] = field(default_factory=set)
def stops(self):
return self.car_calls | {call.floor for call in self.hall_calls}
def heads_for(self, call):
"""Moving the way the call wants to go, with the call's floor still ahead."""
if self.moving is Direction.UP:
return call.direction is Direction.UP and self.floor <= call.floor
if self.moving is Direction.DOWN:
return call.direction is Direction.DOWN and self.floor >= call.floor
return False
def step(self):
"""One tick: serve this floor, or move one floor towards the next stop and serve that floor."""
stops = self.stops()
if not stops:
self.moving = None
return None
if self.floor not in stops:
if self.moving is Direction.UP and max(stops) < self.floor:
self.moving = None
if self.moving is Direction.DOWN and min(stops) > self.floor:
self.moving = None
if self.moving is None:
nearest = min(stops, key=lambda stop: (abs(stop - self.floor), stop))
self.moving = Direction.UP if nearest > self.floor else Direction.DOWN
self.floor += 1 if self.moving is Direction.UP else -1
if self.floor not in stops:
return None
self.car_calls.discard(self.floor)
self.hall_calls = {call for call in self.hall_calls if call.floor != self.floor}
if not self.stops():
self.moving = None
return f"{self.name} stops at {self.floor}"
def cost(car, call, floors):
"""The dispatch rule: the distance, plus twice the building's height if the car must turn round first."""
distance = abs(car.floor - call.floor)
if car.moving is None or car.heads_for(call):
return distance
return distance + 2 * floors
class ElevatorBank:
def __init__(self, floors=10, names="ABC"):
self.floors = floors
self.cars = {name: Car(name) for name in names}
self.ticks = 0
def call(self, floor, direction):
call = HallCall(self._floor(floor), direction)
if (floor, direction) in ((0, Direction.DOWN), (self.floors - 1, Direction.UP)):
raise Refused(f"no {direction.name} call from floor {floor}")
for car in self.cars.values():
if call in car.hall_calls:
return f"call {call} -> {car.name} (already waiting)"
car = min(self.cars.values(), key=lambda car: (cost(car, call, self.floors), car.name))
car.hall_calls.add(call)
return f"call {call} -> {car.name}"
def press(self, name, floor):
self._car(name).car_calls.add(self._floor(floor))
return f"press {floor} in {name}"
def tick(self, count=1):
events = []
for _ in range(count):
self.ticks += 1
for car in self.cars.values():
event = car.step()
if event:
events.append(f"tick {self.ticks}: {event}")
return events
def status(self):
return [f"{car.name} at {car.floor}, " + (f"stops {sorted(car.stops())}" if car.stops() else "idle")
for car in self.cars.values()]
def _floor(self, floor):
if not 0 <= floor < self.floors:
raise Refused(f"no floor {floor}")
return floor
def _car(self, name):
if name not in self.cars:
raise Refused(f"no car {name}")
return self.cars[name]
def run(lines):
"""The boundary: turns command lines into calls and collects what the bank prints."""
bank = ElevatorBank()
out = []
for line in lines:
word, *args = line.split()
try:
if word == "CALL":
out.append(bank.call(int(args[0]), Direction[args[1]]))
elif word == "PRESS":
out.append(bank.press(args[0], int(args[1])))
elif word == "TICK":
out.extend(bank.tick(int(args[0]) if args else 1))
elif word == "STATUS":
out.extend(bank.status())
else:
out.append(f"unknown command {word}")
except Refused as reason:
out.append(f"refused: {reason}")
return out The sample tests · test_elevators.py
from elevators import ElevatorBank, run
UPPER = {"A": range(7), "B": range(7)} # floors 7 to 9: only car C stops there
def test_a_building_without_restrictions_prints_the_same():
"""without serves, a scenario prints exactly what the balanced answer printed"""
commands = ["PRESS A 9", "TICK 2", "CALL 6 UP", "CALL 4 DOWN", "CALL 1 UP", "TICK 3", "STATUS", "TICK 4", "STATUS"]
assert run(commands) == [
"press 9 in A", "call 6 UP -> A", "call 4 DOWN -> B", "call 1 UP -> B", "tick 3: B stops at 1",
"A at 5, stops [6, 9]", "B at 3, stops [4]", "C at 0, idle",
"tick 6: A stops at 6", "tick 6: B stops at 4", "tick 9: A stops at 9",
"A at 9, idle", "B at 4, idle", "C at 0, idle",
]
def test_a_car_on_its_way_down_still_takes_calls_below_it():
"""without serves, car A going down from 8 takes the DOWN call from 4, not the one from 7 behind it"""
commands = ["PRESS A 8", "TICK 8", "PRESS A 2", "TICK 2", "CALL 4 DOWN", "CALL 7 DOWN"]
assert run(commands) == ["press 8 in A", "tick 8: A stops at 8", "press 2 in A", "call 4 DOWN -> A", "call 7 DOWN -> B"]
def test_a_hall_call_goes_to_a_car_that_stops_there():
"""a call from floor 8 goes to C, the only car that stops there, though A is first by name"""
assert run(["CALL 8 DOWN", "TICK 8", "STATUS"], serves=UPPER) == [
"call 8 DOWN -> C", "tick 8: C stops at 8", "A at 0, idle", "B at 0, idle", "C at 8, idle",
]
def test_the_cost_rule_still_chooses_among_those_cars():
"""car A, at floor 6, takes the call from 5 but not the nearer-looking call from 8"""
assert run(["PRESS A 6", "TICK 6", "CALL 5 DOWN", "CALL 8 DOWN"], serves=UPPER) == [
"press 6 in A", "tick 6: A stops at 6", "call 5 DOWN -> A", "call 8 DOWN -> C",
]
def test_a_press_for_a_floor_the_car_skips_is_refused():
"""pressing 8 inside A is refused, inside C it is a stop"""
assert run(["PRESS A 8", "PRESS C 8"], serves=UPPER) == ["refused: A does not stop at 8", "press 8 in C"]
def test_a_call_no_car_can_serve_is_refused():
"""when no car stops at floor 8, the call is refused and no car moves"""
serves = {name: range(7) for name in "ABC"}
assert run(["CALL 8 DOWN", "TICK 3", "STATUS"], serves=serves) == [
"refused: no car stops at 8", "A at 0, idle", "B at 0, idle", "C at 0, idle",
]
def test_floors_that_do_not_exist_are_refused_as_before():
"""floor 12 is still 'no floor 12', not a floor some car skips"""
assert run(["CALL 12 DOWN", "PRESS A 12"], serves=UPPER) == ["refused: no floor 12", "refused: no floor 12"]
def test_each_car_answers_which_floors_it_serves():
"""Car.serves(floor) is the one place that knows where a car stops"""
bank = ElevatorBank(serves=UPPER)
assert [bank.cars["A"].serves(floor) for floor in (0, 6, 7, 9)] == [True, True, False, False]
assert all(bank.cars["C"].serves(floor) for floor in range(10)) A hint
Give the rule one home. Add a field to Car that holds the floors it stops at, with None for every floor, and a method serves(floor) that reads it. Then call() chooses among the cars for which car.serves(floor) is true, and press() asks the same method once it has checked the car and the floor.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
7 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Each weakness reproduced in code that runs Pro: opens with a Pro, Premium or Ultimate pass
Each weakness is easy to see in someone else’s code and easy to miss in your own. This part makes each one fail in front of you: a suite that passes one scenario at a time and fails as a whole, requests that nobody wrote a scenario for, a quick fix that strands a passenger, and one call traced through every layer it crosses.
In this part:
- Hidden global state: scenarios that pass alone and fail together
- Requests nobody wrote a scenario for
- Under-modelling: the quick CANCEL that strands a passenger
- Over-engineering and pattern stuffing: one call through every layer
3 runnable examples (Python) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Read a complete sample first: Low-level design: parking lot (the model)
Shrinking an over-engineered answer in three measured steps Pro: opens with a Pro, Premium or Ultimate pass
An over-engineered answer is fixed by taking structure out, one step at a time, with the scenarios passing after every step. This part removes answer A’s singleton, collapses its one-implementation hierarchies and removes its middle men, and measures what each step took away.
In this part:
- Three refactorings, measured after each step
- Step 1: build the system instead of fetching a singleton
- Step 2: Collapse Hierarchy and Inline Class
- Step 3: Remove Middle Man
- Which steps fit in the last fifteen minutes
1 runnable example (Step 1, Step 2, Step 3, shrink.py) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Scoring answers with the rubric, level by level Pro: opens with a Pro, Premium or Ultimate pass
A rubric turns “this feels heavy” into points that you can check and defend. This part gives each of the five criteria a score of 0, 1 or 2, scores the three elevator answers with the evidence for each point, and says what an answer at each level of experience is expected to show.
In this part:
- The rubric: five criteria, 0 to 2 points each
- The three elevator answers, scored against the rubric
- What each level of experience is expected to show
- Defending a lean answer when the interviewer asks for patterns
about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
What is the difference between over-engineering and under-modelling in a low-level design answer?
Over-engineering adds structure the prompt does not need: an interface for a class with one implementation, a factory for an object that a constructor could build, a layer that only passes calls on. Under-modelling leaves out structure the prompt does need: one class keeps everything in nested dictionaries, a status or a direction is a free-form string, and nothing says what a value means. Both answers can pass their scenarios. The difference shows when a requirement changes. An over-engineered answer spreads a small change across many classes, each of which must be read to find the one that decides; an under-modelled answer has to change most of its methods, because its data cannot express the new idea. A balanced answer gives each idea of the prompt a type and each rule one home, and adds a seam only where a stated change will land.
-
Mid to senior
Your elevator answer has an interface for every class, three factories, a command class for each input word and a Singleton for the system. The interviewer says it feels heavy. What do you remove first, what do you keep, and how do you justify each choice?
-
Mid to senior
Each test of your elevator answer passes when it runs alone, but two fail when the whole suite runs, and which two depends on the order. What do you look for, and how do you change the design so that it cannot happen again?
-
Fresher to senior
Halfway through a 60-minute round, the interviewer asks what your parking-lot answer does when a ticket is lost, a card payment is declined at the exit, or the same ticket is scanned twice. It handles none of them. Which do you design now, which do you only describe, and how?
Key takeaways
- A weak LLD answer usually runs; its weakness shows when a follow-up arrives.
- Five weaknesses recur: over-engineering, under-modelling, pattern stuffing, hidden global state and ignored failure paths.
- Judge an answer by what a likely follow-up costs it: 13 code lines for the balanced elevator answer, 37 lines and five of seven functions for the under-modelled one.
- Give each idea a type and each rule one home, add seams only for stated changes, refuse impossible requests, and build objects where tests can build them.
References
- What to look for in a code review (Google Engineering Practices)
- Yagni (Martin Fowler)
- Eradicating Non-Determinism in Tests (Martin Fowler)
- Catalog of Refactorings (Martin Fowler)
- Design Patterns: Elements of Reusable Object-Oriented Software (Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides (Addison-Wesley))
- unittest: Unit testing framework (organizing test code) (Python Software Foundation)
- importlib.reload (The Python Standard Library) (Python Software Foundation)
- sys.setprofile (The Python Standard Library) (Python Software Foundation)
- ast: Abstract Syntax Trees (Python Software Foundation)
- abc: Abstract Base Classes (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress