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
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.");
}
}