Lessons in this course (8)

6. Design an elevator system and a vending machine (State pattern)

Advanced★ Must knowLesson 6 of 8

Elevator: N elevators, requests from floors (up/down) and inside cars; a dispatcher assigns requests; each elevator moves in a direction serving stops in order (SCAN/"elevator algorithm").

csharp
public enum Direction { Idle, Up, Down }

public sealed class Elevator(int id)
{
    public int Id { get; } = id;
    public int Floor { get; private set; }
    public Direction Direction { get; private set; } = Direction.Idle;
    private readonly SortedSet<int> _up = [], _down = [];

    public void AddStop(int floor) { if (floor >= Floor) _up.Add(floor); else _down.Add(floor); if (Direction == Direction.Idle) Direction = floor >= Floor ? Direction.Up : Direction.Down; }

    public void Step()                                         // called on each tick
    {
        if (Direction == Direction.Up && _up.Count > 0) { Floor = _up.Min; _up.Remove(Floor); }
        else if (Direction == Direction.Down && _down.Count > 0) { Floor = _down.Max; _down.Remove(Floor); }
        Direction = _up.Count > 0 ? Direction.Up : _down.Count > 0 ? Direction.Down : Direction.Idle;
    }
    public int Cost(int floor) => Math.Abs(Floor - floor) + (Direction == Direction.Idle ? 0 : 2);
}

public interface IDispatchStrategy { Elevator Choose(IReadOnlyList<Elevator> elevators, int floor); }
public sealed class NearestElevator : IDispatchStrategy
{
    public Elevator Choose(IReadOnlyList<Elevator> e, int floor) => e.MinBy(x => x.Cost(floor))!;
}

Vending machine with the State pattern — behaviour depends on the current state; each state class handles the actions:

csharp
public interface IVendingState
{
    void InsertCoin(VendingMachine m, decimal amount);
    void SelectProduct(VendingMachine m, string code);
}

public sealed class IdleState : IVendingState
{
    public void InsertCoin(VendingMachine m, decimal amount) { m.Balance += amount; m.State = new HasMoneyState(); }
    public void SelectProduct(VendingMachine m, string code) => Console.WriteLine("Insert money first");
}

public sealed class HasMoneyState : IVendingState
{
    public void InsertCoin(VendingMachine m, decimal amount) => m.Balance += amount;
    public void SelectProduct(VendingMachine m, string code)
    {
        var p = m.Inventory[code];
        if (p.Stock == 0) { Console.WriteLine("Sold out"); return; }
        if (m.Balance < p.Price) { Console.WriteLine($"Insert {p.Price - m.Balance} more"); return; }
        p.Stock--; m.Balance -= p.Price;
        Console.WriteLine($"Dispensed {p.Name}, change {m.Balance}");
        m.Balance = 0; m.State = new IdleState();
    }
}

public sealed class Product(string name, decimal price, int stock) { public string Name = name; public decimal Price = price; public int Stock = stock; }
public sealed class VendingMachine
{
    public IVendingState State { get; set; } = new IdleState();
    public decimal Balance { get; set; }
    public Dictionary<string, Product> Inventory { get; } = [];
    public void InsertCoin(decimal amount) => State.InsertCoin(this, amount);
    public void Select(string code) => State.SelectProduct(this, code);
}