Library
A Philosophy of Software Design · 10 of 11
A Philosophy of Software Design
AI Software Development MEDIUM

Evaluating Software Trends Against Complexity

agile tdd oop design-patterns complexity-lens

Key Principle

Every software trend should be evaluated through a single litmus test: does it actually reduce complexity in large systems? Ousterhout applies this test to object-oriented programming, agile development, test-driven development, design patterns, and getters/setters — and finds that each, when adopted dogmatically, can increase complexity rather than reduce it.

Why This Matters

Software development is prone to bandwagon effects. Teams adopt practices because they are popular, not because they address the specific complexity problems the team faces. When a trend is adopted without critical evaluation, it introduces new constraints and ceremonies that may add cognitive load without compensating benefit. The result is worse than doing nothing: the team now has both the original complexity and the overhead of the trend.

The deeper failure mode is that trend adoption substitutes for design thinking. Teams believe that following TDD or using design patterns is doing design, when in fact these practices operate at a different level than the strategic decisions that determine system complexity.

Good Examples

  • Agile reframed: Incremental development is sound, but the increments should produce clean abstractions, not just working features. "Developing incrementally is generally a good idea, but the increments of development should be abstractions, not features." (Chapter 19) This preserves agile's feedback loops while preventing the design erosion that feature-driven sprints cause.
  • Interface inheritance deepens modules: Many implementations behind one signature is a genuinely deep design — it hides the variation behind a stable interface. This is OOP working well through the complexity lens.
  • TDD for bug fixes: Writing a test first to capture a failure condition, then verifying the fix passes — this is a case where TDD's structure aligns with good design practice rather than fighting it.

Counterpoints

  • TDD as tactical programming: "The problem with test-driven development is that it focuses attention on getting specific features working, rather than finding the best design." (Chapter 19) The write-test-then-pass cycle has no natural point for stepping back to consider structure.
  • Implementation inheritance leaks information: Unlike interface inheritance, implementation inheritance creates dependencies between parent and child classes — "a form of information leakage within the hierarchy." Developers must understand the entire class tree to change any part. Mitigation: prefer composition, or strictly separate parent-managed state from subclass-managed state.
  • Getters/setters and force-fitted patterns: Getters and setters fail the complexity test — they are shallow methods that expose implementation. Design patterns fail when force-fitted into situations where a custom approach would be simpler.

Key Quotes

"Whenever you encounter a proposal for a new software development paradigm, challenge it from the standpoint of complexity: does the proposal really help to minimize complexity in large software systems?" — John Ousterhout, Chapter 19

"The problem with test-driven development is that it focuses attention on getting specific features working, rather than finding the best design." — John Ousterhout, Chapter 19

"Developing incrementally is generally a good idea, but the increments of development should be abstractions, not features." — John Ousterhout, Chapter 19

Rules of Thumb

  • Before adopting any trend, ask: does this reduce complexity for my system, or does it add ceremony?
  • If a practice focuses attention on features rather than abstractions, compensate with explicit design phases
  • Prefer interface inheritance over implementation inheritance; prefer composition over both when the hierarchy creates tight coupling
  • Use TDD selectively — it fits bug reproduction well, but should not be the sole driver of system design
  • Never force-fit a design pattern; if a simpler custom solution exists, use it

Related References