From Spaghetti to Structure: Separating Menu Logic with a MenuManager in C#

A beginner’s journey from a 500-line Program.cs to cleaner, separated code.This comes from my learnings so far and sharing what’s starting to click as I build The Enchanted Garden, a cozy C# game to learn object-oriented programming


The Problem: Program.cs Knows Too Much

When I started coding, I did what many beginners do:

I put everything in Program.cs.

And I mean everything. Well, most everything.

Displaying menus. Reading player input. Validating that input. Looping when the player typed something wrong. Handling what happens after they pick an option. It all lived in one giant file, and it grew to over 500 lines fast.

Here’s an example from my market menu:

while (visitingMarket)
{
Console.WriteLine("visit the market");
market.showSeeds();
Console.WriteLine("which would you like");

if (int.TryParse(Console.ReadLine(), out seedinput) && (seedinput >= 1 && seedinput <= 4))
{
Plant selectedPlant = market.availableSeeds[seedinput - 1];
market.BuySeed(selectedPlant, person);

Console.WriteLine("do you want to make another purchase: Y/N");
string purchaseAgain = Console.ReadLine();

if (purchaseAgain == "Y")
{
visitingMarket = true;
}
else visitingMarket = false;
}
else
{
Console.WriteLine("try again");
}
}

This one block is doing at least four different jobs at once:

  • Displaying the menu
  • Reading and validating input
  • Looping on bad input
  • Handling the actual game logic after a choice is made

And this pattern repeated for every menu in the game. The garden had it. The market had it. The Bramble dialogue had it. All tangled together in Program.cs.


The Insight: Menus Are Their Own Thing

When I started rebuilding the project, I asked a simple question:

What if menus were their own class?

What if something like a MenuManager handled menu flow: displaying options, getting valid input, and returning the next action?

Here’s what that looks like for the main menu:

public string Manage(string action)
{
Menu menu;
List<MenuOption> menuOptions = new List<MenuOption>();
string choice;

switch(action)
{
case "MainMenu":
menuOptions.Add(new MenuOption(1, "Visit the Garden (water, check plants)", "Garden"));
menuOptions.Add(new MenuOption(2, "Go to Bramble (quests & talk)", "Bramble"));
menuOptions.Add(new MenuOption(3, "Visit the Market (buy seeds, sell harvest)", "Market"));
menuOptions.Add(new MenuOption(4, "Check on Pip (your garden creature)", "Pip"));
menuOptions.Add(new MenuOption(5, "End the Day (advance time)", "EndDay"));
menuOptions.Add(new MenuOption(6, "Time to Quit", "Quit"));

menu = new Menu("What will you do today: ", menuOptions);
menu.Display();
choice = menu.GetChoice();

if (choice == "TryAgain") return "MainMenu";
return choice;
}
}

Notice what MenuManager does not know about:

  • Plants
  • Gold
  • The player
  • What happens after a choice is made

Its job is to handle menu flow: display options, get valid input, and return the next action.

Right now I’m just passing strings like "Garden" or "Market" around. It’s simple and works for this stage of the project, even though there are likely more structured ways to handle this as things grow.


Why This Matters: Separation of Concerns

This idea has a name: separation of concerns.

Each part of your code should have a clear responsibility and not try to do everything at once.

Before:

What was happeningWhere it lived
Display market menuProgram.cs
Validate market inputProgram.cs
Handle buying logicProgram.cs
Display garden menuProgram.cs
Validate garden inputProgram.cs
Handle watering logicProgram.cs

After:

What happensWhere it lives
Display menusMenuManager
Validate inputMenuManager
Handle buying logicProgram.cs (or Market logic)
Handle watering logicProgram.cs (or Garden logic)

Menus are isolated. Game logic is isolated. They don’t need to know about each other.


Testability

One thing I didn’t expect when I started thinking about this: cleaner structure makes testing easier.

In the original version, if I wanted to test whether watering a plant worked, I had to run the whole game, navigate menus, and click through multiple steps just to reach that point.

With things separated, I can start testing pieces like MenuManager more independently, and I can focus on specific parts of the game without running everything together.

For a beginner, this feels abstract. But the moment you’re debugging something buried in 500 lines of mixed logic, the value of isolation becomes very real.


The Baton Pattern: Passing the Action Forward

Here’s what Program.cs looks like now:

string action = "MainMenu";

while(true)
{
var menuManager = new MenuManager();
action = menuManager.Manage(action);
}

That’s the entire game loop so far.

Program.cs doesn’t handle menu logic anymore. It just keeps the loop running and passes the current action forward.

Whatever comes back from MenuManager becomes the next step in the loop.

It’s a simple handoff pattern: Program.cs keeps things moving, while MenuManager handles the interaction.


What This Taught Me (as Someone Who Works with Engineers)

I’m not an engineer by trade: I just find them.😅
But actually building something, even at a small level, changed how I think about technical experience as a recruiter.

A few things clicked for me:

1. “Knowing a language” doesn’t mean much without context
It’s easy to say “this person knows C#” or “this person knows Python.” But building even a simple project shows how different it is to actually use a language to structure something, make decisions, and handle edge cases. There’s a big gap between familiarity and ownership.

2. There usually isn’t one “right” way to do something
Even in this small refactor, there were multiple ways I could’ve approached it. Concepts like Separation of Concerns aren’t strict rules: they’re guidelines that help you make better tradeoffs depending on the situation.

3. Building something exposes what you actually understand
Everything feels straightforward until you try to implement it. That’s when things break, assumptions get tested, and you realize what you don’t fully understand yet. That gap between “I get it” and “I built it” is where most of the real learning happens. It gave me a much better appreciation for what engineers are actually doing day-to-day and how to think about experience beyond just tools or keywords.

Never Miss a New Post

Get the latest posts and tips delivered straight to your inbox.

I don’t spam! Read my privacy policy for more info.

Leave a Reply

Your email address will not be published. Required fields are marked *