๐Ÿง  Mastering 5 Design Patterns in C# - Part 2

โœจ Introduction

Continuing our exploration of design patterns in C#, this article presents 5 additional patterns using the same notification system context. These patterns help solve different architectural challenges and improve code flexibility, scalability, and maintainability.

The patterns covered here are:

  • Adapter

  • Command

  • Proxy

  • Composite

  • Template Method


๐Ÿ”Œ 1. Adapter โ€“ Bridging Interfaces

โœ… Intent

Convert the interface of a class into another interface clients expect.

๐ŸŽฏ Purpose

Use when you need to integrate incompatible interfaces without modifying existing code โ€” especially useful when working with third-party libraries or legacy systems.

๐Ÿงฉ Code

public class LegacyNotifier
{
    public void LegacySend(string msg)
    {
        Console.WriteLine($"Legacy system sent: {msg}");
    }
}

public interface INotifier
{
    void Send(string message);
}

public class NotifierAdapter : INotifier
{
    private readonly LegacyNotifier _legacy;

    public NotifierAdapter(LegacyNotifier legacy)
    {
        _legacy = legacy;
    }

    public void Send(string message)
    {
        _legacy.LegacySend(message);
    }
}

๐Ÿง  Explanation

  • LegacyNotifier โ†’ Old system with incompatible method.

  • INotifier โ†’ Expected interface.

  • NotifierAdapter โ†’ Bridges the gap between old and new.

๐Ÿ‘ Pros

  • Promotes reuse of legacy code.

  • Decouples systems.

๐Ÿ‘Ž Cons

  • Adds extra layer of abstraction.

  • Can become messy with many adapters.


๐Ÿงพ 2. Command โ€“ Encapsulating Actions

โœ… Intent

Encapsulate a request as an object, allowing parameterization and queuing of requests.

๐ŸŽฏ Purpose

Use when you need to queue, log, or undo operations, or when actions should be executed later or remotely.

๐Ÿงฉ Code

public interface ICommand
{
    void Execute();
}

public class EmailCommand : ICommand
{
    private readonly string _message;

    public EmailCommand(string message)
    {
        _message = message;
    }

    public void Execute()
    {
        Console.WriteLine($"Email command executed: {_message}");
    }
}

public class CommandInvoker
{
    private readonly List<ICommand> _commands = new();

    public void AddCommand(ICommand command) => _commands.Add(command);

    public void Run()
    {
        foreach (var cmd in _commands)
            cmd.Execute();
    }
}

๐Ÿง  Explanation

  • ICommand โ†’ Interface for commands.

  • EmailCommand โ†’ Encapsulates email sending.

  • CommandInvoker โ†’ Stores and executes commands.

๐Ÿ‘ Pros

  • Enables undo/redo.

  • Decouples sender from receiver.

๐Ÿ‘Ž Cons

  • Requires many small classes.

  • Can be overkill for simple tasks.


๐Ÿงฟ 3. Proxy โ€“ Controlled Access

โœ… Intent

Provide a surrogate or placeholder for another object to control access to it.

๐ŸŽฏ Purpose

Use when you need to add access control, lazy loading, or logging without changing the original object.

๐Ÿงฉ Code

public interface INotifier
{
    void Send(string message);
}

public class RealNotifier : INotifier
{
    public void Send(string message)
    {
        Console.WriteLine($"Real notifier: {message}");
    }
}

public class NotifierProxy : INotifier
{
    private readonly RealNotifier _realNotifier = new();

    public void Send(string message)
    {
        Console.WriteLine("Proxy: Checking permissions...");
        _realNotifier.Send(message);
    }
}

๐Ÿง  Explanation

  • RealNotifier โ†’ Actual implementation.

  • NotifierProxy โ†’ Adds control before delegating.

๐Ÿ‘ Pros

  • Adds security, logging, or caching.

  • Transparent to client.

๐Ÿ‘Ž Cons

  • Adds complexity.

  • Can introduce performance overhead.


๐ŸŒณ 4. Composite โ€“ Hierarchical Structures

โœ… Intent

Compose objects into tree structures to represent part-whole hierarchies.

๐ŸŽฏ Purpose

Use when you need to treat individual objects and groups uniformly, such as nested notifications or UI components.

๐Ÿงฉ Code

public interface INotifier
{
    void Send(string message);
}

public class SingleNotifier : INotifier
{
    public void Send(string message)
    {
        Console.WriteLine($"Single: {message}");
    }
}

public class CompositeNotifier : INotifier
{
    private readonly List<INotifier> _notifiers = new();

    public void Add(INotifier notifier) => _notifiers.Add(notifier);

    public void Send(string message)
    {
        foreach (var notifier in _notifiers)
            notifier.Send(message);
    }
}

๐Ÿง  Explanation

  • SingleNotifier โ†’ Individual notification.

  • CompositeNotifier โ†’ Group of notifiers treated as one.

๐Ÿ‘ Pros

  • Simplifies hierarchical structures.

  • Uniform treatment of objects.

๐Ÿ‘Ž Cons

  • Can be hard to manage deep trees.

  • Debugging can be tricky.


๐Ÿงฌ 5. Template Method โ€“ Algorithm Skeleton

โœ… Intent

Define the skeleton of an algorithm, deferring steps to subclasses.

๐ŸŽฏ Purpose

Use when you have a fixed process with customizable steps, like sending notifications with pre- and post-processing.

๐Ÿงฉ Code

public abstract class NotificationTemplate
{
    public void Send(string message)
    {
        PreSend();
        DoSend(message);
        PostSend();
    }

    protected virtual void PreSend() => Console.WriteLine("Preparing to send...");
    protected abstract void DoSend(string message);
    protected virtual void PostSend() => Console.WriteLine("Finished sending.");
}

public class EmailNotification : NotificationTemplate
{
    protected override void DoSend(string message)
    {
        Console.WriteLine($"Email sent: {message}");
    }
}

๐Ÿง  Explanation

  • NotificationTemplate โ†’ Defines the algorithm structure.

  • EmailNotification โ†’ Implements the core step.

๐Ÿ‘ Pros

  • Promotes code reuse.

  • Enforces consistent process.

๐Ÿ‘Ž Cons

  • Inflexible if steps vary too much.

  • Can lead to deep inheritance chains.


๐Ÿงพ Conclusion

These five additional design patterns offer powerful tools for structuring your C# applications:

  • Adapter bridges incompatible interfaces.

  • Command encapsulates actions for flexibility.

  • Proxy controls access and adds behavior.

  • Composite handles hierarchical structures.

  • Template Method enforces consistent workflows.

Using the same notification example across all patterns helps highlight their unique strengths and trade-offs

#CSharp #DesignPatterns #DotNet #SoftwareEngineering #CleanArchitecture #BackendDev #CodeQuality #ProgrammingTips #Adapter #Command #Proxy #Composite #TemplateMethod #

0
Subscribe to my newsletter

Read articles from Johnny Hideki Kinoshita de Faria directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Johnny Hideki Kinoshita de Faria
Johnny Hideki Kinoshita de Faria

Technology professional with over 15 years of experience delivering innovative, scalable, and secure solutions โ€” especially within the financial sector. I bring deep expertise in Oracle PL/SQL (9+ years), designing robust data architectures that ensure performance and reliability. On the back-end side, Iโ€™ve spent 6 years building enterprise-grade applications using .NET, applying best practices like TDD and clean code to deliver high-quality solutions. In addition to my backend strengths, I have 6 years of experience with PHP and JavaScript, allowing me to develop full-stack web applications that combine strong performance with intuitive user interfaces. I've led and contributed to projects involving digital account management, integration of VISA credit and debit transactions, modernization of payment systems, financial analysis tools, and fraud prevention strategies. Academically, I hold a postgraduate certificate in .NET Architecture and an MBA in IT Project Management, blending technical skill with business acumen. Over the past 6 years, Iโ€™ve also taken on leadership roles โ€” managing teams, mentoring developers, and driving strategic initiatives. I'm fluent in agile methodologies and make consistent use of tools like Azure Boards to coordinate tasks and align team performance with delivery goals.