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
109: The Decorator Pattern
I've seen this happen in plenty of production codebases: a developer starts with a simple requirement, and then "feature creep" hits. Suddenly, the class hierarchy looks like a disaster zone. Let me show you a snippet of a notification system I encountered a while back. The goal was simple: send notifications via different channels. The developer used inheritance to handle the combinations.
public interface INotification {
void Send(string message);
}
public class EmailNotification : INotification {
public void Send(string message) => Console.WriteLine($"Sending Email: {message}");
}
public class SmsNotification : INotification {
public void Send(string message) => Console.WriteLine($"Sending SMS: {message}");
}
// Here is where it gets ugly...
public class EmailAndSmsNotification : INotification {
public void Send(string message) {
new EmailNotification().Send(message);
new SmsNotification().Send(message);
}
}
public class EmailSmsAndSlackNotification : INotification {
public void Send(string message) {
new EmailNotification().Send(message);
new SmsNotification().Send(message);
new SlackNotification().Send(message);
}
}
The Inheritance Explosion
Look at those last two classes. It's a nightmare. If you add a fourth notification channel—say, WhatsApp—you don't just add one class. You have to create every possible combination of that new channel with the existing ones. This is what I call "class explosion." It's rigid, it's repetitive, and it's a fragile way to build software.
The core problem is that the developer is trying to use inheritance to add behavior. Inheritance is for "is-a" relationships. A "Notification that sends Email and SMS" isn't really a different kind of notification; it's just a notification with extra steps. When you find yourself creating classes like EmailAndSmsAndSlackNotification, you've gone down the wrong path.
Wrapping Behavior instead of Extending Classes
This is where the Decorator Pattern saves you. Instead of creating a class for every combination, we create a "wrapper." The wrapper implements the same interface as the object it's wrapping, but it adds its own logic before or after delegating the call to the inner object.
Here is how I would rewrite this to be actually maintainable:
public interface INotification {
void Send(string message);
}
// The base implementation
public class BasicNotification : INotification {
public void Send(string message) => Console.WriteLine($"Base message: {message}");
}
// The Base Decorator
public abstract class NotificationDecorator : INotification {
protected readonly INotification _inner;
public NotificationDecorator(INotification inner) => _inner = inner;
public virtual void Send(string message) => _inner.Send(message);
}
// Concrete Decorators
public class EmailDecorator : NotificationDecorator {
public EmailDecorator(INotification inner) : base(inner) { }
public override void Send(string message) {
base.Send(message);
Console.WriteLine($"Sending Email: {message}");
}
}
public class SmsDecorator : NotificationDecorator {
public SmsDecorator(INotification inner) : base(inner) { }
public override void Send(string message) {
base.Send(message);
Console.WriteLine($"Sending SMS: {message}");
}
}
Stacking your Decorators
Now, instead of having a hard-coded EmailAndSmsNotification class, you compose your object at runtime. You just wrap the objects like an onion.
// I want Email and SMS? Easy.
INotification notify = new BasicNotification();
notify = new EmailDecorator(notify);
notify = new SmsDecorator(notify);
notify.Send("Hello World!");
// Output:
// Base message: Hello World!
// Sending Email: Hello World!
// Sending SMS: Hello World!
Notice how we didn't have to create a new class to handle the combination? If you suddenly need to add Slack, you just create one SlackDecorator and wrap it around whatever you're already using. I love this approach because it follows the Open/Closed Principle: the system is open for extension (new decorators) but closed for modification (you don't have to touch the existing logic to add a new combination).
📋 Practical Task
Build a Dynamic Coffee Order System
You are building a point-of-sale system for a coffee shop. The shop has a basic coffee, but customers can add various "condiments" (Milk, Sugar, Whipped Cream) that increase the cost and change the description of the drink.
Your Goal: Implement the Decorator Pattern to allow for any combination of condiments without creating a separate class for every possible drink combination (e.g., no CoffeeWithMilkAndSugar class).
- Create an
ICoffeeinterface with aGetCost()method (returning a double) and aGetDescription()method (returning a string). - Create a concrete
SimpleCoffeeclass that implementsICoffee(Cost: 2.00, Description: "Simple Coffee"). - Create an abstract
CoffeeDecoratorclass that implementsICoffeeand wraps an instance ofICoffee. - Create at least two concrete decorators:
MilkDecorator(adds 0.50) andSugarDecorator(adds 0.20). - In your
Mainmethod, instantiate a coffee with both milk and sugar, and print the final description and cost to the console.
There are no comments for now.