C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
150: The Template Method Pattern
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 FileandSave to Databaseprivate methods in the base class (since they are the same for everyone). - Make
Clean Whitespacea virtual "hook" method in the base class that does nothing by default. - Make
Parse Entriesa protected abstract method. - Create two concrete classes:
ApacheLogProcessorandWindowsEventLogProcessor, 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.
There are no comments for now.