Free Ebook cover C# Essentials: Building Console Apps with .NET

C# Essentials: Building Console Apps with .NET

New course

12 pages

Methods: Decomposing Console Apps into Reusable Parts

Capítulo 6

Estimated reading time: 8 minutes

+ Exercise

Why Methods Matter in Console Apps

As console programs grow, code written “top to bottom” in a single file becomes hard to read, test, and change. Methods let you name a piece of behavior, reuse it, and keep each part focused on one job. A good method name can replace a comment, and a well-chosen method boundary prevents unrelated details from mixing together.

A practical goal is to separate concerns: one method handles input, another performs processing, and another handles output. This makes it easier to change how you read data (for example, from the console today and from a file later) without rewriting the calculation logic.

Method Signatures: Name, Parameters, and Return Type

A method signature describes how you call a method: its name and its parameter list (and, in C#, the return type is part of the declaration). The basic shape looks like this:

static ReturnType MethodName(Type1 param1, Type2 param2) { /* ... */ }

Key parts:

  • Return type: what the method gives back to the caller. Use void when it returns nothing.
  • Method name: should describe an action or result, such as CalculateTotal or ReadInt.
  • Parameters: inputs the method needs. Parameters are local to the method.

void vs Returning a Value

Use void for methods that perform an action (often output) and don’t need to produce a value for later steps. Use a return value when the result is needed by other code.

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

static void PrintReceipt(string receiptText) { Console.WriteLine(receiptText); }
static decimal CalculateSubtotal(decimal unitPrice, int quantity) { return unitPrice * quantity; }

A common design guideline for console apps: keep most “processing” methods returning values, and keep “output” methods void. This reduces hidden dependencies and makes processing easier to reuse.

Parameters and Return Values in Practice

Single Responsibility per Method

Try to make each method do one thing:

  • Input methods: read and validate user input, return typed values.
  • Processing methods: compute results from inputs, return results.
  • Output methods: format and print results, return void.

Example: A Processing Method with Multiple Inputs

static decimal CalculateTotal(decimal subtotal, decimal taxRate) { decimal tax = subtotal * taxRate; return subtotal + tax; }

Notice that the method does not read from the console and does not print. It only calculates, which makes it reusable in other contexts.

Basic Method Overloading

Overloading means multiple methods share the same name but have different parameter lists. This can make calling code cleaner when the “concept” is the same but inputs vary.

static string FormatCurrency(decimal amount) { return amount.ToString("C"); } static string FormatCurrency(decimal amount, string currencyLabel) { return $"{currencyLabel} {amount:0.00}"; }

When you call FormatCurrency(12.5m), C# selects the overload with one parameter. When you call FormatCurrency(12.5m, "USD"), it selects the two-parameter version.

Keep overloading simple. If overloads start behaving very differently, consider different method names instead.

Step-by-Step Refactor: From Script-Like Code to Methods

Start with a single-file, linear program that mixes input, processing, and output. The goal is to extract methods in small steps while keeping the program working after each step.

Starting Point: All Logic Inline

using System; class Program { static void Main() { Console.Write("Item name: "); string itemName = Console.ReadLine() ?? ""; Console.Write("Unit price: "); decimal unitPrice = decimal.Parse(Console.ReadLine() ?? "0"); Console.Write("Quantity: "); int quantity = int.Parse(Console.ReadLine() ?? "0"); decimal subtotal = unitPrice * quantity; decimal taxRate = 0.07m; decimal total = subtotal + (subtotal * taxRate); Console.WriteLine(); Console.WriteLine($"Item: {itemName}"); Console.WriteLine($"Subtotal: {subtotal:C}"); Console.WriteLine($"Tax: {(subtotal * taxRate):C}"); Console.WriteLine($"Total: {total:C}"); } }

This works, but it is fragile: parsing can throw exceptions, and the calculation is tangled with console prompts and printing.

Step 1: Extract Processing Methods

First, move calculations into methods. This is low risk because it doesn’t change how input/output works yet.

using System; class Program { static void Main() { Console.Write("Item name: "); string itemName = Console.ReadLine() ?? ""; Console.Write("Unit price: "); decimal unitPrice = decimal.Parse(Console.ReadLine() ?? "0"); Console.Write("Quantity: "); int quantity = int.Parse(Console.ReadLine() ?? "0"); decimal subtotal = CalculateSubtotal(unitPrice, quantity); decimal taxRate = 0.07m; decimal tax = CalculateTax(subtotal, taxRate); decimal total = CalculateTotal(subtotal, tax); Console.WriteLine(); Console.WriteLine($"Item: {itemName}"); Console.WriteLine($"Subtotal: {subtotal:C}"); Console.WriteLine($"Tax: {tax:C}"); Console.WriteLine($"Total: {total:C}"); } static decimal CalculateSubtotal(decimal unitPrice, int quantity) { return unitPrice * quantity; } static decimal CalculateTax(decimal subtotal, decimal taxRate) { return subtotal * taxRate; } static decimal CalculateTotal(decimal subtotal, decimal tax) { return subtotal + tax; } }

Now the math is reusable and easier to read. The main method reads like a high-level recipe.

Step 2: Extract Output into a void Method

Next, move printing into a method. This keeps output formatting in one place.

using System; class Program { static void Main() { Console.Write("Item name: "); string itemName = Console.ReadLine() ?? ""; Console.Write("Unit price: "); decimal unitPrice = decimal.Parse(Console.ReadLine() ?? "0"); Console.Write("Quantity: "); int quantity = int.Parse(Console.ReadLine() ?? "0"); decimal subtotal = CalculateSubtotal(unitPrice, quantity); decimal taxRate = 0.07m; decimal tax = CalculateTax(subtotal, taxRate); decimal total = CalculateTotal(subtotal, tax); PrintReceipt(itemName, subtotal, tax, total); } static decimal CalculateSubtotal(decimal unitPrice, int quantity) { return unitPrice * quantity; } static decimal CalculateTax(decimal subtotal, decimal taxRate) { return subtotal * taxRate; } static decimal CalculateTotal(decimal subtotal, decimal tax) { return subtotal + tax; } static void PrintReceipt(string itemName, decimal subtotal, decimal tax, decimal total) { Console.WriteLine(); Console.WriteLine($"Item: {itemName}"); Console.WriteLine($"Subtotal: {subtotal:C}"); Console.WriteLine($"Tax: {tax:C}"); Console.WriteLine($"Total: {total:C}"); } }

PrintReceipt is void because its job is to display information, not to produce a value for later computation.

Step 3: Extract Input Methods and Add Validation

Parsing user input directly with int.Parse and decimal.Parse can fail. A common helper pattern is: prompt, read, validate, repeat until valid, then return the typed value.

using System; class Program { static void Main() { string itemName = ReadNonEmptyString("Item name: "); decimal unitPrice = ReadDecimal("Unit price: "); int quantity = ReadInt("Quantity: "); decimal subtotal = CalculateSubtotal(unitPrice, quantity); decimal taxRate = 0.07m; decimal tax = CalculateTax(subtotal, taxRate); decimal total = CalculateTotal(subtotal, tax); PrintReceipt(itemName, subtotal, tax, total); } static string ReadNonEmptyString(string prompt) { while (true) { Console.Write(prompt); string input = Console.ReadLine() ?? ""; input = input.Trim(); if (input.Length > 0) return input; Console.WriteLine("Please enter a value."); } } static decimal ReadDecimal(string prompt) { while (true) { Console.Write(prompt); string input = Console.ReadLine() ?? ""; if (decimal.TryParse(input, out decimal value)) return value; Console.WriteLine("Please enter a valid decimal number."); } } static int ReadInt(string prompt) { while (true) { Console.Write(prompt); string input = Console.ReadLine() ?? ""; if (int.TryParse(input, out int value)) return value; Console.WriteLine("Please enter a valid whole number."); } } static decimal CalculateSubtotal(decimal unitPrice, int quantity) { return unitPrice * quantity; } static decimal CalculateTax(decimal subtotal, decimal taxRate) { return subtotal * taxRate; } static decimal CalculateTotal(decimal subtotal, decimal tax) { return subtotal + tax; } static void PrintReceipt(string itemName, decimal subtotal, decimal tax, decimal total) { Console.WriteLine(); Console.WriteLine($"Item: {itemName}"); Console.WriteLine($"Subtotal: {subtotal:C}"); Console.WriteLine($"Tax: {tax:C}"); Console.WriteLine($"Total: {total:C}"); } }

Now Main is short and readable, and each method has a clear responsibility. Input methods return values; processing methods return values; output methods are void.

Step 4: Use Overloading to Reduce Duplication in Input Helpers

You can overload input methods to support optional constraints without creating many differently named methods.

static int ReadInt(string prompt) { return ReadInt(prompt, minValue: null); } static int ReadInt(string prompt, int? minValue) { while (true) { Console.Write(prompt); string input = Console.ReadLine() ?? ""; if (int.TryParse(input, out int value)) { if (minValue == null || value >= minValue.Value) return value; Console.WriteLine($"Value must be at least {minValue.Value}."); continue; } Console.WriteLine("Please enter a valid whole number."); } }

Calling code can stay simple:

int quantity = ReadInt("Quantity: ", minValue: 1);

Practice: Write Small Helper Methods

These exercises focus on writing small, reusable methods that do one job well. Try to keep each method short and test it by calling it from Main.

1) Validation Helpers

Create methods that validate values after parsing. This keeps parsing separate from business rules.

static bool IsNonNegative(decimal value) { return value >= 0m; } static bool IsInRange(int value, int minInclusive, int maxInclusive) { return value >= minInclusive && value <= maxInclusive; }

Use them inside input loops:

static decimal ReadNonNegativeDecimal(string prompt) { while (true) { decimal value = ReadDecimal(prompt); if (IsNonNegative(value)) return value; Console.WriteLine("Value must be 0 or greater."); } }

2) Formatting Helpers

Move formatting rules into methods so output stays consistent.

static string FormatLineItem(string label, decimal amount) { return $"{label,-10} {amount,10:C}"; }

Then printing becomes simpler:

Console.WriteLine(FormatLineItem("Subtotal:", subtotal));

You can overload formatting for different types:

static string FormatLineItem(string label, int amount) { return $"{label,-10} {amount,10}"; }

3) Calculation Helpers

Write calculation methods that do not read or write to the console.

static decimal ApplyDiscount(decimal subtotal, decimal discountRate) { return subtotal - (subtotal * discountRate); } static decimal CalculatePercentage(decimal baseAmount, decimal rate) { return baseAmount * rate; }

Combine them in a clear sequence:

decimal discountedSubtotal = ApplyDiscount(subtotal, 0.10m); decimal tax = CalculatePercentage(discountedSubtotal, 0.07m); decimal total = discountedSubtotal + tax;

4) Build a Small “Workflow” Method

After you have helpers, you can create a method that represents a complete operation, while still using smaller methods internally.

static void RunCheckout() { string itemName = ReadNonEmptyString("Item name: "); decimal unitPrice = ReadNonNegativeDecimal("Unit price: "); int quantity = ReadInt("Quantity: ", minValue: 1); decimal subtotal = CalculateSubtotal(unitPrice, quantity); decimal tax = CalculateTax(subtotal, 0.07m); decimal total = CalculateTotal(subtotal, tax); PrintReceipt(itemName, subtotal, tax, total); }

Main can then call RunCheckout(), keeping the entry point minimal while the program remains organized into reusable parts.

Now answer the exercise about the content:

In a refactored console app that separates input, processing, and output into methods, which approach best supports reuse and reduces hidden dependencies?

You are right! Congratulations, now go to the next page

You missed! Try again.

Processing methods should only compute and return results, making them reusable in other contexts. Output methods can be void because they just display information, keeping I/O separate from calculations.

Next chapter

Collections and Basic Data Processing

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.