Design Patterns
Design patterns are named, proven solutions to recurring design problems (from the "Gang of Four" book).
Concept
Design patterns are named, proven solutions to recurring design problems (from the "Gang of Four" book). They give teams a shared vocabulary: "wrap the vendor driver in an adapter", "add logging as a decorator".
Exactly one instance (prefer a DI singleton over a static Instance) Factory Method
Subclasses decide which object to create Abstract Factory
Create families of related objects (real vs simulated bench)
Construct complex objects step by step (fluent API) Create objects by cloning (record + with, beware shallow copies)
Convert one interface into another (vendor SDK → ICanBus) Separate abstraction and implementation so both vary independently
Treat individual objects and groups uniformly (test case / test suite)
Add responsibilities by wrapping (logging, retry, caching) — ASP.NET Core middleware and DelegatingHandler are decorators
A simple entry point to a complex subsystem (flash sequence)
Share immutable state among many objects (signal definitions, interned strings)
A stand-in that controls access (lazy load, cache, remote, security)
Interchangeable algorithms (checksums, retry policies) Notify subscribers of changes (C# events, IObservable<T>) Requests as objects: queue, log, undo
Behaviour depends on state; each state is a class (flashing state machine) Template Method
A fixed algorithm with overridable steps (test setup/execute/teardown) Chain of Responsibility
Pass a request along handlers (NRC handling, middleware pipeline)
Objects communicate through a central mediator (MediatR library)
Sequential access without exposing structure (IEnumerable<T> + yield)
Add operations to a class hierarchy without changing it (Roslyn's CSharpSyntaxWalker)
Capture and restore state (undo, calibration snapshots) When NOT to Use a Pattern
Patterns add indirection. Don't introduce a factory for a class that has one implementation, a strategy for an algorithm that never changes, or a singleton for global mutable state. Modern C# features often replace patterns: delegates/lambdas replace simple strategies and commands, records + with replace prototypes, IEnumerable/yield is the iterator, DI replaces most singletons and factories, pattern matching can replace visitors for closed hierarchies.
Why It Matters
Every .NET framework is built from these patterns (middleware = chain of responsibility + decorator, HttpClientFactory = factory, logging providers = strategy/composite). Interviewers use them to test design thinking, and they make large engineering tools extensible.
Syntax
ICanBus bus = new LoggingCanBus(new RetryingCanBus(new VendorCanAdapter(driver))); // decorators around an adapter
var plan = new TestPlanBuilder("Nightly").AddStep("Flash").WithTimeout(TimeSpan.FromMinutes(30)).Build();
IChecksum checksum = useXor ? new XorChecksum() : new SumChecksum(); // strategyExample
54.1 Creational and structural patterns 54.2 Behavioral patterns
using System.Collections.Concurrent;
Console.WriteLine("== Creational ==");
// Singleton (thread-safe via Lazy<T>; in DI apps prefer AddSingleton)
Console.WriteLine($"Singleton: same instance {ReferenceEquals(BenchRegistry.Instance, BenchRegistry.Instance)}");
// Factory Method: subclasses decide which product to create
foreach (DiagnosticTool tool in new DiagnosticTool[] { new CanTool(), new DoipTool() })
Console.WriteLine($"Factory Method: {tool.GetType().Name} -> {tool.Connect()}");
// Abstract Factory: families of related objects
foreach (IBenchFactory f in new IBenchFactory[] { new VectorBenchFactory(), new SimulatedBenchFactory() })
Console.WriteLine($"Abstract Factory: {f.CreateInterface().Name} + {f.CreatePowerSupply().Name}");
// Builder: step-by-step construction with a fluent API
TestPlan plan = new TestPlanBuilder("Nightly").AddStep("Power on").AddStep("Flash VCU").WithTimeout(TimeSpan.FromMinutes(30)).Build();
Console.WriteLine($"Builder: {plan}");
// Prototype: clone a configured object (records: 'with')
var baseFrame = new CanFrameTemplate(0x7E0, [0x02, 0x10, 0x03]);
var copy = baseFrame with { Id = 0x7E1 };
Console.WriteLine($"Prototype: {baseFrame.Id:X
…// Strategy: interchangeable algorithms
foreach (IChecksum c in new IChecksum[] { new XorChecksum(), new SumChecksum() })
Console.WriteLine($"Strategy: {c.GetType().Name,-12} 0x{c.Compute([0x02, 0x10, 0x03]):X2}");
// Observer: subscribers are notified of changes (C# events / IObservable<T>)
var sensor = new TemperatureSensor();
sensor.Changed += t => Console.WriteLine($"Observer: dashboard shows {t} C");
sensor.Changed += t => { if (t > 100) Console.WriteLine("Observer: alarm! overheating"); };
sensor.Value = 95; sensor.Value = 105;
// Command: requests as objects -> queue, log, undo
var ecu = new EcuState();
var history = new Stack<ICommand>();
foreach (ICommand cmd in new ICommand[] { new SetSessionCommand(ecu, "Extended"), new SetSessionCommand(ecu, "Programming") })
{ cmd.Execute(); history.Push(cmd); }
Console.WriteLine($"Command: session now {ecu.Session}");
history.Pop().Undo();
Console.WriteLine($"Command: after undo {ecu.Session}");
// State: behaviour changes with internal state
var flasher = new Flasher();
foreach (string ev in new[] { "start", "block", "block", "finish", "block" }) flasher.Handle(ev);
// Template Method: fixed algorithm skeleton, customizable s
…Expected Result
The decorator chain logged first, then passed the frame through the retry decorator to the adapter. The proxy queried the real ECU only once. The prototype line is a warning: with copied the reference to the data array, not the array.
== Creational ==
Singleton: same instance True
Factory Method: CanTool -> CAN channel 1 @ 500 kbit/s
Factory Method: DoipTool -> DoIP 192.168.1.50:13400
Abstract Factory: VN1630 CAN interface + Programmable PSU
Abstract Factory: Virtual CAN + Simulated PSU
Builder: Nightly: Power on -> Flash VCU (timeout 30 min)
Prototype: 7E0 -> clone 7E1, shares data array True (shallow copy!)
== Structural ==
Adapter: vendor transmit ch1 id 0x123 dlc 2
Bridge: MarkdownRenderer | P0A80 | | U0100 |
Bridge: CsvRenderer P0A80,U0100
Composite: 'Regression' total duration 37 s, 3 tests
Decorator(log): sending 0x7E0 023E00
Adapter: vendor transmit ch1 id 0x7E0 dlc 3
Facade: VCU: session 0x02 -> security access -> erase -> download vcu.hex -> check CRC -> reset
Flyweight: 2 requests, 1 shared definition: True, pool size 1
(real ECU queried)
Proxy: EHBAUREV250000001 / EHBAUREV250000001Strategy: XorChecksum 0x11
Strategy: SumChecksum 0x15
Observer: dashboard shows 95 C
Observer: dashboard shows 105 C
Observer: alarm! overheating
Command: session now Programming
Command: after undo Extended
State: Idle + start -> Writing
State: Writing + block -> Writing
State: Writing + block -> Writing
State: Writing + finish -> Done
State: Done + block -> Done
Template Method: VoltageTest setup done, PASS, teardown done
Template Method: CommunicationTest setup done, FAIL, teardown done
Chain: NRC 0x33 -> security access denied - unlock first
Chain: NRC 0x78 -> response pending - wait and retry
Chain: NRC 0x22 -> unhandled NRC - generic failure
Mediator: logger got 'voltage set to 13.5 V' from PowerSupply
Iterator: 0x100 0x7E8
Visitor: 2 passed, 1 failed, total 32.5 s
Memento: restored IdleRpm 750, FanOnTemp 95Common Mistakes
Pattern-itis: interfaces and factories everywhere "just in case".
Singletons with mutable global state (hidden coupling, untestable, thread-unsafe).
Shallow-copy prototypes shared between "independent" objects.
Deep decorator/proxy chains that make debugging hard.
Implementing Observer with events and never unsubscribing (memory leaks, Module 19).
Best Practices
Start simple; refactor to a pattern when the need appears (a second implementation, a cross-cutting concern).
Prefer composition (decorator, strategy) over inheritance (template method) when behaviour must vary at run time.
Let DI build decorated objects and choose strategies.
Name classes after the pattern role when it helps readers (...Adapter, ...Decorator, ...Factory).
Interview Relevance
Very high: "explain the decorator/strategy/observer/factory pattern with an example", "adapter vs facade vs proxy?", "what's wrong with singletons?", "which patterns does ASP.NET Core use?", "when shouldn't you use a pattern?".
Practice Questions
Add a CachingCanBus decorator that suppresses identical consecutive frames.
Implement undo/redo for calibration edits with Command + Memento.
Replace the IChecksum strategy classes with Func<byte[], byte> delegates — what do you gain and lose?
Model the UDS session state machine (Default, Extended, Programming) with the State pattern.
Quick Quiz
Q1. (Multiple choice) Adding retry and logging to an ICanBus without changing it is: A) Adapter B) Decorator C) Facade D) Bridge Answer: B.
Q2. (Predict the output) var copy = baseFrame with { Id = 0x7E1 }; ReferenceEquals(baseFrame.Data, copy.Data)
Answer: True — with makes a shallow copy (verified).
Q3. (True/False) ASP.NET Core middleware is an example of Chain of Responsibility.
Answer: True. Each middleware decides whether to handle the request or pass it to next.
Q4. (Find the bug) public static List<string> Log = new(); in a singleton used by parallel tests.
Answer: Global mutable state: List<T> is not thread-safe and tests influence each other. Inject a logger instead.
Coding Challenge
Implement the Strategy pattern for unit conversion with delegates: a dictionary from unit name to Func<double, double> that converts km/h to the target unit, and convert 100 km/h to m/s and mph.
var strategies = new Dictionary<string, Func<double, double>>
{
["m/s"] = kmh => kmh / 3.6,
["mph"] = kmh => kmh / 1.609344,
["km/h"] = kmh => kmh,
};
foreach (string unit in new[] { "m/s", "mph" })
Console.WriteLine($"100 km/h = {strategies[unit](100):F2} {unit}");