-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathVendingMachineSample.cs
More file actions
181 lines (157 loc) · 6.61 KB
/
VendingMachineSample.cs
File metadata and controls
181 lines (157 loc) · 6.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
using FunctionalStateMachine.CommandRunner;
using Microsoft.Extensions.DependencyInjection;
using VendingMachineSampleApp.Configuration;
using VendingMachineSampleApp.Domain;
namespace VendingMachineSampleApp;
/// <summary>
/// Interactive vending machine sample demonstrating the functional state machine pattern.
///
/// Key characteristics of the functional approach:
/// - Data is NOT stored in the state machine
/// - Fire() takes current state, data, and trigger
/// - Fire() returns new state, new data, and commands
/// - The caller is responsible for maintaining state and data
/// - State machine is pure - no side effects during transitions
///
/// This demonstrates:
/// - Complete state machine with complex guards and state transitions
/// - Multiple command types with different handlers
/// - Dependency injection with automatic handler scanning
/// - Real-world domain modeling with state data mutation
/// </summary>
public class VendingMachineSample
{
public static void Main()
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.Clear();
DisplayWelcome();
// Initialize inventory
var inventory = new Dictionary<string, VendingItem>
{
["A1"] = new("A1", "Crisps", 1.50m, 5),
["A2"] = new("A2", "Snickers", 0.75m, 10),
["B1"] = new("B1", "Fanta", 2.00m, 3),
["B2"] = new("B2", "Water", 1.00m, 8),
["C1"] = new("C1", "Biscuits", 1.25m, 4)
};
// Set up dependency injection to register command handlers
var services = new ServiceCollection();
var exitSignal = new ExitSignal();
RegisterCommandHandlers(services, inventory, exitSignal);
var provider = services.BuildServiceProvider();
// Get the dispatcher for VendingMachineCommand
var dispatcher = provider.GetRequiredService<ICommandDispatcher<VendingMachineCommand>>();
// Build the state machine (pure - no data yet)
var machine = VendingMachineBuilder.BuildMachine();
// Initialize machine data (data is EXTERNAL to the state machine)
var machineData = VendingMachineData.Initialize(inventory);
var currentState = VendingMachineState.Idle;
// Create mutable wrapper to pass through async calls
var session = new MachineSession { CurrentState = currentState, CurrentData = machineData };
dispatcher.Run([new ShowInventoryCommand()]);
// Start interactive session
RunInteractiveSession(machine, dispatcher, session, exitSignal);
}
/// <summary>
/// Registers all command handlers using dependency injection with automatic discovery.
/// Uses AddCommandRunners to scan for and register all ICommandRunner implementations.
/// </summary>
private static void RegisterCommandHandlers(
IServiceCollection services,
Dictionary<string, VendingItem> inventory,
ExitSignal exitSignal)
{
// Register inventory as singleton for handlers that need it
services.AddSingleton(inventory);
services.AddSingleton(exitSignal);
services.AddCommandRunners<VendingMachineCommand>();
}
/// <summary>
/// Displays welcome screen with ASCII art.
/// </summary>
private static void DisplayWelcome()
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("╔════════════════════════════════════════════════════════════╗");
Console.WriteLine("║ Welcome to the Vending Machine Simulator! ║");
Console.WriteLine("╚════════════════════════════════════════════════════════════╝");
Console.WriteLine();
Console.ResetColor();
}
/// <summary>
/// Displays the current inventory with item codes and prices.
/// </summary>
/// <summary>
/// Runs the interactive vending machine session.
/// Data and state are maintained by the caller through the MachineSession wrapper and passed to Fire().
/// This demonstrates the functional state machine pattern.
/// </summary>
private static void RunInteractiveSession(
FunctionalStateMachine.Core.StateMachine<VendingMachineState, VendingMachineTrigger, VendingMachineData, VendingMachineCommand> machine,
ICommandDispatcher<VendingMachineCommand> dispatcher,
MachineSession session,
ExitSignal exitSignal)
{
while (!exitSignal.ShouldExit)
{
Console.ForegroundColor = ConsoleColor.White;
Console.Write("Enter command: ");
Console.ResetColor();
var input = Console.ReadLine()?.Trim().ToUpperInvariant() ?? "";
var trigger = ParseTrigger(input, session.CurrentData.Inventory);
try
{
(session.CurrentState, session.CurrentData, var commands) =
machine.Fire(trigger, session.CurrentState, session.CurrentData);
dispatcher.Run(commands);
}
catch (InvalidOperationException)
{
dispatcher.Run([
new DisplayMessageCommand("That action is not valid right now.")
]);
}
if (exitSignal.ShouldExit)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("👋 Thank you for using the vending machine!");
Console.ResetColor();
break;
}
}
}
private static VendingMachineTrigger ParseTrigger(string input, Dictionary<string, VendingItem> inventory)
{
if (string.IsNullOrWhiteSpace(input))
{
return new InvalidInputTrigger(input);
}
if (input == "EXIT")
{
return new ExitTrigger();
}
if (input == "HELP")
{
return new ShowInventoryTrigger();
}
if (input == "CANCEL")
{
return new CancelTrigger();
}
if (decimal.TryParse(input, out var amount) && amount > 0)
{
return new InsertMoneyTrigger(amount);
}
if (input.Length == 2 && inventory.ContainsKey(input))
{
return new SelectItemTrigger(input);
}
return new InvalidInputTrigger(input);
}
}
public class MachineSession
{
public VendingMachineState CurrentState { get; set; }
public required VendingMachineData CurrentData { get; set; }
}