Unveiling the Secrets of Software Design: A Practical Guide to Architectural Patterns Disguised as an Elevator Problem

Efim Shliamin
28 min readMay 11, 2024

--

My website:

Hello! 👋 In this article, we will continue to discuss the situation with the two elevators I previously described in this article:

The article provides a simplified version of the situation (in Python) to learn about OOP, but we didn’t consider something. 🤓 Do you have any idea what it is yet? I’ll remind you of the code from the article:

class Building:

  • Attributes:
    floors: the number of floors in the building.
    elevators: list of elevators in the building.
  • Methods:
    call_elevator(floor, direction): method to call an elevator to the floor with direction (up or down).

class Elevator:

  • Attributes:
    current_floor: the current floor on which the elevator is located.
    state: the state of the elevator (e.g., moving up, moving down, waiting).
    max_capacity: the maximum capacity of the elevator.
    occupancy: the current number of people in the elevator.
  • Methods:
    move_up(): moves the elevator one floor up.
    move_down(): moves the elevator one floor down.
    open_door(): opens the elevator doors.
    close_door(): closes the elevator doors.
    go_to_floor(target_flor): sends the elevator to the specified target_floor.

class Controller (optional):

  • Methods:
    assign_elevator(request): determines which elevator will respond to the request based on all elevators' current state and locations.

And here is the Python code again:

class Building:
def __init__(self, number_of_floors, number_of_elevators):
self.floors = number_of_floors
self.elevators = [Elevator() for _ in range(number_of_elevators)]

def call_elevator(self, floor, direction):
# Here's the logic for choosing the right elevator
pass

class Elevator:
def __init__(self):
self.current_floor = 0
self.state = "waiting"
self.max_capacity = 10
self.occupancy = 0

def move_up(self):
if self.current_floor < building.floors - 1:
self.current_floor += 1
self.state = "moving up"

def move_down(self):
if self.current_floor > 0:
self.current_floor -= 1
self.state = "moving down"

def open_door(self):
self.state = "doors open"

def close_door(self):
self.state = "doors closed"

def go_to_floor(self, target_floor):
while self.current_floor != target_floor:
if self.current_floor < target_floor:
self.move_up()
elif self.current_floor > target_floor:
self.move_down()
self.open_door()
self.close_door()

# Creation of a building with ten floors and two elevators
building = Building(10, 2)

Let’s consider the architectural solutions for this example. 🙂 Do you believe using separate buttons for each elevator is essential, or would it be more convenient to have one standard call system that analyzes all elevators' current position and status to select the most appropriate one?

What if there were more elevators and more floors? 🤔

As we delve deeper into this discussion, it’s clear that a command pattern can significantly simplify the management of the elevator status and its tasks. This would allow, for example, to schedule commands if there is a request to stop immediately. By the way, I think you have chosen a shared call system! 😉 It can make the user interface more intuitive and efficiently distribute tasks between elevators.

I’ll offer you 🖐️ five behavioral design patterns that might be suitable for implementing such a system:

  1. Command Pattern: This pattern allows requests to be encapsulated as objects, simplifying order management and making new commands easy. In the context of elevators, commands could be go_to_floor(target_flor), open_door(), close_door(), etc.
  2. Observer Pattern: This pattern will notify elevators of calls or commands without constantly polling the controller. For example, when a user calls an elevator, the system can notify all suitable elevators of the new request.
  3. Strategy Pattern: This pattern can select an elevator selection algorithm based on the system's current state (e.g., selecting the closest accessible elevator or an elevator already moving in the desired direction).
  4. Command Queue: Organizing commands into a queue will allow us to manage the order of their execution and provide the ability to cancel or modify already scheduled actions.
  5. State Pattern: This pattern helps manage elevator state changes (e.g.,move_up()). It simplifies states' management and transitions between them, making the code cleaner and more transparent.

Which of these patterns seems the most appropriate for us to start with? Which functions in our system could most benefit from implementing these patterns? Implementing them simultaneously may seem complex, but they complement each other and help build a flexible and scalable system. When properly integrated, each pattern solves its problem, making complex systems more accessible to manage and more reliable.

Command Pattern:

Step 1: Defining the command interface

First, we create a command interface with execute() and undo() methods. This will allow us to add new types of commands.

class Command:
def __init__(self, elevator):
self.elevator = elevator

def execute(self):
pass

def undo(self):
pass

Step 2: Creation of specific commands

Next, we define specific commands to control the elevator, such as commands to move to a particular floor.

class MoveToFloorCommand(Command):
def __init__(self, elevator, target_floor):
super().__init__(elevator)
self.target_floor = target_floor
self.previous_floor = elevator.current_floor

def execute(self):
# Simulate moving floor by floor
print(f"Starting at floor {self.elevator.current_floor}")
while self.elevator.current_floor != self.target_floor:
if self.elevator.current_floor < self.target_floor:
self.elevator.current_floor += 1
else:
self.elevator.current_floor -= 1
print(f"Moving to floor {self.elevator.current_floor}")
time.sleep(1) # Simulate time delay of moving floors

print(f"Arrived at floor {self.elevator.current_floor}")
self.elevator.open_door()
time.sleep(1) # Simulate doors open
self.elevator.close_door()

def undo(self):
print(f"Returning to floor {self.previous_floor}")
while self.elevator.current_floor != self.previous_user_defined_floor:
if self.elevator.current_floor < self.previous_floor:
self.elevator.current_floor += 1
else:
self.elevator.current_floor -= 1
print(f"Moving to floor {self.elevator.current_floor}")
time.sleep(1) # Simulate time delay of moving floors
print(f"Returned to floor {self.elevator.current_floor}")

class OpenDoorCommand(Command):
def execute(self):
self.elevator.open_door()

def undo(self):
self.elevator.close_door()

class CloseDoorCommand(Command):
def execute(self):
self.elevator.close_door()

def undo(self):
self.elevator.open_door()

Step 3: Integration of commands into the system

Now that we have commands, we can integrate them into the main elevator code, allowing us to use those commands to perform actions.

class Elevator:
def __init__(self):
self.current_floor = 0
self.state = "waiting"
self.max_capacity = 10
self.occupancy = 0
self.command_history = [] # Initialize the command history

def execute_command(self, command):
command.execute()
self.command_history.append(command)

def undo_last_command(self):
if self.command_history:
last_command = self.command_history.pop()
last_command.undo()

# We don't need it anymore:

# def move_up(self):
# Assuming move_up is now handled by a command
# pass

# def move_down(self):
# Assuming move_down is now handled by a command
# pass

# def open_door(self):
# A command could control the direct method
# self.state = "doors open"

# def close_door(self):
# A command could control the direct method
# self.state = "doors closed"

# def go_to_floor(self, target_floor):
# Simplified for command use
# self.current_floor = target_floor
# self.state = "moving" # This would realistically have more logic

This approach makes the elevator control system flexible and extensible. We can add new commands without changing existing elevator classes or the control system. With the extension of the MoveToFloorCommand to include more detailed movement logic, we have successfully implemented the command pattern in our elevator control system. This change brings several benefits:

1. Encapsulation of actions 🦾
The command pattern encapsulates all the actions in moving an elevator within commands. This means the elevator does not have to know precisely how to move between floors; it simply executes the command. This simplifies the elevator code and makes the system more modular.

2. Ease of adding new commands 🤌
If new types of actions need to be added to the elevator, this can be done by creating new classes of commands without changing the basic elevator code. This makes the system easily extensible.

3. Cancel and repeat commands 😇
The implementation makes adding functionality to undo (and repeat) recent actions easy. This can be useful in case of errors or changes in the user’s plans. For example, we can cancel the move if someone accidentally selects the wrong floor.

4. Separating control logic from execution 👌
The logic to select and control commands can be separated from the logic that directly controls the elevator mechanisms. This simplifies testing because commands can be tested separately from the elevator mechanics.

5. Command history 🙈
The system can track all executed commands, which can help audit actions or the system's state after failures.

Let’s see how we can practically use the implemented code. Imagine that a user calls an elevator to the first floor and then wants to go to the fifth floor:

# Creating an elevator object
my_elevator = Elevator()

# Creating and executing a command to move the elevator to the first floor
command_to_first_floor = MoveToFloorCommand(my_elevator, 1)
my_elevator.execute_command(command_to_first_floor)

# Suppose the user has decided to change the destination and now wants to go to the fifth floor
command_to_fifth_floor = MoveToFloorCommand(my_elevator, 5)
my_elevator.execute_command(command_to_fifth_floor)

# Display the current status of the elevator
print(f"Current floor: {my_elevator.current_floor}")

In this scenario, we create an Elevator object and use the MoveToFloorCommand to move the elevator to different floors. After each command, the elevator state is updated, and we can output the current elevator floor.

Undo command 🙌
Suppose the user changed his mind and wanted to return to the first floor before the elevator started moving to the fifth floor. In this case, we can undo the last command:

# Undo the last command (move to the fifth floor)
my_elevator.undo_last_command()

# Display the current status of the elevator after undo
print(f"Current floor after undo: {my_elevator.current_floor}")

Additional logic
We can also add logic to open and close doors using the appropriate commands if needed in the context of our application:

# Opening doors on the current floor
open_door_command = OpenDoorCommand(my_elevator)
my_elevator.execute_command(open_door_command)

# Closing the doors after passengers have boarded
close_door_command = CloseDoorCommand(my_elevator)
my_elevator.execute_command(close_door_command)

This example demonstrates how to use commands to control an elevator, including dynamically changing plans (cancel command) and performing standard operations such as opening and closing doors. This allows us to develop a flexible and powerful elevator control system! 🙂

Before the command pattern was used, the elevator was usually controlled directly through the Elevator class method calls. In this case, all the functionality to control elevator movement and states were integrated 😫 directly into the class methods. Let’s take a look at what this approach could look like:

Using the old code (without a command pattern):

# Creating an elevator object
my_elevator = Elevator()

# Moving the elevator to the first floor
my_elevator.move_to_floor(1)

# Moving the elevator to the fifth floor
my_elevator.move_to_floor(5)

# Display of the current status of the elevator
print(f"Current floor: {my_elevator.current_floor}")

Key differences and limitations without a command pattern:

1. Direct control: All elevator control actions occur through direct calls to Elevator class methods, which can lead to high code coupling. ☹️

2. Lack of flexibility: Adding new functionality or changing existing behavior requires modification of the Elevator class, which can complicate code maintenance and extension. 👎

3. No command encapsulation: Each Elevator action is implemented inside the methods of the class, making tasks such as undoing or logging actions difficult. 🥴

4. Difficulty in testing: Testing such methods can be more difficult because they are closely related to the elevator's internal state. 😰

Implementing a command pattern allows the elevator control and its physical actions to be separated, thus simplifying the code and making it more flexible and easier to maintain. It also makes extending the system with new functions easier without interfering with the main elevator code.

Since we’ve already discussed the command pattern, how about we look at the observer pattern next? 🤓 This pattern is great for scenarios where we want to keep multiple parts of our system informed about changes happening elsewhere in the system. It is helpful for the elevator system, where different components need to know the state of the elevator.

Observer pattern:

Step 1: Define the observer and subject interfaces

The observer pattern involves two key components:

  1. Subjects: These objects hold essential data, and other system parts must be notified when this data changes. 🔊
  2. Observers: These objects need to be informed about changes in the subject. 👀

In our elevator example, the Elevator can be the subject, and various components that need to react to its movements can be the observers. First, let’s define interfaces for both observers and subjects:

class Observer:
def update(self, message):
pass

class Subject:
def __init__(self):
self._observers = []

def register_observer(self, observer):
self._observers.append(observer)

def remove_observer(self, observer):
self._observers.remove(observer)

def notify_observers(self, message):
for observer in self._observers:
observer.update(message)

Step 2: Implement the observer in the elevator system

We’ll make the Elevator class a subject that notifies observers when it moves to a new floor.

class Elevator(Subject):
def __init__(self):
super().__init__()
self.current_floor = 0
self.max_capacity = 10
self.state = "waiting"
self.command_history = [] # Initialize the command history

def execute_command(self, command):
command.execute()
self.command_history.append(command)

def undo_last_command(self):
if self.command_history:
last_command = self.command_history.pop()
last_command.undo()

Of course, we must also rewrite the previous MoveToFloorCommand 😅 using the observer pattern. Here’s how we can modify the MoveToFloorCommand to include observer notifications, ensuring that any state change or significant action within the command can be observed:

class MoveToFloorCommand(Command):
def __init__(self, elevator, target_floor):
super().__init__(elevator)
self.target_floor = target_floor
self.previous_floor = elevator.current_floor

def execute(self):
print(f"Starting at floor {self.elevator.current_floor}")
while self.elevator.current_floor != self.target_floor:
if self.elevator.current_floor < self.target_floor:
self.elevator.current_floor += 1
else:
self.elevator.current_floor -= 1
# Notify observers about the movement to the new floor
self.elevator.notify_observers(f"Moving to floor {self.elevator.current_floor}")
print(f"Moving to floor {self.elevator.current_floor}")
time.sleep(1) # Simulate time delay of moving floors

# Notify observers on arrival
self.elevator.notify_observers(f"Arrived at floor {self.elevator.current_floor}")
print(f"Arrived at floor {self.elevator.current_floor}")
self.elevator.open_door()
time.sleep(1) # Simulate doors open
self.elevator.close_door()

def undo(self):
print(f"Returning to floor {self.previous_floor}")
while self.elevator.current_array != self.previous_floor:
if self.elevator.current_floor < self.previous_floor:
self.elevator.current_floor += 1
else:
self.elevator.current_floor -= 1
# Notify observers about the movement to the previous floor
self.elevator.notify_observers(f"Returning to floor {self.elevator.current_floor}")
print(f"Moving to floor {self.elevator.current_floor}")
time.sleep(1) # Simulate time delay of moving floors
print(f"Returned to floor {self.elevator.current_floor}")
  • Observer Notifications: Each significant step within the command’s execution, like moving to a new floor or arriving at the target floor, is accompanied by a call to notify_observers(). This method sends a message to all registered observers, which could be anything from logging services to UI updates, informing them of the elevator’s current state.
  • Undo Functionality: The undo() method also includes observer notifications. This feature is handy for situations where actions must be reversed, and observers must be informed about the reversal in real time.

This integration allows the elevator system to maintain robust command-driven behavior while providing real-time updates to various system parts via the observer pattern. This dual pattern approach enhances the system's modularity and responsiveness, making it easier to maintain and extend.

Step 3: Create observers 👀

Let’s create a simple observer that logs messages to the console whenever the elevator reaches a new floor:

class ElevatorMonitor(Observer):
def update(self, message):
print(f"Elevator status update: {message}")

Step 4: Using the observer pattern and the command pattern together 🫶

Finally, we’ll set up a system where the elevator notifies its monitor about floor changes. Suppose we want to simulate the elevator moving from the ground floor to the fifth floor, and we have an observer (like an elevator monitor) that needs to log every elevator movement or state change.

# Create the elevator and attach an observer
my_elevator = Elevator()
monitor = ElevatorMonitor()
my_elevator.register_observer(monitor)

# Create and execute a command to move the elevator to the fifth floor
move_to_fifth = MoveToFloorCommand(my_elevator, 5)
my_elevator.execute_command(move_to_fifth)

# Optionally, simulate undoing the action
# my_elevator.undo_last_command()
  • Command Execution: When the move_to_fifth command is executed, it changes the state of the elevator (i.e., updating the current floor one step at a time) and continuously notifies all observers about these changes. 🎯 As the elevator moves from one floor to another, it sends updates like "Moving to floor 1" and "Moving to floor 2" until it reaches the fifth floor with "Arrived at floor 5".
  • Observer Notification: Each time a command alters the state of the elevator, the notify_observers() method is called with a specific message. The ElevatorMonitor, registered as an observer, receives and logs these messages. This could be seen in real-world applications as updates on an elevator panel or logs for maintenance. 😇

Benefits of using command and observer patterns together:

  • Decoupling Command Execution from State Notification: The elevator’s operations (handled by commands) are decoupled from how these operations are reported or logged (handled by observers). This makes modifying one aspect (like adding new commands or changing log formats) easier without affecting the other. 🤌
  • Enhanced Flexibility and Scalability: New types of commands or observers can be added with minimal changes to existing code. For example, we could add a command for emergency stops or an observer that sends alerts to a security system. 👌

This integration effectively demonstrates how the command pattern can work with the observer pattern to create a robust, flexible system that is easy to maintain and extend. This approach is instrumental in complex systems where actions and their effects must be monitored or recorded without creating tight coupling between components.

For example, let’s extend our program and create an “Emergency Stop” command for the elevator and an observer that alerts a security system whenever the emergency stop is activated. This scenario illustrates how powerful and flexible a design incorporating command and observer patterns can be. 🦾 🙂

Step 1: Define the emergency stop command

This command will immediately halt the elevator and notify all observers about the emergency stop.

class EmergencyStopCommand(Command):
def execute(self):
self.elevator.current_floor = self.elevator.current_floor # Stops any movement
self.elevator.state = "emergency stop"
self.elevator.notify_observers(f"Emergency stop engaged at floor {self.elevator.current_floor}")

Step 2: Define the security system observer

This observer will react to emergency notifications, possibly alerting security personnel or triggering other security protocols.

class SecuritySystem(Observer):
def update(self, message):
if "Emergency stop" in message:
print(f"Security Alert: {message}")
# Additional logic to alert security or handle the emergency could be added here

Step 3: Integrate the command and observer in the elevator system

Now, we’ll add the security system as an observer to the elevator and demonstrate using the emergency stop command.

# Create the elevator and attach the monitor and the security system observer
my_elevator = Elevator()
monitor = ElevatorMonitor()
my_elevator.register_observer(monitor)
security_system = SecuritySystem()
my_elevator.register_observer(security_system)

# Regular operation (moving to the fifth floor)
move_to_fifth = MoveToFloorCommand(my_elevator, 5)
my_elevator.execute_command(move_to_fifth)

# Trigger the emergency stop
emergency_stop = EmergencyStopCommand(my_elevator)
my_elevator.execute_command(emergency_stop)

# Optionally, simulate undoing the action
# my_elevator.undo_last_command()
  • Emergency Stop Command: When executed, this command halts the elevator and sets its state to “emergency stop.” It then notifies all observers, including the security system, about the situation.
  • Security System Observer: On receiving the notification, the Security System checks if the message contains “Emergency stop.” If so, it logs an alert or initiates further actions. This could include contacting emergency services, notifying building management, or activating additional safety measures.

Benefits of this setup:

  • Rapid Response: The system allows for immediate actions in response to emergencies, enhancing safety. 💨
  • Decoupled Design: The elevator control logic is separate from the security response system, making the system easier to manage and adapt. New safety features or different types of emergency responses can be added without altering the core elevator control logic. 👌
  • Extensibility and Flexibility: Additional observers or commands can be integrated without disrupting existing functionality. For example, we could add more observers for maintenance or safety audits. 🤓

This setup provides a robust example of how integrating the command and observer patterns can create a sophisticated, responsive system capable of efficiently handling complex interactions and emergency scenarios. But there’s more! 😃 Integrating the command queue pattern is an excellent way to manage and schedule commands executed on the elevator, allowing for more organized and controlled operations. Let’s do it! 🦾

Command queue pattern:

The command queue pattern enables us to queue up commands that must be executed and process them individually, which is particularly useful in managing multiple requests in systems like elevators.

Step 1: Define the command queue

First, we need to establish a queue mechanism within the Elevator class that can handle the enqueueing and execution of commands. The Elevator class, which acts as the scheduler or controller, handles the queue. This separation of concerns keeps the implementation clean and adheres to the single responsibility principle, where:

  • Each Command is responsible only for defining an action and executing it.
  • The Elevator manages the lifecycle and order of command executions via the queue.
from queue import Queue
import time

class Elevator(Subject):
def __init__(self):
super().__init__()
self.current_floor = 0
self.state = "waiting"
self.command_history = [] # Initialize the command history
# it stores the commands that need to be executed:
self.command_queue = Queue()

def execute_command(self, command):
command.execute()
self.command_history.append(command)

def undo_last_command(self):
if self.command_history:
last_command = self.command_history.pop()
last_command.undo()

# The enqueue_command method adds commands to the queue:
def enqueue_command(self, command):
self.command_queue.put(command)

# The process_commands method retrieves and executes each command from the queue.
# This simulates handling commands sequentially:
def process_commands(self):
while not self.command_queue.empty():
command = self.command_queue.get()
command.execute()
time.sleep(1) # Simulate time between commands for clarity

This code integrates several design patterns: the command pattern, the observer pattern, and a command queue for managing command execution. It’s a complex system; we now will understand how these patterns work together! 🙂

  • from queue import Queue: This imports the Queue class from Python’s standard library, which provides a thread-safe queue implementation. It's used here to store and manage commands that need to be executed.
  • import time: This handles time-related functions, such as pausing execution between command processing with time.sleep(1), simulating the real-time delay between command executions.

Step 2: Modify commands to suit the queue system

To ensure commands work well within a command queue, we must ensure each command is self-contained and manages its execution independently. This generally means ensuring:

  1. Encapsulation: Each command should encapsulate all the information needed for its execution. For example, MoveToFloorCommand contains the target floor.
  2. State Management: Each command should manage its execution state, ideally not relying on a shared state that could lead to race conditions or inconsistencies when commands are processed out of immediate sequence.

Let’s look at our actual commands:

class MoveToFloorCommand(Command):
def __init__(self, elevator, target_floor):
super().__init__(elevator)
self.target_floor = target_floor
self.previous_floor = elevator.current_floor

def execute(self):
print(f"Starting at floor {self.elevator.current_floor}")
while self.elevator.current_floor != self.target_floor:
if self.elevator.current_floor < self.target_floor:
self.elevator.current_floor += 1
else:
self.elevator.current_floor -= 1
# Notify observers about the movement to the new floor
self.elevator.notify_observers(f"Moving to floor {self.elevator.current_floor}")
print(f"Moving to floor {self.elevator.current_floor}")
time.sleep(1) # Simulate time delay of moving floors

# Notify observers on arrival
self.elevator.notify_observers(f"Arrived at floor {self.elevator.current_floor}")
print(f"Arrived at floor {self.elevator.current_floor}")
self.elevator.open_door()
time.sleep(1) # Simulate doors open
self.elevator.close_door()

def undo(self):
print(f"Returning to floor {self.previous_floor}")
while self.elevator.current_array != self.previous_floor:
if self.elevator.current_floor < self.previous_floor:
self.elevator.current_floor += 1
else:
self.elevator.current_floor -= 1
# Notify observers about the movement to the previous floor
self.elevator.notify_observers(f"Returning to floor {self.elevator.current_floor}")
print(f"Moving to floor {self.elevator.current_floor}")
time.sleep(1) # Simulate time delay of moving floors
print(f"Returned to floor {self.elevator.current_floor}")

The MoveToFloorCommand we've defined encapsulates the functionality needed to move the elevator to a specific floor. Let's evaluate it against the criteria suitable for queueing within a command queue system. 🔍

Evaluation criteria:

  1. Encapsulation of Necessary Data: Our command class correctly encapsulates all necessary data, including the target floor and a reference to the elevator. This is good because it contains all the information the command needs to execute. 😃
  2. Self-Contained Execution: The command handles its execution from start to finish, including updating the elevator’s current floor and notifying observers at each step. This is excellent as it ensures the command is fully responsible for its process, a critical aspect of queue suitability. 👌
  3. Independence from Other Commands: Each execution of MoveToFloorCommand manages its state transition based on the elevator's current position independently of other commands. This independence is crucial for commands in a queue because it avoids dependencies that could lead to conflicts or require synchronization. 🙂
  4. State Management: The command modifies the state of the elevator (current_floor, doors opening/closing) and uses the elevator's method to notify observers. This design means the command is tightly integrated with the elevator's state but does not interfere with the operation of other commands, which might also modify the elevator's state. However, this could be an issue if multiple commands are processed rapidly and state changes overlap. To handle this, we should ensure that commands are processed in a way that respects the physical limitations and safety requirements of an elevator system (e.g., we cannot move to another floor until the doors are closed). 🤓

So, our MoveToFloorCommand meets the conditions for inclusion in a command queue system. It is self-contained, encapsulates necessary data, and manages the elevator state independently.

Note: To further improve it, we can consider adding safety checks, error handling, and ensuring thread safety if our application is multi-threaded. These enhancements will ensure that our command functions correctly within a command queue and contribute to the overall reliability and safety of the elevator system. ⚠️

Step 3: Create a command processing loop

Goal: To continuously process commands from the queue until either the queue is empty or the system is told to stop processing.

The current method processes commands only until the queue is empty. However, we might want a way to continuously process commands as they come, even after the queue becomes temporarily empty. To do this, we can add a control mechanism to start and stop the processing actively:

import threading
from queue import Queue
import time

class Elevator(Subject):
def __init__(self):
super().__init__()
self.current_floor = 0
self.state = "waiting"
self.command_history = []
self.command_queue = Queue()
self.is_active = True # Control flag for the processing loop

def execute_command(self, command):
command.execute()
self.command_history.append(command)

def undo_last_command(self):
if self.command_history:
last_command = self.command_history.pop()
last_command.undo()

def enqueue_command(self, command):
self.command_queue.put(command)

def process_commands(self):
while self.is_active:
if not self.command_queue.empty():
command = self.command_queue.get()
command.execute()
time.sleep(1) # Simulate time between commands for clarity
else:
time.sleep(0.1) # Sleep shortly to prevent spinning too fast when the queue is empty

def stop_processing(self):
self.is_active = False
  • The import threading statement includes Python's built-in threading module in our script. The threading module allows us to create, manage, and run threads in our Python application. Threads are separate, independent flows of execution within a single program. Using threads allows our program to run multiple operations concurrently, which can be very useful in scenarios like handling real-time data, performing background tasks, or keeping an interactive application responsive. In the context of our Elevator system, threading allows the command processing loop to run in the background, independent of other operations (such as UI updates or incoming command requests). This prevents the command processing from blocking other activities in our application.
  • Continuous Processing: The loop continues indefinitely until self.is_active is set to False. This allows the elevator to stay ready to process commands that may be enqueued at any time.
  • Efficient Queue Checking: The loop checks the queue and only processes a command if the queue is not empty. If empty, it sleeps briefly to prevent the loop from consuming too much CPU time by repeatedly checking an empty queue.

💡 Here’s how threads are typically used:

  • Creating a Thread: A new thread is created by instantiating threading.Thread, passing the target function (the function that should run in the background) as a parameter.
  • Starting a Thread: The thread begins with the start() method, which tells Python to execute the function specified in the background.
  • Waiting for a Thread to Complete: A thread's join() method waits for the thread to finish, which blocks further execution until the thread's target function completes.
processor_thread = threading.Thread(target=elevator.process_commands)
processor_thread.start()
# Now, the `elevator.process_commands()` runs in the background, allowing other tasks to be handled simultaneously.

This setup is especially valuable in applications that must maintain high responsiveness or handle multiple tasks simultaneously. For example, while the elevator processes commands, our system might still need to respond to user inputs, update displays, or perform other critical operations without delay. 🫡

Step 4: Integrate the command queue in a real-world simulation

Finally, we want to see this system in action. Here’s how we could set it up and test it:

  1. Setup: Create an elevator instance, register observers, and enqueue several commands.
  2. Execution: Start the command processing thread.
  3. Observation: Monitor outputs, which could be logs or direct print statements.

Example setup:

elevator = Elevator()
monitor = ElevatorMonitor()
elevator.register_observer(monitor)
security_system = SecuritySystem()
elevator.register_observer(SecuritySystem())

# Enqueue commands
elevator.enqueue_command(MoveToFloorCommand(elevator, 1))
elevator.enqueue_command(EmergencyStopCommand(elevator))
elevator.enqueue_command(MoveToFloorCommand(elevator, 5))

# Start processing in a background thread
processing_thread = threading.Thread(target=elevator.process_commands)
processing_thread.start()
processing_thread.join() # Join thread if the end of the program might terminate it

# Optionally, stop processing commands
elevator.stop_processing()

This integration allows us to see how commands are processed, how the elevator moves floor to floor, handles emergencies, and notifies observers about different events — all happening through a queue managed in a separate thread for efficient asynchronous processing.

Let’s now summarize in one place all our code that uses the command pattern, observer pattern, and command queue pattern:

import threading
from queue import Queue
import time


class Elevator(Subject):
def __init__(self):
super().__init__()
self.current_floor = 0
self.state = "waiting"
self.command_history = []
self.command_queue = Queue()
self.is_active = True # Control flag for the processing loop

def execute_command(self, command):
command.execute()
self.command_history.append(command)

def undo_last_command(self):
if self.command_history:
last_command = self.command_history.pop()
last_command.undo()

def enqueue_command(self, command):
self.command_queue.put(command)

def process_commands(self):
while self.is_active:
if not self.command_queue.empty():
command = self.command_queue.get()
command.execute()
time.sleep(1) # Simulate time between commands for clarity
else:
time.sleep(0.1) # Sleep shortly to prevent spinning too fast when the queue is empty

def stop_processing(self):
self.is_active = False

class Observer:
def update(self, message):
pass

class Subject:
def __init__(self):
self._observers = []

def register_observer(self, observer):
self._observers.append(observer)

def remove_observer(self, observer):
self._observers.remove(observer)

def notify_observers(self, message):
for observer in self._observers:
observer.update(message)


class ElevatorMonitor(Observer):
def update(self, message):
print(f"Elevator status update: {message}")


class SecuritySystem(Observer):
def update(self, message):
if "Emergency stop" in message:
print(f"Security Alert: {message}")
# Additional logic to alert security or handle the emergency could be added here


class Command:
def __init__(self, elevator):
self.elevator = elevator

def execute(self):
pass

def undo(self):
pass


class EmergencyStopCommand(Command):
def execute(self):
self.elevator.current_floor = self.elevator.current_floor # Stops any movement
self.elevator.state = "emergency stop"
self.elevator.notify_observers(f"Emergency stop engaged at floor {self.elevator.current_floor}")


class OpenDoorCommand(Command):
def execute(self):
print("Doors opening...")
self.elevator.notify_observers("Doors are opening.")
time.sleep(1) # Simulate door opening time

def undo(self):
print("Undo opening doors.")
self.elevator.notify_observers("Undo door opening.")

class CloseDoorCommand(Command):
def execute(self):
print("Doors closing...")
self.elevator.notify_observers("Doors are closing.")
time.sleep(1) # Simulate door closing time

def undo(self):
print("Undo closing doors.")
self.elevator.notify_observers("Undo door closing.")

class MoveToFloorCommand(Command):
def __init__(self, elevator, target_floor):
super().__init__(elevator)
self.target_floor = target_floor
self.previous_floor = elevator.current_floor

def execute(self):
print(f"Starting at floor {self.elevator.current_floor}")
while self.elevator.current_floor != self.target_floor:
if self.elevator.current_floor < self.target_floor:
self.elevator.current_floor += 1
else:
self.elevator.current_floor -= 1
self.elevator.notify_observers(f"Moving to floor {self.elevator.current_floor}")
print(f"Moving to floor {self.elevator.current_floor}")
time.sleep(1) # Simulate time delay of moving floors

self.elevator.enqueue_command(OpenDoorCommand(self.elevator))
self.elevator.enqueue_command(CloseDoorCommand(self.elevator))

def undo(self):
print(f"Returning to floor {self.previous_floor}")
while self.elevator.current_floor != self.previous_floor:
if self.elevator.current_floor < self.previous_floor:
self.elevator.current_floor += 1
else:
self.elevator.current_floor -= 1
self.elevator.notify_observers(f"Returning to floor {self.elevator.current_floor}")
print(f"Moving to floor {self.elevator.current_floor}")
time.sleep(1) # Simulate time delay of moving floors


# Example Setup:

elevator = Elevator()
monitor = ElevatorMonitor()
elevator.register_observer(monitor)
security_system = SecuritySystem()
elevator.register_observer(security_system)

# Enqueue commands
elevator.enqueue_command(MoveToFloorCommand(elevator, 1))
elevator.enqueue_command(EmergencyStopCommand(elevator))
elevator.enqueue_command(MoveToFloorCommand(elevator, 5))

# Start processing in a background thread
processing_thread = threading.Thread(target=elevator.process_commands)
processing_thread.start()
processing_thread.join() # Join thread if the end of the program might terminate it

# Optionally, stop processing commands
elevator.stop_processing()

Well, our current elevator system implementation nicely integrates several design patterns! 🙂 Let’s review how each component fits into our previously mentioned patterns and how we can incorporate the strategy and state patterns.

Current Patterns Review

  1. Command Pattern: MoveToFloorCommand and EmergencyStopCommand encapsulate actions and their parameters. Each command has an execute() method, making it a good use of the command pattern.
  2. Command Queue Pattern: The Elevator class uses a queue to manage commands, processing them sequentially. This aligns well with the command queue pattern, which helps schedule tasks.
  3. Observer pattern: The Elevator class, acting as a Subject, notifies multiple observers (ElevatorMonitor, SecuritySystem) about state changes, which they react to accordingly. This is a textbook implementation of the observer pattern.

State pattern:

The state pattern can help manage the elevator’s state more efficiently. Currently, we use a simple string attribute to track the state, but this can become cumbersome as the number of states grows and their interactions become more complex. 🥴

Step 1: Define common operations for different states (like move, stop, emergency).

First, we must define an interface for the elevator’s different states (e.g., Moving, Idle, EmergencyStopped). This interface will include methods representing common operations the elevator needs to perform in any state.

Questions to Consider: What operations does every elevator state need to handle? Common ones include enter, exit, move_to_floor, andhandle_emergency.

Step 2: Implement specific behaviors for each state (e.g., Moving, Idle, EmergencyStopped).

Here’s the Python code for the state interface and a basic implementation for each state:

class ElevatorState:
def enter(self, elevator):
pass

def exit(self, elevator):
pass

def handle_command(self, elevator, command):
pass

def handle_emergency(self, elevator):
pass

class IdleState(ElevatorState):
def enter(self, elevator):
print("Elevator is idle.")

def handle_command(self, elevator, command):
command.execute()
if isinstance(command, MoveToFloorCommand):
elevator.change_state(MovingState())

def handle_emergency(self, elevator):
elevator.change_state(EmergencyState())

class MovingState(ElevatorState):
def enter(self, elevator):
print(f"Moving to floor {elevator.current_floor}")

def handle_command(self, elevator, command):
command.execute()
elevator.change_state(IdleState())

def exit(self, elevator):
print(f"Arrived at floor {elevator.current_floor}")

class EmergencyState(ElevatorState):
def enter(self, elevator):
print("Emergency state activated.")

def exit(self, elevator):
print("Emergency state deactivated.")

Step 3: Use Elevator to transition between states and delegate state-specific requests to the current state object.

Let’s integrate state management into the Elevator class and summarize the code now:

import threading
from queue import Queue
import time

class Elevator(Subject):
def __init__(self):
super().__init__()
self.current_floor = 0
self.state = IdleState()
self.state.enter(self)
self.command_history = []
self.command_queue = Queue()
self.is_active = True # Control flag for the processing loop

def execute_command(self, command):
self.state.handle_command(self, command)
self.command_history.append(command)
self.notify_observers(f"Command executed: {command}")

def undo_last_command(self):
if self.command_history:
last_command = self.command_history.pop()
last_command.undo()
self.notify_observers(f"Command undone: {last_command}")

def enqueue_command(self, command):
self.command_queue.put(command)

def process_commands(self):
while self.is_active:
if not self.command_queue.empty():
command = self.command_queue.get()
self.execute_command(command)
time.sleep(1) # Simulate time between commands
else:
time.sleep(0.1) # Prevent spinning too fast when the queue is empty

def change_state(self, new_state):
self.state.exit(self)
self.state = new_state
self.state.enter(self)

def handle_emergency(self):
self.state.handle_emergency(self)

def stop_processing(self):
self.is_active = False


class ElevatorState:
def enter(self, elevator):
pass

def exit(self, elevator):
pass

def handle_command(self, elevator, command):
pass

def handle_emergency(self, elevator):
pass

class IdleState(ElevatorState):
def enter(self, elevator):
print("Elevator is idle.")

def handle_command(self, elevator, command):
command.execute()
if isinstance(command, MoveToFloorCommand):
elevator.change_state(MovingState())

def handle_emergency(self, elevator):
elevator.change_state(EmergencyState())

class MovingState(ElevatorState):
def enter(self, elevator):
print(f"Moving to floor {elevator.current_floor}")

def handle_command(self, elevator, command):
command.execute()
elevator.change_state(IdleState())

def exit(self, elevator):
print(f"Arrived at floor {elevator.current_floor}")

class EmergencyState(ElevatorState):
def enter(self, elevator):
print("Emergency state activated.")

def exit(self, elevator):
print("Emergency state deactivated.")

class Observer:
def update(self, message):
pass

class Subject:
def __init__(self):
self._observers = []

def register_observer(self, observer):
self._observers.append(observer)

def remove_observer(self, observer):
self._observers.remove(observer)

def notify_observers(self, message):
for observer in self._observers:
observer.update(message)

class ElevatorMonitor(Observer):
def update(self, message):
print(f"Elevator status update: {message}")

class SecuritySystem(Observer):
def update(self, message):
if "Emergency stop" in message:
print(f"Security Alert: {message}")
# Additional logic to alert security or handle the emergency could be added here

class Command:
def __init__(self, elevator):
self.elevator = elevator

def execute(self):
pass

def undo(self):
pass

class EmergencyStopCommand(Command):
def execute(self):
self.elevator.current_floor = self.elevator.current_floor # Stops any movement
self.elevator.state = "emergency stop"
self.elevator.notify_observers(f"Emergency stop engaged at floor {self.elevator.current_floor}")

class OpenDoorCommand(Command):
def execute(self):
print("Doors opening...")
self.elevator.notify_observers("Doors are opening.")
time.sleep(1) # Simulate door opening time

def undo(self):
print("Undo opening doors.")
self.elevator.notify_observers("Undo door opening.")

class CloseDoorCommand(Command):
def execute(self):
print("Doors closing...")
self.elevator.notify_observers("Doors are closing.")
time.sleep(1) # Simulate door closing time

def undo(self):
print("Undo closing doors.")
self.elevator.notify_observers("Undo door closing.")

class MoveToFloorCommand(Command):
def __init__(self, elevator, target_floor):
super().__init__(elevator)
self.target_floor = target_floor
self.previous_floor = elevator.current_floor

def execute(self):
print(f"Starting at floor {self.elevator.current_floor}")
while self.elevator.current_floor != self.target_floor:
if self.elevator.current_floor < self.target_floor:
self.elevator.current_floor += 1
else:
self.elevator.current_floor -= 1
self.elevator.notify_observers(f"Moving to floor {self.elevator.current_floor}")
print(f"Moving to floor {self.elevator.current_floor}")
time.sleep(1) # Simulate time delay of moving floors

self.elevator.enqueue_command(OpenDoorCommand(self.elevator))
self.elevator.enqueue_command(CloseDoorCommand(self.elevator))

def undo(self):
print(f"Returning to floor {self.previous_floor}")
while self.elevator.current_floor != self.previous_floor:
if self.elevator.current_floor < self.previous_floor:
self.elevator.current_floor += 1
else:
self.elevator.current_floor -= 1
self.elevator.notify_observers(f"Returning to floor {self.elevator.current_floor}")
print(f"Moving to floor {self.elevator.current_floor}")
time.sleep(1) # Simulate time delay of moving floors

# Example Setup:

elevator = Elevator()
monitor = ElevatorMonitor()
elevator.register_observer(monitor)
security_system = SecuritySystem()
elevator.register_observer(security_system)

# Enqueue commands
elevator.enqueue_command(MoveToFloorCommand(elevator, 1))
elevator.enqueue_command(EmergencyStopCommand(elevator))
elevator.enqueue_command(MoveToFloorCommand(elevator, 5))

# Start processing in a background thread
processing_thread = threading.Thread(target=elevator.process_commands)
processing_thread.start()
processing_thread.join() # Join thread if the end of the program might terminate it

# Optionally, stop processing commands
elevator.stop_processing()

Command Implementation Evaluation:

  • The implementation of each command encapsulates the necessary details to perform specific actions, like moving the elevator to a designated floor or opening and closing doors. Each command affects the elevator’s state and interacts with its environment, which aligns with the command pattern.
  • However, there might be an area for improvement in error handling within each command execution. For example, what happens if the elevator receives a command to move to a non-existent floor? Adding checks and balances could make the system more robust.

State Behavior Encapsulation:

  • Each state class (IdleState, MovingState, EmergencyState) defines and handles behavior relevant to those states, such as managing transitions and executing state-specific actions.
  • However, ensuring that all exit conditions are cleanly handled further enhances the transition between states. For instance, ensuring that any state-related cleanup or setup (like notifying observers of state exits) is consistently implemented might improve the system’s reliability.

Observer Communication Clarity:

  • The communication between the Elevator and its observers appears adequate for the scenarios covered, such as status updates during normal operations and emergencies.
  • To enhance this, we must consider implementing different updates or more detailed messages depending on observer needs. For instance, security systems might benefit from more detailed alerts or different information than currently provided.

Command Queue Handling:

  • The command queue pattern is effectively implemented, ensuring commands are executed in the order they are received. The loop within process_commands checks the queue's state and processes commands accordingly.
  • One potential improvement could be adding mechanisms to prioritize specific commands, like emergency stops, which might need to interrupt the normal command flow. This would involve a more sophisticated queuing strategy.

Strategy pattern:

Let’s consider where to apply the strategy pattern in our elevator code. The strategy pattern involves defining a family of algorithms, encapsulating each as a separate class, and making them interchangeable. This lets you change the algorithms independently from the clients that use them.

Which elevator behaviors could change frequently and be good candidates for using the strategy pattern? 🙂

Our code already uses some elements of the state pattern to control the different states of the elevator. Still, if we want to implement a strategy pattern, we can create strategies for different algorithms for moving the elevator between floors. For example, we can develop various strategies for floor selection depending on the current elevator load or time of day.

Let’s implement this by starting by defining a strategy interface for moving the elevator:

class MoveStrategy(ABC):
@abstractmethod
def move(self, elevator, target_floor):
pass

Now, let us add specific implementations of this strategy. For example, the standard relocation strategy and the relocation strategy prioritizing high floors during peak hours:

class StandardMoveStrategy(MoveStrategy):
def move(self, elevator, target_floor):
print(f"Standard move to floor {target_floor}")
while elevator.current_floor != target_floor:
if elevator.current_floor < target_floor:
elevator.current_floor += 1
else:
elevator.current_floor -= 1
elevator.notify_observers(f"Moving to floor {elevator.current_floor}")
print(f"Moving to floor {elevator.current_floor}")
time.sleep(1)

class PeakHoursMoveStrategy(MoveStrategy):
def move(self, elevator, target_floor):
print(f"Peak hours move to floor {target_floor}")
# Implement a strategy that perhaps moves faster or skips certain floors
while elevator.current_floor != target_floor:
if elevator.current_floor < target_floor:
elevator.current_floor += 2 # Moves two floors at a time
else:
elevator.current_floor -= 2
elevator.notify_observers(f"Moving to floor {elevator.current_floor}")
print(f"Moving to floor {elevator.current_floor}")
time.sleep(1)

Now we modify the MoveToFloorCommand class to use the strategy:

class MoveToFloorCommand(Command):
def __init__(self, elevator, target_floor, strategy: MoveStrategy):
super().__init__(elevator)
self.target_floor = target_floor
self.strategy = strategy

def execute(self):
print(f"Starting move from floor {self.elevator.current_floor} to {self.target_floor}")
self.strategy.move(self.elevator, self.target_floor)
self.elevator.enqueue_command(OpenDoorCommand(self.elevator))
self.elevator.enqueue_command(CloseDoorCommand(self.elevator))

def undo(self):
print(f"Returning to floor {self.previous_floor}")
while self.elevator.current_floor != self.previous_floor:
if self.elevator.current_floor < self.previous_floor:
self.elevator.current_floor += 1
else:
self.elevator.current_floor -= 1
self.elevator.notify_observers(f"Returning to floor {self.elevator.current_floor}")
print(f"Moving to floor {self.elevator.current_floor}")
time.sleep(1) # Simulate time delay of moving floors

We can now choose our strategy when creating a command:

# Example usage
peak_hours_strategy = PeakHoursMoveStrategy()
standard_strategy = StandardMoveStrategy()

elevator.enqueue_command(MoveToFloorCommand(elevator, 3, standard_strategy))
elevator.enqueue_command(MoveToFloorCommand(elevator, 10, peak_hours_strategy))

This will allow us to flexibly manage elevator behavior by changing strategies based on context, such as the time of day or the current elevator workload.

The final version of the code:

import threading
from queue import Queue
import time

class Elevator(Subject):
def __init__(self):
super().__init__()
self.current_floor = 0
self.state = IdleState()
self.state.enter(self)
self.command_history = []
self.command_queue = Queue()
self.is_active = True # Control flag for the processing loop

def execute_command(self, command):
self.state.handle_command(self, command)
self.command_history.append(command)
self.notify_observers(f"Command executed: {command}")

def undo_last_command(self):
if self.command_history:
last_command = self.command_history.pop()
last_command.undo()
self.notify_observers(f"Command undone: {last_command}")

def enqueue_command(self, command):
self.command_queue.put(command)

def process_commands(self):
while self.is_active:
if not self.command_queue.empty():
command = self.command_queue.get()
self.execute_command(command)
time.sleep(1) # Simulate time between commands
else:
time.sleep(0.1) # Prevent spinning too fast when the queue is empty

def change_state(self, new_state):
self.state.exit(self)
self.state = new_state
self.state.enter(self)

def handle_emergency(self):
self.state.handle_emergency(self)

def stop_processing(self):
self.is_active = False

class ElevatorState:
def enter(self, elevator):
pass

def exit(self, elevator):
pass

def handle_command(self, elevator, command):
pass

def handle_emergency(self, elevator):
pass

class IdleState(ElevatorState):
def enter(self, elevator):
print("Elevator is idle.")

def handle_command(self, elevator, command):
command.execute()
if isinstance(command, MoveToFloorCommand):
elevator.change_state(MovingState())

def handle_emergency(self, elevator):
elevator.change_state(EmergencyState())

class MovingState(ElevatorState):
def enter(self, elevator):
print(f"Moving to floor {elevator.current_floor}")

def handle_command(self, elevator, command):
command.execute()
elevator.change_state(IdleState())

def exit(self, elevator):
print(f"Arrived at floor {elevator.current_floor}")

class EmergencyState(ElevatorState):
def enter(self, elevator):
print("Emergency state activated.")

def exit(self, elevator):
print("Emergency state deactivated.")

class Observer:
def update(self, message):
pass

class Subject:
def __init__(self):
self._observers = []

def register_observer(self, observer):
self._observers.append(observer)

def remove_observer(self, observer):
self._observers.remove(observer)

def notify_observers(self, message):
for observer in self._observers:
observer.update(message)

class ElevatorMonitor(Observer):
def update(self, message):
print(f"Elevator status update: {message}")

class SecuritySystem(Observer):
def update(self, message):
if "Emergency stop" in message:
print(f"Security Alert: {message}")
# Additional logic to alert security or handle the emergency could be added here

class Command:
def __init__(self, elevator):
self.elevator = elevator

def execute(self):
pass

def undo(self):
pass

class EmergencyStopCommand(Command):
def execute(self):
self.elevator.current_floor = self.elevator.current_floor # Stops any movement
self.elevator.state = "emergency stop"
self.elevator.notify_observers(f"Emergency stop engaged at floor {self.elevator.current_floor}")

class OpenDoorCommand(Command):
def execute(self):
print("Doors opening...")
self.elevator.notify_observers("Doors are opening.")
time.sleep(1) # Simulate door opening time

def undo(self):
print("Undo opening doors.")
self.elevator.notify_observers("Undo door opening.")

class CloseDoorCommand(Command):
def execute(self):
print("Doors closing...")
self.elevator.notify_observers("Doors are closing.")
time.sleep(1) # Simulate door closing time

def undo(self):
print("Undo closing doors.")
self.elevator.notify_observers("Undo door closing.")


class MoveStrategy(ABC):
@abstractmethod
def move(self, elevator, target_floor):
pass


class StandardMoveStrategy(MoveStrategy):
def move(self, elevator, target_floor):
print(f"Standard move to floor {target_floor}")
while elevator.current_floor != target_floor:
if elevator.current_floor < target_floor:
elevator.current_floor += 1
else:
elevator.current_floor -= 1
elevator.notify_observers(f"Moving to floor {elevator.current_floor}")
print(f"Moving to floor {elevator.current_floor}")
time.sleep(1)

class PeakHoursMoveStrategy(MoveStrategy):
def move(self, elevator, target_floor):
print(f"Peak hours move to floor {target_floor}")
# Implement a strategy that perhaps moves faster or skips certain floors
while elevator.current_floor != target_floor:
if elevator.current_floor < target_floor:
elevator.current_floor += 2 # Moves two floors at a time
else:
elevator.current_floor -= 2
elevator.notify_observers(f"Moving to floor {elevator.current_floor}")
print(f"Moving to floor {elevator.current_floor}")
time.sleep(1)


class MoveToFloorCommand(Command):
def __init__(self, elevator, target_floor, strategy: MoveStrategy):
super().__init__(elevator)
self.target_floor = target_floor
self.strategy = strategy

def execute(self):
print(f"Starting move from floor {self.elevator.current_floor} to {self.target_floor}")
self.strategy.move(self.elevator, self.target_floor)
self.elevator.enqueue_command(OpenDoorCommand(self.elevator))
self.elevator.enqueue_command(CloseDoorCommand(self.elevator))

def undo(self):
print(f"Returning to floor {self.previous_floor}")
while self.elevator.current_floor != self.previous_floor:
if self.elevator.current_floor < self.previous_floor:
self.elevator.current_floor += 1
else:
self.elevator.current_floor -= 1
self.elevator.notify_observers(f"Returning to floor {self.elevator.current_floor}")
print(f"Moving to floor {self.elevator.current_floor}")
time.sleep(1) # Simulate time delay of moving floors

# Example Setup:

elevator = Elevator()
monitor = ElevatorMonitor()
elevator.register_observer(monitor)
security_system = SecuritySystem()
elevator.register_observer(security_system)

# Enqueue commands
elevator.enqueue_command(MoveToFloorCommand(elevator, 1))
elevator.enqueue_command(EmergencyStopCommand(elevator))
elevator.enqueue_command(MoveToFloorCommand(elevator, 5))

# Start processing in a background thread
processing_thread = threading.Thread(target=elevator.process_commands)
processing_thread.start()
processing_thread.join() # Join thread if the end of the program might terminate it

# Optionally, stop processing commands
elevator.stop_processing()

# Example usage
peak_hours_strategy = PeakHoursMoveStrategy()
standard_strategy = StandardMoveStrategy()

elevator.enqueue_command(MoveToFloorCommand(elevator, 3, standard_strategy))
elevator.enqueue_command(MoveToFloorCommand(elevator, 10, peak_hours_strategy))

Our code implements a sophisticated simulation of an elevator system using multiple design patterns, each serving different aspects of functionality and maintainability. Thank you for reading! 🙂

--

--

Efim Shliamin

Proficient Computer Scientist, B.Sc., with expertise in Software Engineering & Data Science, adept in solving complex biological and medical software problems.