Design Patterns in C#

Design patterns are reusable solutions to commonly occurring problems in software design. They provide a common vocabulary and a way to think about software design in a structured manner. Design patterns can be classified into three main categories:

Tricky Interview Question

Explain the difference between the Strategy and the Template Method patterns. When would you use one over the other?

Code Example

// Strategy Pattern
public interface IPaymentStrategy
{
    void Pay(int amount);
}

public class CreditCardPayment : IPaymentStrategy
{
    private string cardNumber;
    private string expiryDate;
    private string cvv;

    public CreditCardPayment(string cardNumber, string expiryDate, string cvv)
    {
        this.cardNumber = cardNumber;
        this.expiryDate = expiryDate;
        this.cvv = cvv;
    }

    public void Pay(int amount)
    {
        Console.WriteLine($"Paid {amount} using Credit Card: {cardNumber}");
    }
}

public class PayPalPayment : IPaymentStrategy
{
    private string email;

    public PayPalPayment(string email)
    {
        this.email = email;
    }

    public void Pay(int amount)
    {
        Console.WriteLine($"Paid {amount} using PayPal: {email}");
    }
}

public class ShoppingCart
{
    private IPaymentStrategy paymentStrategy;
    private int amount;

    public ShoppingCart(IPaymentStrategy paymentStrategy, int amount)
    {
        this.paymentStrategy = paymentStrategy;
        this.amount = amount;
    }

    public void Checkout()
    {
        paymentStrategy.Pay(amount);
    }
}

// Template Method Pattern
public abstract class DataProcessor
{
    public void ProcessData()
    {
        ReadData();
        Process();
        ValidateData();
        SaveData();
    }

    protected abstract void ReadData();
    protected abstract void Process();

    protected virtual void ValidateData()
    {
        Console.WriteLine("Data validation complete.");
    }
    protected abstract void SaveData();
}

public class CSVProcessor : DataProcessor
{
    protected override void ReadData()
    {
        Console.WriteLine("Reading data from CSV file.");
    }

    protected override void Process()
    {
        Console.WriteLine("Processing CSV data.");
    }

    protected override void SaveData()
    {
        Console.WriteLine("Saving data to database from CSV.");
    }
}

public class XMLProcessor : DataProcessor
{
    protected override void ReadData()
    {
        Console.WriteLine("Reading data from XML file.");
    }

    protected override void Process()
    {
        Console.WriteLine("Processing XML data.");
    }

    protected override void SaveData()
    {
        Console.WriteLine("Saving data to database from XML.");
    }
}

public class Example
{
    public static void Main(string[] args)
    {
        // Strategy Pattern
        var cart = new ShoppingCart(new CreditCardPayment("1234-5678-9012-3456", "12/24", "123"), 100);
        cart.Checkout();

        cart = new ShoppingCart(new PayPalPayment("test@example.com"), 50);
        cart.Checkout();

        // Template Method Pattern
        DataProcessor csvProcessor = new CSVProcessor();
        csvProcessor.ProcessData();

        DataProcessor xmlProcessor = new XMLProcessor();
        xmlProcessor.ProcessData();
    }
}

Detailed Answer

The Strategy Pattern allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. It lets the algorithm vary independently from clients that use it. The client chooses a strategy at runtime.

The Template Method Pattern defines the skeleton of an algorithm in a base class but lets subclasses override specific steps of the algorithm without changing its structure.

When to use which:

Potential Pitfalls:

Code Debugging Task

Find and fix the bug in the following code that prevents the correct strategy from being selected.

public interface IDiscountStrategy
{
    decimal ApplyDiscount(decimal price);
}

public class ChristmasDiscount : IDiscountStrategy
{
    public decimal ApplyDiscount(decimal price)
    {
        return price * 0.8m; // 20% discount
    }
}

public class NewYearDiscount : IDiscountStrategy
{
    public decimal ApplyDiscount(decimal price)
    {
        return price * 0.9m; // 10% discount
    }
}

public class DiscountManager
{
    private IDiscountStrategy discountStrategy;

    public DiscountManager(IDiscountStrategy discountStrategy)
    {
        this.discountStrategy = discountStrategy;
    }

    public decimal ApplyDiscount(decimal price)
    {
        return discountStrategy.ApplyDiscount(price);
    }
}

public class Example
{
    public static void Main(string[] args)
    {
        decimal price = 100m;
        string currentMonth = "December"; // Simulate current month

        IDiscountStrategy discountStrategy;

        if (currentMonth == "December")
        {
            discountStrategy = new NewYearDiscount(); // Bug: Should be ChristmasDiscount
        }
        else if (currentMonth == "January")
        {
            discountStrategy = new NewYearDiscount();
        }
        else
        {
            discountStrategy = new NewYearDiscount(); // No discount
        }

        DiscountManager discountManager = new DiscountManager(discountStrategy);
        decimal discountedPrice = discountManager.ApplyDiscount(price);

        Console.WriteLine($"Original Price: {price}");
        Console.WriteLine($"Discounted Price: {discountedPrice}");
    }
}

Detailed Explanation of the Bug and Corrected Code

Bug:

The bug is in the Main method of the Example class. When the current month is “December”, the code incorrectly instantiates NewYearDiscount instead of ChristmasDiscount.

Corrected Code:

public interface IDiscountStrategy
{
    decimal ApplyDiscount(decimal price);
}

public class ChristmasDiscount : IDiscountStrategy
{
    public decimal ApplyDiscount(decimal price)
    {
        return price * 0.8m; // 20% discount
    }
}

public class NewYearDiscount : IDiscountStrategy
{
    public decimal ApplyDiscount(decimal price)
    {
        return price * 0.9m; // 10% discount
    }
}

public class DiscountManager
{
    private IDiscountStrategy discountStrategy;

    public DiscountManager(IDiscountStrategy discountStrategy)
    {
        this.discountStrategy = discountStrategy;
    }

    public decimal ApplyDiscount(decimal price)
    {
        return discountStrategy.ApplyDiscount(price);
    }
}

public class Example
{
    public static void Main(string[] args)
    {
        decimal price = 100m;
        string currentMonth = "December"; // Simulate current month

        IDiscountStrategy discountStrategy;

        if (currentMonth == "December")
        {
            discountStrategy = new ChristmasDiscount(); // Corrected: Instantiate ChristmasDiscount
        }
        else if (currentMonth == "January")
        {
            discountStrategy = new NewYearDiscount();
        }
        else
        {
            discountStrategy = new NewYearDiscount(); // No discount
        }

        DiscountManager discountManager = new DiscountManager(discountStrategy);
        decimal discountedPrice = discountManager.ApplyDiscount(price);

        Console.WriteLine($"Original Price: {price}");
        Console.WriteLine($"Discounted Price: {discountedPrice}");
    }
}

Explanation:

The corrected code instantiates ChristmasDiscount when the current month is “December”, ensuring that the correct discount strategy is applied.

Additional Questions and Answers

Question: What is IoC (Inversion of Control) and what is it used for?

Inversion of Control (IoC) is an abstract principle and set of guidelines for writing loosely coupled code. The essence is that each system component should be as isolated as possible from others, not relying on implementation details of other components in its operation.

Question: What is a DI (Dependency Injection) container?

A DI container is one way to implement the IoC principle. This container knows about all interfaces and their implementations in the system and can map them. Before using it, you need to register known types and their mappings (interface–>implementation).

Question: What DI container implementations do you know in C#? Which DI container is the best?

Castle Windsor, Autofac, Ninject, Unity…and many others. The question of which is best isn’t entirely appropriate. It depends on the specific implementation requirements and use case.

Question: Definition of the Singleton pattern

Singleton is a creational pattern that ensures a class has only one instance while providing a global access point to it. Used when exactly one instance of a class is needed. Singletons can be thread-safe or not, with simple or lazy initialization.

class Singleton
{
    private static readonly Singleton _instance = new Singleton();
    private Singleton() {}
    static Singleton() {}
    public static Singleton Instance { get { return _instance; } }
}
Question: What is MVVM?

The MVVM (Model-View-ViewModel) pattern separates application logic from visual components, primarily used in WPF. Key components:

  • Model: Describes application data and may contain related logic like property validation
  • View: Defines the visual interface (buttons, text fields) for user interaction
  • ViewModel: Connects Model and View through data binding. Automatically propagates model changes to view while containing logic for data retrieval and updates, keeping Model and View decoupled.