\( \newcommand{\NOT}{\neg} \newcommand{\AND}{\wedge} \newcommand{\OR}{\vee} \newcommand{\XOR}{\oplus} \newcommand{\IMP}{\Rightarrow} \newcommand{\IFF}{\Leftrightarrow} \newcommand{\TRUE}{\text{True}\xspace} \newcommand{\FALSE}{\text{False}\xspace} \newcommand{\IN}{\,{\in}\,} \newcommand{\NOTIN}{\,{\notin}\,} \newcommand{\TO}{\rightarrow} \newcommand{\DIV}{\mid} \newcommand{\NDIV}{\nmid} \newcommand{\MOD}[1]{\pmod{#1}} \newcommand{\MODS}[1]{\ (\text{mod}\ #1)} \newcommand{\N}{\mathbb N} \newcommand{\Z}{\mathbb Z} \newcommand{\Q}{\mathbb Q} \newcommand{\R}{\mathbb R} \newcommand{\C}{\mathbb C} \newcommand{\cA}{\mathcal A} \newcommand{\cB}{\mathcal B} \newcommand{\cC}{\mathcal C} \newcommand{\cD}{\mathcal D} \newcommand{\cE}{\mathcal E} \newcommand{\cF}{\mathcal F} \newcommand{\cG}{\mathcal G} \newcommand{\cH}{\mathcal H} \newcommand{\cI}{\mathcal I} \newcommand{\cJ}{\mathcal J} \newcommand{\cL}{\mathcal L} \newcommand{\cK}{\mathcal K} \newcommand{\cN}{\mathcal N} \newcommand{\cO}{\mathcal O} \newcommand{\cP}{\mathcal P} \newcommand{\cQ}{\mathcal Q} \newcommand{\cS}{\mathcal S} \newcommand{\cT}{\mathcal T} \newcommand{\cV}{\mathcal V} \newcommand{\cW}{\mathcal W} \newcommand{\cZ}{\mathcal Z} \newcommand{\emp}{\emptyset} \newcommand{\bs}{\backslash} \newcommand{\floor}[1]{\left \lfloor #1 \right \rfloor} \newcommand{\ceil}[1]{\left \lceil #1 \right \rceil} \newcommand{\abs}[1]{\left | #1 \right |} \newcommand{\xspace}{} \newcommand{\proofheader}[1]{\underline{\textbf{#1}}} \)

11.3 A “Manager” Class

In the previous section, we defined four different data classes—Vendor, Customer, Courier, Order—to represent different entities in our food delivery system. We must now determine how to keep track of all of these entities, and how they can interact with each other. For example, a user would want to be able to look up a list of vendors in their area to order food from. In code, how does a single Customer object “know” about all the different Vendors in the system? Should each Customer have an attribute containing list of Vendors? The question of how objects “know” about other objects is similar to the notion of variable scope. A variable’s scope determines where it can be accessed in a program; the scope of an object dictates the object’s lifetime and who the object belongs to. But now consider our current problem domain, with the hundreds of food vendor and potential thousands of customers. What should the scope of all those objects be?

There are many ways to approach this problem. A common object-oriented design approach is to create a new manager class whose role is to keep track of all of the entities in the system and to mediate the interactions between them (like a customer placing a new order). This class is more complex than the others we saw in the last section, and so we will not use a data class, and instead use a general class with a custom initializer and keep most of the instance attributes private.

Here is the manager class we’ll create for our food delivery system. The FoodDeliverySystem class will store (and have access to) every customer, courier, and food vendor represented in our system.

class FoodDeliverySystem:
    """A system that maintains all entities (vendors, customers, couriers, and orders).

    Representation Invariants:
        - self.name != ''
        - all(vendor == self._vendors[vendor].name for vendor in self._vendors)
        - all(customer == self._customers[customer].name for customer in self._customers)
        - all(courier == self._couriers[courier].name for courier in self._couriers)
    """
    # Private Instance Attributes:
    #   - _vendors: a mapping from vendor name to Vendor object.
    #       This represents all the vendors in the system.
    #   - _customers: a mapping from customer name to Customer object.
    #       This represents all the customers in the system.
    #   - _couriers: a mapping from courier name to Courier object.
    #       This represents all the couriers in the system.
    #   - _orders: a list of all orders (both open and completed orders).

    _vendors: dict[str, Vendor]
    _customers: dict[str, Customer]
    _couriers: dict[str, Courier]
    _orders: list[Order]

    def __init__(self) -> None:
        """Initialize a new food delivery system.

        The system starts with no entities.
        """
        self._vendors = {}
        self._customers = {}
        self._couriers = {}
        self._orders = []

Design decisions: we are using names as keys in the _vendors, _customers, and _couriers dictionaries. This means we’re assuming these names are unique, which of course is not true in the real world!

Often applications will use a different piece of identifying information that must be unique, like a user name or email address.

Changing state

What we have done so far is model the static properties of our food delivery system, that is, the attributes that are necessary to capture a particular snapshot of the state of the system at a specific moment in time. Next, we’re going to look at how to model the dynamic properties of the system: how the entities interact with each other and cause the system state to change over time.

Adding entities

Though a FoodDeliverySystem instance starts with no entities, we can define simple methods to add entities to the system. You can picture this happening when a new vendor/customer/courier signs up for our app. By making our collection attributes private and requiring client code call these methods, we can check for uniqueness of these entity names as well.

class FoodDeliverySystem:
    ...

    def add_vendor(self, vendor: Vendor) -> bool:
        """Add the given vendor to this system.

        Do NOT add the vendor if one with the same name already exists.

        Return whether the vendor was successfully added to this system.
        """
        if vendor.name in self._vendors:
            return False
        else:
            self._vendors[vendor.name] = vendor
            return True

    def add_customer(self, customer: Customer) -> bool:
        """Add the given customer to this system.

        Do NOT add the customer if one with the same name already exists.

        Return whether the customer was successfully added to this system.
        """
        # Similar implementation to add_vendor

    def add_courier(self, courier: Courier) -> bool:
        """Add the given courier to this system.

        Do NOT add the courier if one with the same name already exists.

        Return whether the courier was successfully added to this system.
        """
        # Similar implementation to add_vendor

Placing orders

The main driving force in our simulation is customer orders. When a customer places an order, a chain of events is triggered:

  1. The order is sent to the vendor.
  2. The order is assigned to a courier.
  3. The courier travels to the vendor and picks up the food, and then brings it to the customer.
  4. Once the courier has reached their destination, they indicate that the delivery has been made.

To represent these events in our program, we need to create functions that mutate the state of the system. Where should we create these functions? We could write them as top-level functions, or as methods of one of our existing entity classes (turning that class from a data class into a general class). We have previously said that one of the roles of the FoodDeliverySystem is to mediate interactions between the various entities in the system, and so this makes it a natural class to add these mutating methods.

class FoodDeliverySystem:
    ...

    def place_order(self, order: Order) -> None:
        """Record the new given order.

        Assign a courier to this new order (if a courier is available).

        Preconditions:
            - order not in self.orders
        """

    def complete_order(self, order: Order) -> None:
        """Mark the given order as complete.

        Make the courier who was assigned this order available to take a new order.

        Preconditions:
            - order in self.orders
        """

We could then place an order from a customer using FoodDeliverySystem.place_order, which would be responsible for both recording the order and assigning a courier to that order. FoodDeliverySystem.complete_order does the opposite, marking the order as complete and un-assigning the courier so that they are free to take a new order. With both FoodDeliverySystem.place_order and FoodDeliverySystem.complete_order, we can begin to see how a simulation might take place where many customers are placing orders to different restaurants that are being delivered by available couriers.

Note that this discussion should make sense even though we haven’t implemented either of these methods. Questions like “How do we choose which courier to assign to a new order?” and “How do we mark an order as complete?” are about implementation rather than the public interface of these methods. We’ll discuss one potential implementation of these methods in lecture, but we welcome you to attempt your own implementations as an exercise.