Skip to Content
Course content

150: The Template Method Pattern

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I was looking through some old reporting code the other day, and I found a classic example of what I call "The Copy-Paste Drift." We had three different classes for exporting data: CsvExporter, JsonExporter, and XmlExporter. At first glance, they looked fine, but when I looked closer, they all followed the exact same sequence: open a file handle, write a header, loop through the data to write rows, and then close the handle.

The "Copy-Paste" Trap

Let's look at how that usually starts. You write your first exporter, and it works great. Then you need a second one, so you copy the first, change a few lines, and move on. It looks something like this:

public class CsvExporter {
    public void Export(IEnumerable<string> data) {
        Console.WriteLine("Opening file stream...");
        Console.WriteLine("Writing CSV Header: Column1, Column2");
        foreach(var item in data) {
            Console.WriteLine($"Writing CSV Row: {item}");
        }
        Console.WriteLine("Closing file stream.");
    }
}

public class JsonExporter {
    public void Export(IEnumerable<string> data) {
        Console.WriteLine("Opening file stream...");
        Console.WriteLine("Writing JSON Header: [");
        foreach(var item in data) {
            Console.WriteLine($"Writing JSON Row: \"{item}\",");
        }
        Console.WriteLine("Closing file stream: ]");
    }
}

It's not terrible yet. But notice how Opening file stream... and Closing file stream... are identical in both? If we decide to change how we handle file streams—say, adding a logging step or changing the buffer size—we now have to hunt down every single exporter class and update them manually. That's a recipe for bugs.

Trying to share the heavy lifting

My first instinct is usually to move the shared parts into a base class. I'll create an Exporter base and make the varying parts virtual. Let's try that:

public abstract class Exporter {
    public virtual void OpenFile() => Console.WriteLine("Opening file stream...");
    public virtual void CloseFile() => Console.WriteLine("Closing file stream.");
}

public class CsvExporter : Exporter {
    public void Export(IEnumerable<string> data) {
        OpenFile();
        Console.WriteLine("Writing CSV Header...");
        // ... write data ...
        CloseFile();
    }
}

Here's the problem: I'm still trusting the CsvExporter (and any future developer) to call OpenFile() first and CloseFile() last. If someone forgets to call CloseFile(), we have a memory leak. If they call them in the wrong order, the program crashes. I don't want to suggest the order; I want to enforce it.

Locking down the workflow

This is where the Template Method Pattern comes in. Instead of the subclasses controlling the execution, the base class defines the "skeleton" of the algorithm. I'll create a public` method that is marked as sealed (or just not virtual) which calls a series of protected abstract methods.

The base class says: "I know the steps. You just tell me how to do the specific parts."

public abstract class DataExporter {
    // This is the Template Method
    public void Export(IEnumerable<string> data) {
        OpenFile();
        WriteHeader();
        foreach (var item in data) {
            WriteRow(item);
        }
        CloseFile();
    }

    private void OpenFile() => Console.WriteLine("Opening file stream...");
    private void CloseFile() => Console.WriteLine("Closing file stream.");

    // Subclasses MUST implement these
    protected abstract void WriteHeader();
    protected abstract void WriteRow(string item);
}

public class CsvExporter : DataExporter {
    protected override void WriteHeader() => Console.WriteLine("Writing CSV Header...");
    protected override void WriteRow(string item) => Console.WriteLine($"CSV Row: {item}");
}

public class JsonExporter : DataExporter {
    protected override void WriteHeader() => Console.WriteLine("Writing JSON Header: [");
    protected override void WriteRow(string item) => Console.WriteLine($"JSON Row: \"{item}\",");
}

Now, the Export method is the source of truth. If I need to add a "Validate Permissions" step before opening the file, I add it in DataExporter once, and every single exporter in the system instantly inherits that behavior. The subclasses can't mess up the sequence because they don't even have access to the Export logic—they just fill in the blanks.

Flexibility without the chaos

You might wonder: "What if some exporters need an extra step, but others don't?" For example, maybe the XmlExporter needs to write a closing tag at the end, but the CsvExporter doesn't.

I handle this by adding a "hook." A hook is just a virtual method in the base class that has an empty body by default. It's an optional extension point.

public abstract class DataExporter {
    public void Export(IEnumerable<string> data) {
        OpenFile();
        WriteHeader();
        foreach (var item in data) { WriteRow(item); }
        WriteFooter(); // The Hook
        CloseFile();
    }

    protected virtual void WriteFooter() { 
        // Default: do nothing. Subclasses can override if they want.
    }
    // ... other methods ...
}

By using this pattern, we've moved from "hope the developer does it right" to "the architecture makes it impossible to do it wrong."




📋 Practical Task

Building a Multi-Format Log Parser

You are building a system that processes log files from different servers. Every log file, regardless of format, must be processed in this exact order: Read File $\rightarrow$ Clean Whitespace $\rightarrow$ Parse Entries $\rightarrow$ Save to Database.

Your task is to implement the Template Method Pattern to ensure this sequence is never altered.

  • Create an abstract base class LogProcessor.
  • Implement a public void Process(string filePath)` method that defines the skeleton (the four steps listed above).
  • Make Read File and Save to Database private methods in the base class (since they are the same for everyone).
  • Make Clean Whitespace a virtual "hook" method in the base class that does nothing by default.
  • Make Parse Entries a protected abstract method.
  • Create two concrete classes: ApacheLogProcessor and WindowsEventLogProcessor, each implementing their own specific parsing logic.

Test your implementation by calling Process() on both parser types and verifying the console output shows the steps occurring in the correct order.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.