2
\$\begingroup\$

I am recently learning OOP concepts and thought that implementing a simple workflow simulating a vending machine would help me put what I've learnt into practice. I am open to criticisms and feedback, and am mainly trying to get some comments regarding:

  • Best practices for OOP
  • File structure
  • Data encapsulation
  • Unit testing practices
  • General architecture advice for OOP

The project simulates a vending machine dispensing drinks for customers for X hours (user input), given that arrival times follow a Poisson(K) distribution and an initial stock list (user input). The project also produces a sales report for the given simulation.

The link to my the entire github repository is here: https://github.com/jtsw1990/oop_vending_machine

Below is a snippet of vending_machine.py, which is the class to simulate the behavior of a typical vending machine. Any help is appreciated not just for this snippet, but the other classes in the repo as well like the customer class.

import csv
import numpy as np


class DataReader:
    '''
    Object to read in initial drinks data set
    to be used as input in VendingMachine class
    '''

    def __init__(self, filepath):
        self.df = []
        with open(filepath, "r") as file:
            my_reader = csv.reader(file, delimiter=",")
            next(my_reader)
            for row in my_reader:
                self.df.append(row)


class VendingMachine:
    '''
    Insert doc string here
    '''

    def __init__(self, max_capacity):
        print("__init__ is being called here")
        self.max_capacity = max_capacity
        self.current_earnings = 0
        self.current_stock = 0
        self.stock_list = None
        self.drinks_displayed = None

    def __repr__(self):
        print("__repr__ was being called here")
        return "VendingMachine({!r})".format(self.max_capacity)

    @property
    def max_capacity(self):
        print("max_cap property being called here")
        return self.__max_capacity

    @max_capacity.setter
    def max_capacity(self, max_capacity):
        print("max_cap setter called here")
        if not isinstance(max_capacity, (int, float)):
            raise TypeError("Please enter an integer value")
        elif max_capacity < 0:
            raise ValueError("Capacity cannot be negative")
        elif max_capacity % 1 != 0:
            raise TypeError("Please enter an integer value")
        else:
            self.__max_capacity = max_capacity

    def load_drinks(self, filepath):
        self.drink_list = DataReader(filepath).df
        if self.stock_list is None:
            self.stock_list = {
                row[0]: [float(row[1]), int(row[2])]
                for row in self.drink_list
            }
        current_stock = sum([value[-1] for key, value in self.stock_list.items()])
        if current_stock > self.max_capacity:
            raise ValueError("Loaded drinks past capacity")
        else:
            self.current_stock = current_stock

    def display_stock(self):
        self.drinks_displayed = [
            x[0] for x in list(self.stock_list.items()) if x[-1][-1] > 0
        ]
        return self.drinks_displayed

    def dispense_drink(self, drink_name):
        '''
        Method to simulate a vending machine object
        dispensing a drink. Returns drink_name as a string if available.
        Returns None if out of stock.
        '''
        try:
            if self.stock_list[drink_name][-1] > 0:
                self.stock_list[drink_name][-1] -= 1
                self.current_earnings = np.round(
                    self.current_earnings + self.stock_list[drink_name][0], 2)
                self.current_stock = sum(
                    [value[-1] for key, value in self.stock_list.items()])
                return drink_name
            else:
                return None
        except KeyError:
            print("Machine out of stock")
            return None


if __name__ == "__main__":
    test = VendingMachine(200)
    test.load_drinks("drinks_list.csv")
    test.display_stock()
    for i in range(150):
        test.dispense_drink("coke")
        test.dispense_drink("ice_lemon_tea")
        test.dispense_drink("fanta_orange")
        test.dispense_drink("fanta_grape")
        test.dispense_drink("sprite")
        test.dispense_drink("pepsi")
        test.dispense_drink("mountain_dew")
    print(test.stock_list)
    print(test.display_stock())
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

The biggest change needed here is to delete your DataReader class, replace direct use of Numpy with Pandas (which wraps Numpy), and use named columns in your dataframe instead of numeric indices.

Pandas has excellent built-in support for CSV operations and structured tabular data where column names can be used to make the code more legible and robust. For instance, you can permute the order of columns in your CSV file and none of your code would need to change.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Ahh that's a really good point, and my bad. I was initially writing this on the iPad using the Pythonista app where Pandas was not supported. Should've switched it out when changing back to PC thanks! \$\endgroup\$
    – jtsw1990
    Commented Jan 6, 2021 at 2:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.