DEV Community

Jeya Shad
Jeya Shad

Posted on • Originally published at faun.pub on

I Finally Killed the “If-Else” Chain: From Spaghetti Code to a Plugin System (in Python & .NET)

How the Registry Pattern saved my codebase and how to implement it in both Python and C#.

Split-screen illustration comparing a chaotic bowl of “spaghetti code” labeled “if-else chain” with Python and .NET logos, transforming into an organized, interlocking “plugin system” of gears and blocks labeled “registry pattern”.

I was tired of scrolling through endless if-else chains every time I added a new feature.
One small change turned my messy logic into a clean plugin system.
Here’s how the Registry Pattern helped me refactor it in both Python and .NET.

📚 Table of Contents


I have a confession to make. I used to love if statements.

They are easy. They are logical. When you are learning to code, if is the hammer, and every problem looks like a nail. But recently, I looked at a project of mine and realized I had built a monster.

I was working on a feature to export data, and my code looked like a staircase to nowhere. if format == 'pdf', do this. elif format == 'csv', do that. elif, elif, elif...

It worked, but it was ugly. Every time I wanted to add a new feature, I had to scroll through hundreds of lines of code, risking breakage with every keystroke. I knew there had to be a better way.

That’s when I stumbled upon the Registry Pattern. It didn’t just clean up my code; it changed how I think about software architecture.

What You Will Learn in This Article

  • The Trap: Why infinite if-else chains kill maintainability.
  • The Fix: Implementing the Registry Pattern in Python using Decorators.
  • The Comparison: How to achieve the exact same architecture in .NET (C#) using Keyed Services and Reflection.
  • The Big Picture: How to move from writing “Scripts” to building “Systems.”

The Problem: The “Open-Closed” Violation

Let’s look at the bad code first. This is a classic example of what not to do, even though we have all done it.

# 🚫 The Old Way: The "Spaghetti" Approach

def export_data(data: dict, format: str):
    if format == "pdf":
        export_pdf(data)
    elif format == "csv":
        export_csv(data)
    elif format == "json":
        export_json(data)
    else:
        raise ValueError(f"Unknown format: {format}")
Enter fullscreen mode Exit fullscreen mode

Why is this bad?

This violates a core software rule called the Open-Closed Principle (OCP). The principle states that your code should be open for extension (you can add new stuff) but closed for modification (you shouldn’t have to touch working code).

In the example above, if I want to add an XML exporter, I have to open this core function, find the right spot in the if chain, and surgically insert a new condition. In a small script, that's fine. In a massive application? That is a recipe for bugs.

🔑 The Real Change Is Not Syntax; It’s Thinking

Before using the registry pattern, my code was asking questions. “Is this PDF?” “Is this CSV?” “Is this JSON?”

Every new feature meant adding one more question.

After using the registry pattern, my code stopped asking questions. Instead, it just says: “Give me the exporter for this format.”

That’s it. This small change matters because:

  • Code that asks many questions grows fast and breaks easily.
  • Code that looks things up stays stable, even when features grow.

This is the moment where code stops behaving like a script and starts behaving like a system.

The Solution: The Python Registry

The solution is surprisingly simple: Stop asking “If”. Start using a Map.

Instead of checking logic step-by-step, we create a central Registry. a simple dictionary. The keys are the format names (like “pdf” or “csv”), and the values are the actual functions that do the work.

Step 1: The Registry Without Decorators (The Simple Version)

Before we get fancy with decorators, let’s see the simplest version of the idea. We just use a dictionary to map names to functions.

# The Registry: A simple dictionary
exporters = {
    "pdf": export_pdf,
    "csv": export_csv,
}

def export_data(data: dict, format: str):
    # No more if-statements! Just look it up.
    exporter = exporters.get(format)

    if not exporter:
        raise ValueError(f"Unknown format: {format}")
    exporter(data)
Enter fullscreen mode Exit fullscreen mode

This already solves the big problem:

  • No if-elif-else chains.
  • No touching this function when adding new exporters.

Step 2: The “Pro” Way (Using Decorators)

But we can go one step cooler. Since Python is a dynamic language, we can use Decorators. I created a @register decorator. Now, whenever I write a new function, I just slap a sticker on it saying "I am a PDF exporter!" and the system automatically adds it to the list.

Here is the clean version:

from typing import Callable

# 1. The Registry 
# This dictionary will hold our commands. 
exporters: dict[str, Callable] = {}

# 2. The Decorator 
# Think of this as a sticker gun. We use it to label our functions.
def register_exporter(format: str):
    def decorator(func: Callable):
        exporters[format] = func
        return func
    return decorator

# 3. The Implementation 
# Now we just "tag" our functions!
@register_exporter("pdf")
def export_pdf(data):
    print(f"Exporting to PDF: {data}")

@register_exporter("csv")
def export_csv(data):
    print(f"Exporting to CSV: {data}")

# 4. The Clean Usage 
def export_data(data: dict, format: str):
    exporter = exporters.get(format)
    if not exporter:
        raise ValueError(f"Unknown format: {format}")
    exporter(data)
Enter fullscreen mode Exit fullscreen mode

⚠️ A Note on Imports: For the decorator to work, Python needs to actually read the file containing the function. If you split your exporters into different files (e.g., pdf_exporter.py) but never import that file in your main script, the decorator code never runs, and the registry will stay empty!

Small Note: This Pattern Has a Name

In practice, this approach is often a mix of Strategy, Registry, and Factory patterns. The exact name is less important than the idea.

  • One interface.
  • Many implementations.
  • Selected at runtime using a key.

Names are optional. Clean thinking is not.

“But wait… I’m a .NET Developer!”

The Universal Truth: Applying This to .NET / C

As I was implementing this, I started wondering: I also work with C# and .NET. Does this concept exist there?

This is where things get interesting. The Concept is universal, but the Implementation depends on the language’s philosophy.

  • Python is Dynamic: We use a dictionary and decorators because Python lets us modify code structures on the fly.
  • C# is Static: We can’t just “monkey-patch” a dictionary as easily. Instead, .NET gives us two powerful tools to solve this: Dependency Injection (DI) and Attributes.

First, we need a shared interface so our system knows what an “Exporter” looks like:

// First, we define the contract.
public interface IExporter
{
    void Export(object data);
}
Enter fullscreen mode Exit fullscreen mode

Option A: The “Modern” Way (.NET 8 Keyed Services)

In modern .NET, the Dependency Injection container is your registry. You don’t need to build a dictionary manually; Microsoft built one for you.

With .NET 8, you can use Keyed Services , which works almost exactly like our Python dictionary:

// Program.cs
// "Registering" the exporters with a key (string)
builder.Services.AddKeyedSingleton<IExporter, PdfExporter>("pdf");
builder.Services.AddKeyedSingleton<IExporter, CsvExporter>("csv");
Enter fullscreen mode Exit fullscreen mode

Option B: The “Magic” Way (Reflection + Attributes)

If you want that true “plugin” feel — where you just create a new class file and it magically starts working without registering it anywhere — we use Reflection.

Step 1: Create a Custom Attribute This is like the sticker we put on our classes.

[AttributeUsage(AttributeTargets.Class)]
public class ExporterAttribute : Attribute
{
    public string Format { get; }
    public ExporterAttribute(string format) => Format = format;
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Tag Your Classes Now, we just create classes and tag them. We don’t touch any other code.

[Exporter("pdf")] // 👈 This tag handles the registration!
public class PdfExporter : IExporter
{
    public void Export(object data) => Console.WriteLine("Exporting to PDF...");
}
Enter fullscreen mode Exit fullscreen mode

Step 3: The “Auto-Discovery” Engine This is the setup code that replaces that massive switch statement. It scans your entire application for any class wearing the [Exporter] sticker.

public class ExporterFactory
{
    private readonly Dictionary<string, Type> _registry;

    public ExporterFactory()
    {
        _registry = new Dictionary<string, Type>();

        // The Magic: Scan the assembly for our attribute
        var exporterTypes = Assembly.GetExecutingAssembly()
            .GetTypes()
            .Where(t => t.GetCustomAttribute<ExporterAttribute>() != null);

        foreach (var type in exporterTypes)
        {
            var attr = type.GetCustomAttribute<ExporterAttribute>();
            _registry.Add(attr.Format, type);
        }
    }

    public IExporter Create(string format)
    {
        if (!_registry.ContainsKey(format)) 
            throw new Exception($"No exporter found for {format}");

        // Create an instance of the class dynamically
        return (IExporter)Activator.CreateInstance(_registry[format]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, we write a simple “scanner” that looks for these tags when the app starts. It finds every class wearing the [Exporter] sticker and adds it to our list automatically.

⚠️ A Quick Warning About Reflection

Reflection feels powerful and it is but it comes with trade-offs.

  • Harder to debug: If something breaks, the stack trace can be scary.
  • Slower startup: Scanning all your files takes time.
  • “Magic” feeling: New team members might not understand how PdfExporter is being called since it's never explicitly referenced.

My advice: Use Reflection if you are building a real plugin system where users drop DLLs into a folder. For most normal business apps, Keyed Services (Option A) is simpler and safer.

🛠Refactoring in Action: The .NET Transformation

Let’s bring it all together. Just like our Python example, we want to see the “Before” and “After” to really appreciate the cleanup.

🚫 The “Before” (The Switch Statement of Doom)

Here is the C# code we are trying to kill. This logic usually lives in a “Service” class, and it grows every time you add a feature.

public class ExportService
{
    public void ExportData(string format, object data)
    {
        // Every time you add a format, you must touch this file.
        // This is a Merge Conflict waiting to happen. 
        switch (format.ToLower())
        {
            case "pdf": new PdfExporter().Export(data); break;
            case "csv": new CsvExporter().Export(data); break;
            case "xml": new XmlExporter().Export(data); break;
            default: throw new ArgumentException("Unknown format");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ The “After” (Clean & Decoupled)

Whether you chose Option A (Keyed Services) or Option B (Reflection), your core service code now looks like this. Notice how the switch statement is completely gone.

public class ExportService
{
    private readonly IServiceProvider _services;

    // We inject the "Registry" (the ServiceProvider)
    public ExportService(IServiceProvider services)
    {
        _services = services;
    }

    public void ExportData(string format, object data)
    {
        // 1. Ask the system: "Do you have an exporter for 'pdf'?"
        var exporter = _services.GetKeyedService<IExporter>(format);

        if (exporter == null)
            throw new ArgumentException($"No exporter found for {format}");

        // 2. Use it. We don't care if it's PDF, CSV, or XML.
        exporter.Export(data);
    }
}
Enter fullscreen mode Exit fullscreen mode

The Takeaway:

  • Zero Modification: To add a new JsonExporter, you simply create the class. You never open ExportService.cs again.
  • Testable: You can easily mock IExporter to test the service without actually generating PDF files.

Why This Matters: From “Script” to “System”

This pattern is the difference between writing a script and building a System.

Imagine you are building a Command Line Tool (CLI) or a Payment Gateway.

  • The Script Way: You write a massive if statement for every payment provider (Stripe, PayPal, Adyen).
  • The System Way: You define an interface. You let other developers (or your future self) add new providers by simply dropping a new file into the folder.

With the Registry Pattern, you aren’t just cleaning up code; you are building an architecture that can grow without collapsing.

The Reality Check (Pros & Cons)

Of course, no pattern is perfect.

The Pros ✅

  • Scalable: You can have 5 exporters or 500; the code complexity stays the same.
  • Git Friendly: No more merge conflicts because two developers tried to edit the same big if/else block at the same time.

The Cons ⚠️

  • “Magic” Behavior: Because the registration happens automatically, it can sometimes be hard for a new developer to find where a command is defined.
  • Import Order (Python specific): If you don’t import the file containing the decorator, the decorator never runs, and the command won’t register.

When You Should Not Use the Registry Pattern

Be honest with yourself. If you have:

  • Only 2 or 3 conditions
  • Logic that will never grow
  • A small script or one-off tool

Then if-else is perfectly fine. Patterns are tools, not rules.

Use the registry pattern when:

  • You expect growth.
  • Multiple developers work on the code.
  • You want to add features without touching old logic.

Conclusion

If you find yourself writing an if-elif-else chain that is longer than your screen, stop. Take a breath. And try building a Registry.

It might feel like “over-engineering” at first, but your future self will thank you when they can add a feature in 5 minutes instead of 5 hours.

Have you ever refactored a big if-else chain like this?
Or do you still prefer explicit conditionals?

Let me know in the comments!


If You Wish To Support Me As A Creator

Leave a comment telling me your thoughts

Thank you! These tiny actions go a long way, and I really appreciate it!

LinkedIn: https://www.linkedin.com/in/Shadhujan/

Github: https://github.com/Shadhujan

Portfolio: https://shadhujan-portfolio.vercel.app/

Insta: https://www.instagram.com/jeya.shad38/


Top comments (0)