10/1/16

Designing a Vending Machine

I interact with Vending machines at work every single day, to purchase some snacks or get a drink and so on. I have always wanted to implement one just out of curiosity but never got around to doing it. Then one day I got an email from a software consulting firm asking me if i was interested in interviewing with them and if so, I should do a code exercise and send it back to them. One of the coding exercise options turned out to be a vending machine. It had a set of features that were expected. Though I had no interest in interviewing with that company, I decided that I was going to code the Vending Machine for fun anyways.

Following were the vending machine features in that coding exercise:
  • Accept Coins
    • As a vendor I want a vending machine that accepts coins So that I can collect money from the customer. The vending machine will accept valid coins (nickels, dimes, and quarters) and reject invalid ones (pennies). When a valid coin is inserted the amount of the coin will be added to the current amount and the display will be updated. When there are no coins inserted, the machine displays INSERT COIN. Rejected coins are placed in the coin return. 
    • NOTE: The temptation here will be to create Coin objects that know their value. However, this is not how a real vending machine works. Instead, it identifies coins by their weight and size and then assigns a value to what was inserted. You will need to do something similar
  • Select Product
    • As a vendor I want customers to select products So that I can give them an incentive to put money in the machine. 
    • There are three products: cola for $1.00, chips for $0.50, and candy for $0.65. When the respective button is pressed and enough money has been inserted, the product is dispensed and the machine displays THANK YOU. If the display is checked again, it will display INSERT COIN and the current amount will be set to $0.00. If there is not enough money inserted then the machine displays PRICE and the price of the item and subsequent checks of the display will display either INSERT COIN or the current amount as appropriate.
  • Make Change
    • As a vendor I want customers to receive correct change So that they will use the vending machine again.
    • When a product is selected that costs less than the amount of money in the machine, then the remaining amount is placed in the coin return.
  • Return Coins
    • As a customer I want to have my money returned So that I can change my mind about buying stuff from the vending machine.
    • When the return coins button is pressed, the money the customer has placed in the machine is returned and the display shows INSERT COIN.
  • Sold Out
    • As a customer I want to be told when the item I have selected is not available So that I can select another item.
    • When the item selected by the customer is out of stock, the machine displays SOLD OUT. If the display is checked again, it will display the amount of money remaining in the machine or INSERT COIN if there is no money in the machine.
  • Exact Change Only
    • As a customer I want to be told when exact change is required So that I can determine if I can buy something with the money I have before inserting it.
    • When the machine is not able to make change with the money in the machine for any of the items that it sells, it will display EXACT CHANGE ONLY instead of INSERT COIN.

My implementation included the following classes:

Client: This class represents the end user that will be interacting with the Vending Machine.

public class Client
    {
        public void Start()
        {
            var vm = new VendingMachineFacade(new CoinService(),new ProductService(new ProductRepository(),new ProductInventoryRepository()));

            //case 1 - invalid coins
            var response = vm.AcceptCoin(new InputCoin() { Weight = 1000, Size = 50 });
            if(response.IsSuccess == false)
            {
                //print response.Message to console
                return;
            }

            //case 2 - valid coins,return coins
            vm.AcceptCoin(new InputCoin() { Weight = 100, Size = 50 });
            vm.ReturnCoins();

            //case 3 - valid coins, invalid product code
            vm.AcceptCoin(new InputCoin() { Weight = 100, Size = 50 });
            vm.SelectProduct("COOO1");

            //case 4 - valid coins, valid product code, exact change only
            vm.AcceptCoin(new InputCoin() { Weight = 100, Size = 50 });
            vm.SelectProduct("CO1");

            //case 5 - valid coins, valid product code, less amount entered
            vm.AcceptCoin(new InputCoin() { Weight = 100, Size = 50 });
            vm.SelectProduct("CO1");

            //case 6 - valid coins, valid product code, more amount entered, make change
            vm.AcceptCoin(new InputCoin() { Weight = 100, Size = 50 });
            vm.SelectProduct("CO1");

            //case 7 - valid coins, valid product code, correct(>=) amount entered, sold out
            vm.AcceptCoin(new InputCoin() { Weight = 100, Size = 50 });
            vm.SelectProduct("CO1");

            //case 8 - valid coins, valid product code, correct(>=) amount entered, sold out, return coins
            vm.AcceptCoin(new InputCoin() { Weight = 100, Size = 50 });
            vm.SelectProduct("CO1");
            vm.ReturnCoins();
        }
    }

VendingMachineFacade :  This class represents the vending machine interface that the client will be interacting with.

public class VendingMachineFacade
    {
        private double _cost;
        private CoinService _coinService;
        private ProductService _productService;
        public VendingMachineFacade(CoinService coinService,ProductService prodService)
        {
            _coinService = coinService;
            _productService = prodService;
        }

        //behaviors
        public VendingResponse AcceptCoin(InputCoin coin)
        {
            VendingResponse response = new VendingResponse();                       

            //check if the values correspond to an accepted coin            
            var currentCoin = _coinService.GetCoin(coin.Weight, coin.Size);

            //not a valid coin
            if (currentCoin == null)
            {
                response.Message = "Insert Coin";
                response.IsRejectedCoin = true;
                response.RejectedCoin = coin; //return rejected coin
                return response;                                
            }

            //valid coin
            _cost += currentCoin.Value;
            response.Message = _cost.ToString();
            response.IsRejectedCoin = false;
            return response;
        }
        public VendingResponse SelectProduct(string code)
        {
            var response = new VendingResponse();
            
            //check if the code is valid            
            //if no, return error object with details
            var product = _productService.GetProduct(code);

            //invalid code entered
            if(product == null)
            {
                response.Message = "Invalid Product Selected. Please try again";
                response.IsSuccess = false;
                return response;
            }

            //no coins entered, but selection pressed
            if (_cost == 0)
            {
                //if exact change item, message = "exact change only"
                response.Message = "Insert Coin";
                response.IsSuccess = false;
                return response;
            }

            //entered coins less than cost
            if (_cost < product.Cost)
            {
                response.Message = string.Format("Price : {0}", product.Cost);
                response.IsSuccess = false;
                return response;
            }

            //if exact change product            
            if (_productService.IsExactChangeOnlyProduct(product) && product.Cost != _cost)
            {
                response.Message = "Exact Change Only";
                response.IsSuccess = false;
                return response;
            }

            //all good, valid code and valid amount entered
            var quantity = _productService.GetProductQuantity(code);
            if (quantity > 0)
            {
                response.Message = "Thank You";
                response.IsSuccess = true;
                _productService.UpdateProductQuantity(code);
                MakeChange(_cost - product.Cost);
                return response;
            }
            else
            {
                response.Message = "SOLD OUT";
                response.IsSuccess = false;
                return response;
            }                        
        }
        public ItemChange ReturnCoins()
        {
            return MakeChange(_cost);
        }
        private ItemChange MakeChange(double input)
        {
            ItemChange itemchange = new ItemChange();
            var change = input - _cost;
            if (change == 0) return itemchange;
            double remainingAmount = 0;

            //get the number of quarters in the remaining amount
            var quarters = (int)(change / 0.25);
            if (quarters > 0)
            {
                itemchange.NoOfQuarters = quarters;
                remainingAmount = (change - (quarters * 0.25));
                if (remainingAmount == 0) return itemchange;
            }

            var nickels = (int)(change/ 0.10);
            if(nickels > 0)
            {
                itemchange.NoOfNickels = nickels;
                remainingAmount = (change - (nickels * 0.10));
                if (remainingAmount == 0) return itemchange;
            }            

            var dimes = (int)(change / 0.05);

            if (dimes > 0)
            {
                itemchange.NoOfDimes = dimes;
                remainingAmount = (change - (dimes * 0.05));
                if (remainingAmount == 0) return itemchange;
            }
                        
            return itemchange;
        }        
        private void SoldOut(string code)
        {
            //inventory manager checks quantity
        }
        private void ExactChangeOnly() { }
    }  


InputCoin: This would represent the coin inserted by the user

 public class InputCoin
    {
        public int Weight { get; set; }
        public int Size { get; set; }
    }


CoinService :  This class provides functionality related to Coins.

  public class CoinService
    {
        private static IEnumerable AcceptedCoins = new List() { new Quarter(), new Dime(), new Nickel() };

        public Coin GetCoin(int weight, int size)
        {
            return AcceptedCoins.Where(x => x.Weight == weight && x.Size == size).FirstOrDefault();
        }
    }


ProductService :  This class provides functionality related to the actual products that can be purchased.

  public class ProductService
    {
        private ProductRepository _productRepository;
        private ProductInventoryRepository _productInventoryRepository;
        public ProductService(ProductRepository repository, ProductInventoryRepository inventoryRepository)
        {
            _productRepository = repository;
            _productInventoryRepository = inventoryRepository;
        }

        public int GetProductQuantity(string code)
        {
            var quantities = _productInventoryRepository.GetInventory();
            return quantities[code];
        }

        public Product GetProduct(string code)
        {
            return GetAllProducts().Where(x => x.Code == code).First();
        }

        public IEnumerable GetAllProducts()
        {
            return _productRepository.GetProductList();
        }

        public void UpdateProductQuantity(string code)
        {
            //this should happen in a lock to handle concurrency
            _productInventoryRepository.UpdateInventory(code);
        }

        public bool IsExactChangeOnlyProduct(Product product)
        {
            if (product.Type == ProductType.Chips) return true;
            return false;
        }
    }


ProductRepository : This class is the acts as the ORM layer for interaction with the product datasources.

 public class ProductRepository
    {
        private static List _products;
        //reader writer lock
        public virtual IEnumerable GetProductList()
        {
            if (_products == null)
            {
                _products = new List();
                _products.Add(new Product() { Code = "CO1", Type = ProductType.Cola, Cost = 1.0 });
                _products.Add(new Product() { Code = "CO2", Type = ProductType.Cola, Cost = 1.0 });
                _products.Add(new Product() { Code = "CO3", Type = ProductType.Cola, Cost = 1.0 });

                _products.Add(new Product() { Code = "CH1", Type = ProductType.Chips, Cost = 0.50 });
                _products.Add(new Product() { Code = "CH2", Type = ProductType.Chips, Cost = 0.50 });
                _products.Add(new Product() { Code = "CH3", Type = ProductType.Chips, Cost = 0.50 });

                _products.Add(new Product() { Code = "CA1", Type = ProductType.Candy, Cost = 0.65 });
                _products.Add(new Product() { Code = "CA2", Type = ProductType.Candy, Cost = 0.65 });
                _products.Add(new Product() { Code = "CA3", Type = ProductType.Candy, Cost = 0.65 });
            }

            return _products;
        }
    }
  

ProductInventoryRepository: This class is the acts as the ORM layer for interaction with the product inventory related datasources.

public class ProductInventoryRepository
    {
        private static Dictionary _productQuantities;
        public Dictionary GetInventory()
        {
            if (_productQuantities == null)
            {
                _productQuantities = new Dictionary();
                _productQuantities.Add("CO1", 10);
                _productQuantities.Add("CO2", 10);
                _productQuantities.Add("CO3", 10);

                _productQuantities.Add("CH1", 10);
                _productQuantities.Add("CH2", 10);
                _productQuantities.Add("CH3", 10);

                _productQuantities.Add("CA1", 10);
                _productQuantities.Add("CA2", 10);
                _productQuantities.Add("CA3", 10);
            }

            return _productQuantities;
        }
        public void UpdateInventory(string code)
        {
            //sorround with reader writer lock
            var currentCount = _productQuantities[code];
            if(currentCount > 0)
                _productQuantities[code]--;
        }
    }



ItemChange: This class represents the return amount


public class ItemChange
{
    public int NoOfQuarters;
    public int NoOfNickels;
    public int NoOfDimes;
}


This represents my initial thought process in regards to a Vending Machine implementation. I will continue to tweak this code as time permits and as I get better ideas,

No comments: