Skip to content
Search lessons, topics, tests…
Esc

    ↑ ↓ moveEnter openEsc close

    Guided course · Architecture and Design in .NET

    Design Patterns in C# with Practical Examples: back to the course

    Module 4 · 4. Behavioral Patterns and Interview Practice · Lesson 12 of 12

    Pattern Anti-Patterns and Interview Capstone

    Learning outcome

    • Identify common pattern anti-patterns (over-applied Singletons, God Factories, unnecessary Adapter/Facade, and premature Decorator/Proxy use).
    • Produce a short, production-safe refactor plan for real code.
    • Explain and defend a pragmatic pattern choice under interview or review pressure.

    Intuition

    Patterns are trade-offs, not checklists. A pattern chosen just because “we always use X” is often a mismatch. Look for signals: global state, large classes doing multiple responsibilities, deep inheritance chains, and duplicated conditional logic that screams Strategy/State. When those signals meet operational concerns (testability, observability, deployability), you have a justification for refactor.

    Deep dive

    • Forces to weigh: coupling vs. indirection, runtime flexibility vs. performance, testability vs. simplicity, and operational cost (deploy/observe).
    • Typical misuses:
    • Singleton-as-global-variable: hides dependencies, breaks test isolation.
    • Factory that returns dozens of unrelated types (God Factory): opaqueness and brittle addition of new families.
    • Adapter used to abstract trivial dependencies that could be solved by a small interface and composition.
    • Overuse of Decorator when a focused Strategy or simple conditional would be clearer.

    Use the executable example below to compare the global Singleton misuse with an explicit composition-and-Strategy alternative.

    Failure modes

    • Over-refactoring: splitting into many tiny interfaces without clear ownership increases cognitive load.
    • Finger-pointing: calling a design “wrong” without considering deployment/test constraints.
    • Performance paranoia: adding indirection when latency is critical and the alternative is safe.

    Interview drill

    • Given a class with many responsibilities, list the concrete signals you would extract to propose a pattern (e.g., two or more independent reasons to change => split by SRP).
    • Convert a Singleton-based cache to a DI-constructed cache: what tests you write first? (unit tests verifying no global state effects; integration test validating lifetime scope.)
    • Trade-off question: when would you prefer a simple if/else over Strategy? Short answer: when the branch is trivial, stable, and unlikely to grow.

    Revision checklist

    • Does each class have a single reason to change? If not, consider SRP-driven split.
    • Are dependencies explicit (constructor injection) instead of hidden global state?
    • Does the pattern reduce complexity or merely move it? Prefer explicit, readable code until the cost of repetition is measurable.
    • Are we introducing runtime reflection or heavy factories for no clear extensibility need?

    Production code

    • Prefer explicit constructor injection (IService) and small focused interfaces for testability.
    • Use patterns where they reduce coupling or better express intent. For example, Strategy is preferable to a switch over types when you expect new behaviors to be added independently.
    • Add telemetry and health probes when pattern introduces runtime indirection (e.g., Circuit Breaker, Proxy).

    Code walkthrough

    Below is a compact, deterministic console example that demonstrates: 1) Detecting a Singleton anti-pattern in a small scenario, and 2) Showing a production-safe refactor direction using composition/strategy.

    The example prints a clear explanation and uses an extension member that documents the misuse and suggests the compositional alternative.

    Notes in the example explain why the refactor (Strategy + DI simulation) is preferable to the simpler global Singleton, and include a misuse warning.

    Executable example provided in the lesson demonstrates the concept concretely. Read it before answering the interview drill.

    Exercises and micro-tests (self-check)

    • Exercise: Identify a class in your codebase that has more than three reasons to change. Sketch an SRP-driven split and decide which pattern (Factory/Strategy/Adapter) best groups the responsibilities.
    • Micro-test: Write a unit test that creates two independent instances of your refactored service and verifies they do not share mutable state.
    • Challenge: Given a monolithic Factory creating types across multiple families, design an Abstract Factory split and explain migration steps that keep production compatibility.

    Sample interview questions to practice answering aloud

    • "When would you pick Adapter vs. Facade?"
    • "How do you prove a Singleton is causing a test isolation failure?"
    • "Give me three low-risk steps to refactor a God Factory in a running service."

    Executable code examples

    Detecting Singleton misuse and refactor direction (deterministic)

    Program.cs

    C#Runs
    using System;
    using System.Collections.Generic;
    
    // Deterministic example demonstrating a common anti-pattern (Singleton) and a safer refactor.
    // Why this pattern is preferable to a simpler global singleton:
    // - Constructor-injected composition makes dependencies explicit and testable.
    // - Strategy separates variant behavior into focused classes instead of conditional logic.
    // Misuse warning: don't convert everything to tiny classes without a measurable need.
    
    public interface IMessageProcessor
    {
        string Process(string input);
    }
    
    // Anti-pattern: Singleton used as global processor holder with hidden state.
    public sealed class GlobalProcessor
    {
        // Hidden global instance and mutable state makes testing and reasoning hard.
        public static GlobalProcessor Instance { get; } = new GlobalProcessor();
        private GlobalProcessor() { }
    
        public string Mode { get; set; } = "upper"; // mutable global behavior
    
        public string Process(string input)
        {
            if (Mode == "upper") return input.ToUpperInvariant();
            if (Mode == "reverse") return Reverse(input);
            return input;
        }
    
        private static string Reverse(string s)
        {
            var arr = s.ToCharArray();
            Array.Reverse(arr);
            return new string(arr);
        }
    }
    
    // Safer approach: Strategy pattern with composition and explicit injection.
    public class UpperStrategy : IMessageProcessor { public string Process(string input) => input.ToUpperInvariant(); }
    public class ReverseStrategy : IMessageProcessor { public string Process(string input) { var a = input.ToCharArray(); Array.Reverse(a); return new string(a); } }
    
    public class ProcessorHost
    {
        private readonly IMessageProcessor _strategy;
        public ProcessorHost(IMessageProcessor strategy) => _strategy = strategy;
        public string Run(string input) => _strategy.Process(input);
    }
    
    // Classic static extension class (compatible with current C# toolchains) to provide explanatory helper.
    public static class ProcessorHostExtensions
    {
        public static void ExplainMisuse(this ProcessorHost host)
        {
            // This demonstrates how an extension method can carry descriptive, discoverable behavior.
            Console.WriteLine("[Extension] Prefer composition+Strategy over global Singletons for testability and explicit dependencies.");
            Console.WriteLine("[Misuse Warning] Global mutable state (like GlobalProcessor.Instance.Mode) breaks isolation and can hide side effects.");
        }
    }
    
    class Program
    {
        static void Main()
        {
            // Demonstrate Singleton anti-pattern behavior (deterministic):
            GlobalProcessor.Instance.Mode = "upper";
            var s1 = GlobalProcessor.Instance.Process("hello");
    
            // Changing global mode affects all callers — a hidden side effect.
            GlobalProcessor.Instance.Mode = "reverse";
            var s2 = GlobalProcessor.Instance.Process("hello");
    
            Console.WriteLine("Singleton outputs:");
            Console.WriteLine(s1); // HELLO
            Console.WriteLine(s2); // olleh
    
            // Safer: use composition with Strategy — no shared mutable state.
            var hostA = new ProcessorHost(new UpperStrategy());
            var hostB = new ProcessorHost(new ReverseStrategy());
    
            Console.WriteLine("\nStrategy outputs (no shared state):");
            Console.WriteLine(hostA.Run("hello")); // HELLO
            Console.WriteLine(hostB.Run("hello")); // olleh
    
            // Use extension method to explain the choice in-place (useful during code review).
            hostA.ExplainMisuse();
    
            // Final guidance line (deterministic summary)
            Console.WriteLine("\nSummary: Prefer explicit composition (constructor injection) + Strategy for variant behavior; avoid mutable Singletons.");
        }
    }
    Sign in to mark lessons done and keep your place in the course.Sign in