Sitemap

Structured Programming

19 min readMar 27, 2026

--

Press enter or click to view image in full size

Do you want to bring order to a chaotic codebase or just learn how to write better code? Behold, an ancient paradigm that was introduced in the 1960s–70s, and still is not falling behind others — structured programming.

Today, you will see why conditional logic plays a crucial role in programming, but also creates complexity in your code. We will explore why if/else statements may not always be the best choice. You will see different examples of what techniques could be used to simplify them, starting from simple ones and gradually progressing to more advanced approaches.

If you are interested in other programming paradigms, look here:

Meanwhile,

if (YouAreInterested()) 
{
LetUsBegin();
}

Structured Programming

As was already mentioned, structured programming was born in 60s. The program back then was simple; still, developers would write sh💩tty code. This paradigm was intended to bring greater clarity to the code.

It is based on the idea that code flow can be described only by three control statements:

  • sequence — code expressions that run one after another in a predictable top-to-bottom order
  • iteration — loops like for, while, do...while, until
  • conditions — branching using if, else, switch to choose between alternative paths

Of course, different programming languages offer various constructs, but most of them are just syntactic sugar aimed at reducing duplication: if you want to avoid repeating code, you extract a function; if you want to avoid repeating functions, you create a class; if you want to avoid repeating code across types, you use generics, and so on.

These features simply help us group and reuse code more effectively — nothing fundamentally new that we couldn’t achieve before. But in its very core, every program is just a bunch of expressions, loops, and conditions. Therefore, it is important to understand how to organize control flow cleanly.

Structured programming is a programming paradigm that emphasizes organizing code into clear, logical structures.

Its goal is to make programs easier to understand, test, maintain, and reason about.

Finding a Complexity

Clean code, readability, simplified maintenance, and similar ideals are highly subjective. What one developer considers “clean”, another may see as over-engineered.

The real problem is that complexity rarely disappears; it just moves somewhere else. Sometimes abstraction hides it, sometimes patterns shift it, but it never vanishes completely. It is really hard to tell where complexity is hidden.

So, let’s try to find out:

— sequence

Simple expressions — math operations, variable assignments, basic method calls — are already as clear as they can be. There’s a limit to how much you can “simplify” something that is inherently simple.

What does help is following a few practical suggestions that improve readability:

  • keep statements straightforward — avoid clever one-liners that obscure intent
// ❌
if (!string.IsNullOrWhiteSpace(user?.Name ?? "")) DoSomething(user.Name.Trim());

// ✅
if (user == null) return;
if (string.IsNullOrWhiteSpace(user.Name)) return;

DoSomething(user.Name.Trim());
  • one expression per line — each expression is isolated, easy to understand, and trivial to debug — no hidden complexity inside a giant line
// ❌
var result = (a + b) * GetFactor() - c;

// ✅
var sum = a + b;
var multiplied = sum * GetFactor();
var result = multiplied - c;
  • one . operator per line when chaining — code should be read top-to-bottom, not left-to-right across a long horizontal chain
// ❌
var result = users.Where(u => u.IsActive).OrderBy(u => u.Name).Select(u => u.Email).ToList();

// ✅
var result = users
.Where(u => u.IsActive)
.OrderBy(u => u.Name)
.Select(u => u.Email)
.ToList();

Not strict rules, just some recommendations, to make your code easier for the next developer to follow 🙃.

— iteration

In the past, people used to implement loops with goto operator. This often resulted in “spaghetti code” — code that jumps unpredictably across the program and becomes extremely hard to follow. But almost no one uses it today anyway, so it’s hardly relevant 😅.

Nowadays, loops are straightforward as hell and do not require to be even more simplified.

However, there is another operator that can cause “jumps” in flow: throw. Throws in one method can be handled higher up the call stack, potentially far from the original source, which makes the control flow less explicit and sometimes harder to follow.

Structured programming suggests avoiding exceptional states and explicitly covering all cases, as can be done using the Result pattern:

var result = Math.Divide(2, 0);

// handle division by zero
if (!result.IsSuccess)
{
return result.Error;
}

// continue flow execution
var value = result.Value;
. . .

— conditions

The if/else statement is, without exaggeration, one of the most powerful tools you can learn in programming. It lets your program change its execution flow and behave differently depending on the situation. This is where real logic begins.

To analyze how complex that logic becomes, developers often use Cyclomatic Complexity — a metric that represents your program’s control flow as a graph.

Every action (function call, assignment, operation) is a node, and the transitions between them are edges. Each new if adds another branch, making this graph more complex.

The formula is simple:

Complexity = Edges – Nodes + 2

The metric is not ideal, but it is a good start. There are the following categorizations of cyclomatic complexity:

  • 1–10: simple function, little risk
  • 11–20: more complex, moderate risk
  • 21–50: complex, high risk
  • > 50: untestable code, very high risk

⚠️Notice⚠️

cyclomatic complexity is calculated only within a single function. Even if that function calls another method with high internal complexity, that entire call is still just one node

Long story short, the vast majority of complexity in software is created by conditions. That’s why the ability to write clear, readable decision logic is such an essential skill.

It’s one of the things that separates a junior developer from a senior one — clear, intentional, well-structured condition statements 😉. And this is exactly what we are going to learn today.

Dealing with Complexity

The goal of this paradigm is to keep logic simple and straightforward: one instruction after another, with minimal changes in the execution paths, minimal nesting.

Two common ways to achieve this are:

  • invert the condition — handle edge cases early to simplify the main flow
  • extract and move code — pull out chunks of logic into separate functions to reduce clutter

Let’s see it in practice.

⚠️ Disclaimer ⚠️

To anticipate the obvious comment that “these examples are simple enough and if/else would work,” I want to clarify: these are just training examples 🙃.

I’m not going to bring in a full codebase just to demonstrate how to refactor. Shame on you for criticizing me over something this trivial 🫵!

Phew😮‍💨 Now we can start… I hope 😊

Ternary Operator

When you only need to choose between two values, an if/else block can feel unnecessarily long:

public string GetStatusMessage(bool isOnline)
{
if (isOnline)
{
return "User is online";
}
else
{
return "User is offline";
}
}

In simple cases, a ternary operator can express the same idea more concisely:

public string GetStatusMessage(bool isOnline)
{
return isOnline ? "User is online" : "User is offline";
}

Early Return

In the example below, we have created two different execution flows.

public string CheckNumberSign(int number)
{
if (number > 0)
{
return "Positive";
}
else
{
return "Non-positive";
}
}

The else branch is technically unnecessary because the first return already exits the method. Keeping it adds an extra nesting level and makes the code harder to read, especially when more conditions appear later.

A cleaner approach is to eliminate redundant branches and let the method “fail fast”:

public string CheckNumberSign(int number)
{
if (number <= 0)
{
return "Non-positive";
}

// the rest of the code

return "Positive";
}

By removing the else, the intent becomes clearer: we handle the exceptional or early case first, then continue with the main logic.

This pattern is one of the most important to master. It simplifies control flow and makes the method easier to extend and maintain.

Guards

Validating input at the start of a method is essential, but repetitive if checks can clutter the main logic:

public void CreateUser(User user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}

Console.WriteLine($"Creating user {user.Name}");
}

The Guard can be used to validate method arguments in a streamlined manner, which is also faster, less verbose, more expressive, and less error-prone than manually writing checks and throwing exceptions.

public void CreateUser(User user)
{
ArgumentNullException.ThrowIfNull(user);

Console.WriteLine($"Creating user {user.Name}");
}

Extracting Logic into Separate Methods

When logic inside each branch becomes complex, the method can quickly become cluttered.

public string CheckNumberSign(int number)
{
if (number > 0)
{
if (number % 2 == 0)
{
return "Positive Even";
}
else
{
return "Positive Odd";
}
}
else
{
if (number == 0)
{
return "Zero";
}
else
{
return "Negative";
}
}
}

A good way to simplify and improve readability is to extract that logic into separate methods:

public string CheckNumberSign(int number)
{
if (number > 0)
{
return GetPositive(number);
}
else
{
return GetNonPositive(number);
}
}

private string GetPositive(int number)
{
if (number % 2 == 0)
{
return "Positive Even";
}
else
{
return "Positive Odd";
}
}

private string GetNonPositive(int number)
{
if (number == 0)
{
return "Zero";
}
else
{
return "Negative";
}
}

This approach helps name intentions instead of just describing conditions and reduces the cyclomatic complexity of the main function.

Each method now clearly defines its behaviour, making it easier to read and extend, while the main function serves as an entry point.

Avoid Nested Code

Another common issue appears when a method doesn’t return a value but performs an action instead. Often, you can end up in a deeply nested if statements, that make code harder to read and maintain:

public void Register(User user)
{
if (user != null)
{
if (user.IsActive)
{
if (!string.IsNullOrEmpty(user.Email))
{
if (user.Age >= 18)
{
if (user.HasAcceptedTerms)
{
user.IsRegistered = true;
}
else
{
throw new Exception($"User {user.Id} must accept terms and conditions");
}
}
else
{
throw new Exception($"User {user.Id} is too young to register");
}
}
}
}
}

This pyramid of conditions hides the main logic and increases cognitive load.

A cleaner approach would be to reverse some conditions and apply the early returns approach:

public void Register(User user)
{
if (user is null)
{
return;
}

if (!user.IsActive)
{
return;
}

if (string.IsNullOrEmpty(user.Email))
{
return;
}

if (user.Age < 18)
{
throw new Exception($"User {user.Id} is too young to register");
}

if (!user.HasAcceptedTerms)
{
throw new Exception($"User {user.Id} must accept terms and conditions");
}

// finish with all verification
// continue with a main flow

user.IsRegistered = true;
}

The code now reads top-down, with no indentation traps or mental branching — handle the special case first, then move on with the main logic.

In general, if your entire block is wrapped in an if, flip the condition to avoid nesting and keep the condition close to its early-exit path.

Merge If Statements

When multiple conditions block execution in a similar way or has the same business value, you can merge them into a single if statement to reduce nesting and improve readability:

public void Register(User user)
{
// merge conditions
if (user is null ||
!user.IsActive ||
string.IsNullOrEmpty(user.Email))
{
return;
}

if (user.Age < 18)
{
throw new Exception($"User {user.Id} is too young to register");
}

if (!user.HasAcceptedTerms)
{
throw new Exception($"User {user.Id} must accept terms and conditions");
}

user.IsRegistered = true;
}

This keeps the checks concise and readable while preserving the business rules.

By the way, this refactoring can also be done with multiple nested ifs:

public void Register(User user)
{
if (user != null)
{
if (user.IsActive)
{
if (!string.IsNullOrEmpty(user.Email))
{
user.IsRegistered = true;
}
}
}
}

Extracting Complex Conditions into Variables

When multiple conditions are combined in a single if statement, readability can suffer:

public void CheckAccess(User user)
{
if (user.IsActive && user.IsAdmin && !user.IsBanned)
{
Console.WriteLine("Access granted");
}
}

All the logic is crammed into one line, making it harder to quickly understand what grants access and what blocks it.

A cleaner approach is to extract the condition into a well-named variable:

public void CheckAccess(User user)
{
bool hasAccess = user.IsActive && user.IsAdmin && !user.IsBanned;

if (!hasAccess)
{
return;
}

Console.WriteLine("Access granted");
}

The hasAccess variable documents the intent.

Extract the Condition Into a Descriptive Method

Complex conditions can quickly make if statements hard to read, especially when multiple checks are combined:

public void ProcessOrder(Order order)
{
if (order.IsPaid && !order.IsCancelled && order.Items.Count > 0)
{
Console.WriteLine("Processing order");
}
}

All the business logic is buried inside the if, making it harder to understand at a glance.

A better approach is to extract the condition into a descriptive method:

public void ProcessOrder(Order order)
{
if (!CanProcess(order))
{
return;
}

Console.WriteLine("Processing order");
}

private bool CanProcess(Order order)
{
return order.IsPaid &&
!order.IsCancelled &&
order.Items.Count > 0;
}

This version improves readability by separating decision logic from action.

Get iamprovidence’s stories in your inbox

Join Medium for free to get updates from this writer.

The CanProcess() method corresponds to business logic naming, clearly communicates what conditions are required for processing, and the main method reads like a simple, top-down narrative.

Replace if/else with Switch

When a condition depends on multiple discrete values, a long if/else can get cumbersome:

public void PrintDayType(DayOfWeek day)
{
if (day == DayOfWeek.Saturday)
{
Console.WriteLine("Weekend");
}
else if (day == DayOfWeek.Sunday)
{
Console.WriteLine("Weekend");
}
else
{
Console.WriteLine("Weekday");
}
}

This works, but the logic becomes harder to extend as more cases are added.

A switch statement provides a clearer structure:

public void PrintDayType(DayOfWeek day)
{
switch (day)
{
case DayOfWeek.Saturday:
case DayOfWeek.Sunday:
Console.WriteLine("Weekend");
break;
default:
Console.WriteLine("Weekday");
break;
}
}

Or you can even make use of a switch expression:

public void PrintDayType(DayOfWeek day)
{
var dayType = day switch
{
DayOfWeek.Saturday => "Weekend",
DayOfWeek.Sunday => "Weekend",

_ => "Weekday"
};

Console.WriteLine(dayType);
}

The switch groups related cases together and separates different branches explicitly.

It scales better when handling multiple values and improves readability, showing at a glance the condition logic.

Use a Dictionary for Lookup

When mapping discrete inputs to outputs, switch or multiple if/else statements can become verbose, harder to maintain, and duplicated in multiple places:

public string GetEnvironmentUrl(string environment)
{
switch (environment)
{
case "Development":
return "https://dev.example.com";
case "Staging":
return "https://staging.example.com";
case "Production":
return "https://www.example.com";

default:
return "Unknown environment";
}
}

public string GetEnvironmentColor(string environment)
{
switch (environment)
{
case "Development":
return "green";
case "Staging":
return "yellow";
case "Production":
return "blue";

default:
return "Unknown environment";
}
}

A more concise and scalable approach is to use a dictionary with value or configuration objects:

private static readonly Dictionary<string, EnvConfig> EnvironmentConfig = new()
{
["Development"] = new EnvConfig("https://dev.example.com", "green"),
["Staging"] = new EnvConfig("https://staging.example.com", "yellow"),
["Production"] = new EnvConfig("https://www.example.com", "blue"),
};

public string GetEnvironmentUrl(string environment)
{
return EnvironmentConfig.GetValueOrDefault(environment)?.Url ?? "Unknown environment";
}

public string GetEnvironmentColor(string environment)
{
return EnvironmentConfig.GetValueOrDefault(environment)?.Color ?? "Unknown environment";
}

This approach improves readability, reduces boilerplate, and makes it easier to add/update or remove settings in the future by encapsulating them in a single place.

Pattern Matching

A long chain of if/else statements with type checks can often be simplified using pattern matching:

var message = order switch
{
OnlineOrder { IsPriority: true, Amount: var amt } => $"Priority online order of ${amt}",
{ IsPriority: false, Amount: var amt } => $"Regular online order of ${amt}",
InStoreOrder { StoreLocation: "New York" } => "In-store order at New York store",
InStoreOrder { StoreLocation: var loc } => $"In-store order at {loc} store",

_ => "Unknown order type"
};

This keeps all cases in one place, reduces boilerplate, improves readability, and allows comparing objects that aren’t bound by inheritance, such as domain models and DTOs.

Personally, I’m not a huge fan of pattern matching 🙃. It often feels like a workaround for poor design. I’d usually recommend moving behavior into the types themselves using polymorphism, which is a more robust long-term solution.

LINQ Approaches

Sometimes, if/else chains appear when processing collections or filtering data.

Using LINQ can simplify the logic by expressing the intent declaratively instead of imperatively.

public List<Order> GetPendingOrders(List<Order> orders)
{
var result = new List<Order>();
foreach (var order in orders)
{
if (!order.IsCancelled && !order.IsProcessed)
{
result.Add(order);
}
}

return result;
}

Here, the filtering logic is buried inside a loop with an if statement.

A more concise and readable approach uses LINQ:

public List<Order> GetPendingOrders(List<Order> orders)
{
return orders
.Where(o => !o.IsCancelled && !o.IsProcessed)
.ToList();
}

LINQ helps reduce verbose if/else blocks when working with collections, making code cleaner and more maintainable.

Create Object

When conditional statements exist only to configure behavior or data, using objects can make code simpler and more maintainable.

public static void PrintShapeInfo(int shapeId)
{
switch (shapeId)
{
case 0:
Console.WriteLine("Circle with radius 5");
break;
case 1:
Console.WriteLine("Rectangle 10x20");
break;
case 2:
Console.WriteLine("Triangle with sides 3, 4, 5");
break;
}
}

Here, the switch is just selecting which data to display.

If you add more shapes, you must keep editing this method — the logic and data are mixed together.

A cleaner approach is to create objects representing the shapes:

record Shape(string name, string description);

var shapes = new[]
{
new Shape("Circle", "Circle with radius 5"),
new Shape("Rectangle", "Rectangle 10x20"),
new Shape("Triangle", "Triangle with sides 3, 4, 5")
};

public static void PrintShapeInfo(int shapeId)
{
Console.WriteLine(shapes[shapeId].ToString());
}

This pattern works well anytime your method is mostly “pick the right data/configuration and do something with it”.

Polymorphism

A long chain of if/else statements that check for types is a common sign that polymorphism would be a better fit:

public double CalculateArea(Shape shape)
{
if (shape is Circle c)
{
return Math.PI * c.Radius * c.Radius;
}
else if (shape is Triangle t)
{
return 0.5 * t.Base * t.Height;
}
else if (shape is Rectangle r)
{
return r.Width * r.Height;
}
else
{
throw new ArgumentException("Unknown shape type");
}
}

This design mixes business logic with type handling, making the method grow every time a new shape is added.

A cleaner and more extensible solution is to use polymorphism:

public abstract class Shape
{
public abstract double GetArea();
}

// Concrete classes
public class Circle(double Radius) : Shape
{
public override double GetArea() => Math.PI * Radius * Radius;
}
public class Triangle : Shape { . . . }
public class Rectangle : Shape { . . . }

. . .

public double CalculateArea(Shape shape)
{
return shape.GetArea();
}

This follows the Open/Closed Principle: the system is open for extension but closed for modification, reducing the risk of breaking existing logic while keeping the codebase clean and scalable.

Polymorphism is often used to replace enum-based branching, as those can be encapsulated within separate classes, allowing behavior to vary without explicit conditional statements.

Marker Interface

A marker interface is a special case of polymorphism.

Even if your objects are mostly stateless, using a marker interface can simplify your code and reduce the need for constant updates as your system evolves.

For example, suppose you have different context types, each containing a User property:

User user = context switch 
{
UserContext userCtx => userCtx.User,
TeamMemberContext teamCtx => teamCtx.User,
ParticipantContext partCtx => partCtx.User,

_ => null,
}

Every time you add a new context type, you would need to update this switch expression.

By introducing a marker interface, you can unify these types and simplify the code:

var user = (context as IContextWithUser)?.User;

Now, any context that implements IContextWithUser automatically works, and you avoid constantly modifying conditional logic. This keeps your code cleaner, more maintainable, and adheres to the Open/Closed Principle.

State Pattern

When an object can behave differently depending on its internal state, long if/else or switch chains often appear:

public class Order
{
public OrderStatus Status { get; set; }

public void Cancel()
{
if (Status == OrderStatus.Paid)
{
Console.WriteLine("Cannot cancel a paid order");
}
else if (Status == OrderStatus.Pending)
{
Status = OrderStatus.Cancelled;
Console.WriteLine("Order cancelled");
}
}

public void Ship()
{
if (Status == OrderStatus.Pending)
{
Status = OrderStatus.Shipped;
Console.WriteLine("Order shipped");
}
else
{
Console.WriteLine("Cannot ship order in current state");
}
}
}

This design tightly couples behavior with state checks, making it hard to extend or maintain.

A better approach is the state pattern, which encapsulates behavior for each state in its own class:

interface IOrderState
{
void Cancel(Order order);
void Ship(Order order);
}

class PendingState : IOrderState
{
public void Cancel(Order order)
{
order.SetState(new CancelledState());
Console.WriteLine("Order cancelled");
}
public void Ship(Order order)
{
order.SetState(new ShippedState());
Console.WriteLine("Order shipped");
}
}

class PaidState : IOrderState
{
public void Cancel(Order order)
{
Console.WriteLine("Cannot cancel a paid order");
}
public void Ship(Order order)
{
order.SetState(new ShippedState());
Console.WriteLine("Order shipped");
}
}

class ShippedState : IOrderState { ... }
class CancelledState : IOrderState { ... }

public class Order
{
private IOrderState _state;

public void SetState(IOrderState state) => _state = state;

public void Cancel() => _state.Cancel(this);
public void Ship() => _state.Ship(this);
}
  • each state encapsulates its own behavior, removing large conditional chains
  • adding a new state doesn’t require modifying existing states
  • the main Order entity reads cleanly: delegate actions to the current state
  • reduces bugs and improves maintainability for complex workflows

This pattern is particularly useful when objects have multiple behaviors that depend on their current state, making if/else logic cumbersome.

Strategy Pattern

Long if/else or switch statements that select behavior based on a type are common when implementing multiple algorithms or payment methods:

public class PaymentProcessor
{
public void Pay(string type, decimal amount)
{
if (type == "CreditCard")
{
Console.WriteLine($"Processing credit card payment of {amount}");
}
else if (type == "PayPal")
{
Console.WriteLine($"Processing PayPal payment of {amount}");
}
else if (type == "Bitcoin")
{
Console.WriteLine($"Processing Bitcoin payment of {amount}");
}
}
}

This design is hard to extend: adding a new payment type requires modifying the Pay method, which violates the Open/Closed Principle.

A cleaner approach is the strategy pattern, where each algorithm or behavior is encapsulated in its own class:

public interface IPaymentStrategy
{
void Pay(decimal amount);
}

public class CreditCardPayment : IPaymentStrategy
{
public void Pay(decimal amount)
{
Console.WriteLine($"Processing credit card payment of {amount}");
}
}

public class PayPalPayment : IPaymentStrategy { . . . }
public class BitcoinPayment : IPaymentStrategy { . . . }

public class PaymentProcessor(IPaymentStrategy _strategy)
{
public void Pay(decimal amount)
{
_strategy.Pay(amount);
}
}

Benefits:

  • each payment method is self-contained
  • PaymentProcessor doesn’t need to know the details of each payment type
  • adding a new payment type only requires creating a new strategy class, leaving existing code untouched
  • the design is extensible, maintainable, and aligns with SOLID principles

Command + Factory Patterns

Sometimes you have multiple actions that depend on input or type, and a simple if/else or switch chain grows unwieldy.

public class FileProcessor
{
public void Execute(string commandType, string fileName)
{
if (commandType == "Read")
{
Console.WriteLine($"Reading file {fileName}");
}
else if (commandType == "Write")
{
Console.WriteLine($"Writing file {fileName}");
}
else if (commandType == "Delete")
{
Console.WriteLine($"Deleting file {fileName}");
}
}
}

You can use the command pattern to encapsulate each action in its own class:

public interface IFileCommand
{
void Execute(string fileName);
}

public class ReadCommand : IFileCommand
{
public void Execute(string fileName)
{
Console.WriteLine($"Reading file {fileName}");
}
}

public class WriteCommand : IFileCommand { . . . }
public class DeleteCommand : IFileCommand { . . . }

And the factory pattern decides which command to create based on input:

public static class FileCommandFactory
{
public static IFileCommand Create(string commandType) => commandType switch
{
"Read" => new ReadCommand(),
"Write" => new WriteCommand(),
"Delete" => new DeleteCommand(),

_ => throw new ArgumentException("Unknown command type", nameof(commandType))
};
}

This combination is especially useful when you have many actions and want a scalable, maintainable way to dispatch behavior:

var command = FileCommandFactory.Create("Write");
command.Execute("example.txt");

Chain of Responsibility

In the OOP world, a chain of responsibility pattern is just another way of writing a switch statement or if/else conditions.

public void RunMigrations()
{
if (!_migrationHistory.Contains("AddUsersTable"))
{
Console.WriteLine("Applying migration: AddUsersTable");
}

if (!_migrationHistory.Contains("AddOrdersTable"))
{
Console.WriteLine("Applying migration: AddOrdersTable");
}
. . .
}

Each handler decides whether it should execute and defines its behaviour:

public interface IMigrationHandler
{
bool CanExecute();
void Execute();
}

public class AddUsersTableMigration : IMigrationHandler
{
public bool CanExecute()
{
return _migrationHistory.Contains(nameof(AddUsersTableMigration));
}

public void Execute()
{
Console.WriteLine("Applying migration: AddUsersTable");
}
}

public class AddOrdersTableMigration : IMigrationHandler { . . . }

. . .

And the runner simply iterates through all handlers:

public class MigrationRunner
{
private readonly IEnumerable<IMigrationHandler> _migrations;

// inject all handles via DI
public MigrationRunner(IEnumerable<IMigrationHandler> migrations)
{
_migrations = migrations;
}

public void RunMigrations()
{
foreach (var migration in _migrations)
{
if (migration.CanExecute())
{
migration.Execute();
}
}
}
}

Why it works:

  • each migration encapsulates its own execution logic
  • adding a new migration doesn’t require modifying the runner
  • the main logic simply iterates through handlers, keeping code clean and extendable

This is a practical OOP approach to replacing long if/else chains with scalable, maintainable code.

How to choose the right approach?

Not every condition deserves a design pattern.

Use simple if statements when:

  • there are only a few checks
  • the logic is linear and obvious
  • readability is better with if than with abstraction
if (!user.IsActive) return "Inactive";
if (user.IsBanned) return "Blocked";

return "OK";

However, if conditions get out of hand, it’s time to change the approach when:

  • you have more than 3–4 nested or chained conditions
  • branches start duplicating code
  • each branch represents a separate business scenario
  • comments become necessary to explain the logic
  • the block grows beyond 100 lines

What can I say🤔

Structured programming is great, but it’s not a magic fix for every problem. It has clear benefits, but also some limitations.

For example, by organizing your code into sequences, loops, and well-defined functions, you can make it much easier to read, debug, and maintain. Nice 😁.

However, this doesn’t mean that every bit of logic needs to follow strict structures. Sometimes, a more flexible or unconventional approach is appropriate, while other times, overengineering a simple problem can make it unnecessarily complicated. Still, you should avoid deep nesting — if it goes beyond 3 levels, something is likely wrong.

To sum up: use structured programming to keep your code clean, modular, and predictable, but don’t stress if certain cases require a different approach.

Conclusion

Structured programming teaches us how to write code in a clean, predictable, well-organized manner.

It helps to deal with complexity by simplifying code flow.

The key benefits of structured programming are:

  • improved readability: structured programming enforces a clear, logical flow using sequences, loops, and conditionals, making code easier to read and understand
  • simplified debugging and maintenance: by breaking code into well-defined blocks and functions, it becomes easier to locate errors and update functionality
  • modularity: structured programming encourages modular design, allowing functions and procedures to be reused across different parts of a program
  • reduced complexity: simplifying condition statements makes the program flow predictable and easier to follow

💬 Let me know in the comments what are your ways to simplify if/else?

👏 Clap for this article if you enjoyed

👇 If you don’t like this one, there are other articles you may want to check out

☕️ You can also buy me a coffee with a link below

✅ Follow not to miss out on more

😉 But most importantly, stay awesome

--

--

iamprovidence

Written by iamprovidence

👨🏼‍💻 Full Stack Dev writing about software architecture, patterns and other programming stuff https://www.buymeacoffee.com/iamprovidence