I previously asked for a review for a Vending Machine implementing State Pattern in this linked post.
And received an answer from @radarbob. The code below is my revised attempt based on some of @radarbob's answer.
Notes / Changes from my original attempt:
- VendingMachine internals no longer public
- protected properties instead
- Abstract VendingMachine base, with Concrete VendingMachine with virtual methods allowing for extensibility
- Products/Inventory by composition
- No Writelines in Domain
Original Question:
I was asked to implement a Vending Machine in a recent interview coding challenge. My attempt at a solution is based on the state pattern.
I would like to get some code reviews on this bearing in mind I would have about 40 minutes to implement this in the interview scenario. When reviewing I would like you to consider how I have done in relation to SOLID principles.
And also consider that the requirements from the interview are:
- Create a vending machine library
- Consumers of the library can list products
- Ability to select product
- After product selected, ability to insert coins
- When enough coins are submitted, product and change dispensed
- Ability to cancel
- Dont worry about stock levels
- Only one consumer at a time
- Think about extensibility
public abstract class VendingMachineBase
{
protected IState State;
protected List<Product> Products;
protected int MoneyInserted;
protected int ProductChosen;
protected VendingMachineBase(List<Product> products)
{
_ = products ?? throw new ArgumentNullException(nameof(products), "You must provide a product inventory");
Products = products;
}
public void ChangeState(IState state)
{
State = state;
}
public List<Product> ListProducts()
{
return Products;
}
}
public interface IState
{
(Product, int) DoWork();
}
public class VendingMachine : VendingMachineBase
{
public VendingMachine(List<Product> products) :base(products)
{
MoneyInserted = 0;
State = new AwaitingChoice(this);
}
public virtual void MakeChoice(int productId)
{
ProductChosen = productId;
State.DoWork();
}
public virtual int GetChosenProductId()
{
return ProductChosen;
}
public virtual void InsertMoney(int amount)
{
MoneyInserted += amount;
State.DoWork();
}
public virtual int GetMoneyInsertedAmount()
{
return MoneyInserted;
}
public virtual (Product, int) DispenseItem()
{
return State.DoWork();
}
public virtual int RefundMoney()
{
State = new Canceling(this);
var (_, refund) = State.DoWork();
MoneyInserted = 0;
return refund;
}
public virtual void ResetMachine()
{
MoneyInserted = 0;
ProductChosen = 0;
}
}
public class AwaitingChoice : IState
{
private readonly VendingMachine _vendingMachine;
public AwaitingChoice(VendingMachine vendingMachine)
{
_vendingMachine = vendingMachine;
_vendingMachine.ResetMachine();
}
public (Product, int) DoWork()
{
_vendingMachine.ChangeState(new AwaitingMoney(_vendingMachine));
return (null, 0);
}
}
public class AwaitingMoney : IState
{
private readonly VendingMachine _vendingMachine;
private readonly Product _productChosen;
public AwaitingMoney(VendingMachine vendingMachine)
{
_vendingMachine = vendingMachine;
_productChosen = _vendingMachine.ListProducts().FirstOrDefault(x => x.Id == _vendingMachine.GetChosenProductId());
}
public (Product, int) DoWork()
{
if (_vendingMachine.GetMoneyInsertedAmount() >= _productChosen.Price)
{
_vendingMachine.ChangeState(new Dispensing(_vendingMachine));
}
return (null, 0);
}
}
public class Dispensing : IState
{
private readonly VendingMachine _vendingMachine;
private readonly Product _productChosen;
public Dispensing (VendingMachine vendingMachine)
{
_vendingMachine = vendingMachine;
_productChosen = _vendingMachine.ListProducts().FirstOrDefault(x => x.Id == _vendingMachine.GetChosenProductId());
}
public (Product, int) DoWork()
{
var returningChange = _vendingMachine.GetMoneyInsertedAmount() > _productChosen.Price
? _vendingMachine.GetMoneyInsertedAmount() - _productChosen.Price
: 0;
_vendingMachine.ChangeState(new AwaitingChoice(_vendingMachine));
return (_productChosen, returningChange);
}
}
public class Canceling : IState
{
private readonly VendingMachine _vendingMachine;
public Canceling(VendingMachine vendingMachine)
{
_vendingMachine = vendingMachine;
}
public (Product, int) DoWork()
{
var refund = _vendingMachine.GetMoneyInsertedAmount();
_vendingMachine.ChangeState(new AwaitingChoice(_vendingMachine));
return (null, refund);
}
}
public class Product
{
public int Id { get; }
public string Name { get; }
public int Price { get; }
public Product(int id, string name, int price)
{
Id = id;
Name = name;
Price = price;
}
}
class Program
{
static void Main(string[] args)
{
//worker installs vending machine and populates products
var products = new List<Product>()
{
new Product(1, "Guinness", 3),
new Product(2, "Tayto", 2)
};
var vendingMachine = new VendingMachine(products);
//Customer uses Vending Machine
//Choose Product
vendingMachine.MakeChoice(1);
//Insert money
vendingMachine.InsertMoney(1);
//Not enough, insert more
vendingMachine.InsertMoney(3);
var (product, change) = vendingMachine.DispenseItem();
Console.WriteLine($"Got my {product.Name}, change is {change}.");
//Choose Product
vendingMachine.MakeChoice(2);
//Insert money
vendingMachine.InsertMoney(1);
//Ah, no coins left, I will cancel
var refund = vendingMachine.RefundMoney();
Console.WriteLine($"Got my refund {refund}.");
Console.ReadLine();
}
}