SOLID Principles and Clean Code

Engineering tools live for years and are extended constantly (new ECUs, new protocols, new tests).

Concept

Single Responsibility A class has one reason to change TestRunner that also parses CAN logs, writes HTML and sends e-mail

Open for extension, closed for modification A growing switch over check types in the runner Liskov Substitution

Subtypes must be usable wherever the base type is expected Square: Rectangle changing both sides; overrides that throw NotSupportedException Interface Segregation

Clients shouldn't depend on methods they don't use One IDiagnostics with 40 members for a read-only viewer Dependency Inversion

High-level policy depends on abstractions; details implement them

new VectorCanDriver() inside business logic Meaningful names: activeDtcCount, not n2; methods are verbs (ParseFrame), booleans read as questions (IsExtended).

Small methods doing one thing at one level of abstraction.

No duplication (DRY) — but don't merge things that only look alike.

Clear abstractions and explicit error handling (exceptions or result types, never silent catch {}).

Comments explain *why*, not *what*; prefer self-documenting code and named constants over magic numbers.

Code smells: long method, large class, long parameter list, feature envy, primitive obsession (string vin everywhere instead of a Vin type), shotgun surgery, dead code.

Refactoring: improve structure without changing behaviour, in small steps protected by tests (extract method, introduce parameter object, replace conditional with polymorphism).

Maintainability: code is read far more often than written.

Why It Matters

Engineering tools live for years and are extended constantly (new ECUs, new protocols, new tests). SOLID and clean code determine whether adding a check means writing one new class or editing twenty files.

Syntax

interface ICheck { string Name { get; } bool Passes(Reading r); }        // OCP + ISP
class TestRunner(IEcu ecu, IReporter reporter) { ... }                   // SRP + DIP

Example

55.1 The five principles in code 55.2 Refactoring a smelly method

// ---- LSP violation: a Square is-a Rectangle mathematically, but not behaviourally ----
Rectangle r = new Square();
r.Width = 4; r.Height = 5;
Console.WriteLine($"LSP violation: expected area 20, got {r.Area}  (Square changed both sides)");
IShape[] shapes = [new Rect(4, 5), new Sq(4)];
Console.WriteLine($"LSP fixed with immutable shapes: {string.Join(", ", shapes.Select(s => $"{s.GetType().Name} {s.Area}"))}");

// ---- SRP + DIP: each class has one reason to change; high-level policy depends on abstractions ----
var runner = new TestRunner(new FakeEcu(), new ConsoleReporter());
runner.Run(["VIN", "DTC", "Voltage"]);

// ---- OCP: add a new check without modifying the runner (new class, same interface) ----
IReadOnlyList<ICheck> checks = [new VoltageCheck(11.5, 14.8), new TemperatureCheck(105)];
var reading = new Reading(Volts: 12.1, TempC: 108);
foreach (var c in checks) Console.WriteLine($"OCP: {c.Name,-11} {(c.Passes(reading) ? "PASS" : "FAIL")}");

// ---- ISP: small role interfaces; a read-only client doesn't depend on flashing ----
IDtcReader reader = new FullDiagnosticClient();
Console.WriteLine($"ISP: read-only tool sees only {string.Join(",", typeof(IDtcReader).GetMetho
…
// Before: one long method with magic numbers, unclear names, mixed responsibilities
static string Proc(string l)
{
    var p = l.Split(' ');
    if (p.Length < 3) return "bad";
    var x = Convert.ToUInt32(p[1], 16);
    if (x > 0x7FF) return "ext";
    var d = p[2].Length / 2;
    if (d > 8) return "bad";
    return "ok " + x + " " + d;
}

// After: small, named, single-purpose pieces; constants; a result type; no duplication
Console.WriteLine($"{"line",-28} {"before",-10} after");
foreach (string line in new[] { "0.5 7E0 02100300000000", "0.6 18FEF100 0102", "x", "0.7 100 01020304050607080910" })
    Console.WriteLine($"{line,-28} {Proc(line),-10} {FrameParser.Parse(line)}");

static class FrameParser
{
    private const uint MaxStandardId = 0x7FF;
    private const int MaxClassicCanBytes = 8;

    public static ParseResult Parse(string line)
    {
        string[] fields = line.Split(' ');
        if (fields.Length < 3) return ParseResult.Invalid("expected 'time id data'");
        uint id = Convert.ToUInt32(fields[1], 16);
        if (id > MaxStandardId) return ParseResult.Invalid("extended identifiers are not supported");
        int byteCount = fields[2].Length / 2;
        
…

Expected Result

The first line proves the LSP violation: code written against Rectangle got area 25 instead of 20.

Same decisions, but the refactored version says *why* a line was rejected, uses named constants, and returns the identifier in hex as engineers expect. (Characterization tests like this comparison table are how you refactor legacy code safely.)

LSP violation: expected area 20, got 25  (Square changed both sides)
LSP fixed with immutable shapes: Rect 20, Sq 16
SRP/DIP: VIN      EHBAUREV250000001  PASS
SRP/DIP: DTC      none               PASS
SRP/DIP: Voltage  12.6               PASS
OCP: Voltage     PASS
OCP: Temperature FAIL
ISP: read-only tool sees only ReadDtcs
line                         before     after
0.5 7E0 02100300000000       ok 2016 7  id 0x7E0, 7 bytes
0.6 18FEF100 0102            ext        invalid: extended identifiers are not supported
x                            bad        invalid: expected 'time id data'
0.7 100 01020304050607080910 bad        invalid: 10 data bytes exceed 8

Common Mistakes

Applying SOLID mechanically — an interface for every class, dozens of one-method classes.

Inheritance for code reuse where composition fits (breaks LSP easily).

"Clean" code without tests — refactoring becomes guessing.

Comments that repeat the code or drift out of date.

Giant refactorings mixed with feature changes in one commit.

Best Practices

Refactor continuously in small, tested steps ("leave the campground cleaner").

Introduce abstractions at real seams: hardware, I/O, time, external services.

Use value objects against primitive obsession (Vin, CanId, FirmwareVersion).

Let analyzers and code review enforce conventions (Module 60).

Interview Relevance

Very high: "explain SOLID with examples", "what is the Liskov principle and give a violation", "dependency inversion vs dependency injection?", "what code smells do you know?", "how do you refactor legacy code safely?".

Practice Questions

Find an SRP violation in your own code and split the class.

Replace a switch on a TestType enum with polymorphic ICheck classes.

Introduce a parameter object for a method with seven parameters.

Write characterization tests before refactoring the Proc method.

Quick Quiz

Q1. (Multiple choice) A read-only dashboard is forced to implement Flash and ClearDtcs. Which principle is violated? A) SRP B) OCP C) LSP D) ISP Answer: D.

Q2. (Predict the output) Rectangle r = new Square(); r.Width = 4; r.Height = 5; r.Area

Answer: 25 (verified) — the Liskov violation.

Q3. (True/False) Dependency inversion means using a DI container.

Answer: False. It's a design principle (depend on abstractions); a DI container is one way to wire it.

Q4. (Find the bug) if (x > 0x7FF) return "ext"; — what's the smell?

Answer: A magic number and a cryptic result; use a named constant (MaxStandardId) and a descriptive result type.

Coding Challenge

Refactor this OCP violation so that new report formats can be added without modifying ReportWriter: string Write(string format, string[] rows) => format switch { "csv" => string.Join(",", rows), "md" => ..., _ => throw ... };

var writer = new ReportWriter([new CsvFormat(), new MarkdownFormat()]);
Console.WriteLine(writer.Write("csv", ["P0A80", "U0100"]));
Console.WriteLine(writer.Write("md", ["P0A80", "U0100"]));

interface IReportFormat { string Name { get; } string Render(string[] rows); }
class CsvFormat : IReportFormat { public string Name => "csv"; public string Render(string[] rows) => string.Join(",", rows); }
class MarkdownFormat : IReportFormat { public string Name => "md"; public string Render(string[] rows) => string.Join("\n", rows.Select(r => $"- {r}")); }

class ReportWriter(IEnumerable<IReportFormat> formats)
{
    private readonly Dictionary<string, IReportFormat> _formats = formats.ToDictionary(f => f.Name);
    public string Write(string format, string[] rows) =>
        _formats.TryGetValue(format, out var f) ? f.Render(rows) : throw new NotSupportedException(format);
}