Design patterns are essential knowledge for .NET developers preparing for technical interviews. They represent reusable solutions to common programming problems and demonstrate your understanding of object-oriented design principles. Mastering these patterns shows that you can write maintainable, scalable code and solve architectural challenges effectively.

1. Singleton Pattern

Q: What is the Singleton Pattern and why is thread safety important?

Answer: The Singleton Pattern ensures that a class has only one instance and provides a global point of access to it. Thread safety is critical in multi-threaded environments. The most robust C# implementation uses the Lazy class, which provides thread-safe lazy initialization without explicit locking mechanisms. This approach is preferred because it is elegant, efficient, and handles all edge cases automatically through the .NET Framework’s internal synchronization.

public class DatabaseConnection { private static readonly Lazy inst = new Lazy(() => new DatabaseConnection()); public static DatabaseConnection Instance => inst.Value; private DatabaseConnection() { } public void ExecuteQuery(string q) { Console.WriteLine(q); } }

2. Factory Pattern

Q: How does the Factory Pattern simplify object creation?

Answer: The Factory Pattern creates objects without exposing exact classes, relying on a factory method or factory class. This decouples client code from concrete implementations, making the system more flexible and maintainable. When adding new types, you modify only the factory, not client code. This pattern is particularly useful when object creation logic is complex or when you need to support multiple implementations based on runtime conditions.

public interface ILogger { void Log(string message); } public class ConsoleLogger : ILogger { public void Log(string message) => Console.WriteLine(message); } public static class LoggerFactory { public static ILogger CreateLogger(string type) => type switch { "console" => new ConsoleLogger(), _ => throw new ArgumentException() }; }

3. Abstract Factory Pattern

Q: When should you use Abstract Factory instead of simple Factory?

Answer: The Abstract Factory Pattern creates families of related objects without specifying concrete classes. Use it when your system works with multiple families of products and needs consistency within each family. For UI frameworks supporting themes, Abstract Factory ensures Light theme buttons work with Light theme checkboxes and dialogs. This pattern enforces consistency across product families and prevents mixing incompatible components from different families.

public interface IButton { void Render(); } public class LightButton : IButton { public void Render() => Console.WriteLine("Light"); } public interface IThemeFactory { IButton CreateButton(); } public class LightThemeFactory : IThemeFactory { public IButton CreateButton() => new LightButton(); }

4. Builder Pattern

Q: How does the Builder Pattern handle complex object construction?

Answer: The Builder Pattern constructs complex objects step by step, separating construction from representation. This solves the telescoping constructor problem by providing a fluent interface where clients set only needed properties. Validation occurs at build time, and the object remains immutable after construction. This approach improves readability and reduces constructor parameter lists that would otherwise become unmanageable.

public class Employee { public string FirstName { get; set; } public string LastName { get; set; } private Employee() { } public class Builder { private readonly Employee e = new Employee(); public Builder SetFirstName(string n) { e.FirstName = n; return this; } public Employee Build() { if (string.IsNullOrEmpty(e.FirstName)) throw new InvalidOperationException(); return e; } } }

5. Adapter Pattern

Q: How does the Adapter Pattern solve incompatible interface problems?

Answer: The Adapter Pattern converts a class’s interface into another clients expect, allowing incompatible classes to work together. This is crucial when integrating legacy code or third-party libraries without modifying source. The adapter bridges the gap between the expected interface and the actual implementation. This enables reuse of existing classes without modification and reduces coupling between systems.

public class LegacyGateway { public void ProcessPayment(double n) { Console.WriteLine(n); } } public interface IProcessor { void Process(decimal x); } public class LegacyAdapter : IProcessor { private readonly LegacyGateway g; public LegacyAdapter(LegacyGateway gateway) { g = gateway; } public void Process(decimal x) { g.ProcessPayment((double)x); } }

6. Decorator Pattern

Q: How does the Decorator Pattern add behavior dynamically without using inheritance?

Answer: The Decorator Pattern dynamically attaches responsibilities to objects, providing flexible alternatives to subclassing. Instead of numerous subclasses for feature combinations, decorators wrap objects and add behavior at runtime. This follows the Open/Closed Principle perfectly and allows unlimited composition of features without creating subclass hierarchies. Decorators are more flexible than inheritance for adding behavior to objects.

public interface IStream { void Write(string data); string Read(); } public class FileStream : IStream { private string data = ""; public void Write(string d) { data = d; } public string Read() => data; } public abstract class StreamDecorator : IStream { protected IStream inner; public virtual void Write(string d) { inner.Write(d); } public virtual string Read() => inner.Read(); }

7. Observer Pattern

Q: How does the Observer Pattern establish one-to-many dependencies?

Answer: The Observer Pattern defines one-to-many dependency where changing one object (subject) notifies all dependents (observers) automatically. This is fundamental for event-driven programming and decouples subjects from observers. .NET events are a built-in implementation of this pattern, making it natural for C# developers to work with. The pattern enables loose coupling between components that need to communicate about state changes.

public class Stock { private decimal price; public event EventHandler PriceChanged; public decimal Price { get { return price; } set { if (price != value) { price = value; PriceChanged?.Invoke(this, EventArgs.Empty); } } } } class Program { static void Main() { var s = new Stock(); s.PriceChanged += (o, e) => Console.WriteLine("Changed"); s.Price = 100m; } }

8. Strategy Pattern

Q: How does the Strategy Pattern enable runtime algorithm selection?

Answer: The Strategy Pattern defines a family of algorithms, encapsulates each, and makes them interchangeable. It lets algorithms vary independently from clients. Perfect for runtime selection without conditionals—strategies are objects swappable dynamically. This pattern eliminates complex if-else chains and allows adding new algorithms without modifying existing code. Each algorithm can be tested independently, improving code maintainability.

public interface ISort { void Sort(int[] arr); } public class QuickSort : ISort { public void Sort(int[] arr) { System.Array.Sort(arr); } } public class Processor { private ISort strategy; public Processor(ISort s) { strategy = s; } public void Process(int[] data) { strategy.Sort(data); } }

9. Repository Pattern

Q: Why is the Repository Pattern important for data access abstraction?

Answer: The Repository Pattern abstracts the data access layer, providing a collection-like interface for domain objects. It decouples business logic from data access, making code testable and maintainable. Hiding complexity behind a repository lets you change data sources without affecting business logic. This pattern centralizes data access logic and provides a consistent interface for all data operations, whether accessing databases, web services, or files.

public class User { public int Id { get; set; } public string Name { get; set; } } public interface IUserRepository { User GetById(int id); IEnumerable GetAll(); void Add(User user); } public class UserRepository : IUserRepository { private List users = new(); public User GetById(int id) => users.FirstOrDefault(u => u.Id == id); public void Add(User user) { user.Id = users.Count + 1; users.Add(user); } }

10. Dependency Injection Pattern

Q: How does Dependency Injection promote loose coupling and testability?

Answer: Dependency Injection provides dependencies from external sources rather than objects creating them. Classes depend on abstractions (interfaces) not concrete implementations, enabling loose coupling. This allows easy testing through mock objects and flexible swapping of implementations. Dependency Injection is fundamental to writing testable code and enables frameworks like ASP.NET Core to manage dependencies automatically through container systems.

public interface IEmailService { void Send(string to, string msg); } public class SmtpEmailService : IEmailService { public void Send(string to, string msg) { Console.WriteLine($"Email to {to}"); } } public class UserService { private readonly IEmailService svc; public UserService(IEmailService service) { svc = service; } public void RegisterUser(string name, string email) { Console.WriteLine(name); svc.Send(email, "Welcome!"); } }

Key Takeaways for Interviews

Pattern Categories: Creational patterns (Singleton, Factory, Abstract Factory, Builder) handle object creation. Structural patterns (Adapter, Decorator) deal with object composition. Behavioral patterns (Observer, Strategy, Repository, Dependency Injection) manage communication and responsibility assignment between objects.

When to Use Each Pattern: Use Singleton for single instances with global access, Factory for complex or variable creation, Strategy for runtime algorithm selection, Observer for event-driven systems, Repository for data access abstraction, and Dependency Injection for loose coupling. Understanding when NOT to use patterns is equally important.

Practical Implementation: C# has built-in support for many patterns. Dependency Injection is native through interfaces and constructor parameters. Observer pattern leverages events directly. Understanding how C# features simplify patterns demonstrates expertise and language fluency to interviewers.

Trade-offs and Limitations: Be ready to discuss when patterns might be overkill. Over-engineering makes code harder to understand. The goal is simplicity with necessary abstraction, balancing design principles with practical development needs and code readability.

Leave a Reply

Your email address will not be published. Required fields are marked *