AgentSkillsCN

code-quality-foundations

代码质量的四大支柱、目标设定、抽象层次,以及权衡思维。在评估代码质量、设定质量目标、选择抽象层级、做出设计权衡,或依据质量四大支柱对代码进行审计时使用此功能。涵盖可读性、模块化、可测试性、可重用性,以及“最小惊讶原则”。

SKILL.md
--- frontmatter
name: code-quality-foundations
description: Code quality pillars, goals, abstraction layers, and tradeoff thinking. Use when evaluating code quality, setting quality goals, choosing abstraction levels, making design tradeoffs, or auditing code against quality pillars. Covers readability, modularity, testability, reusability, and the principle of least astonishment.

Code Quality Foundations

Why code quality matters, what to aim for, and how to think about tradeoffs.

Quick Reference

The Four Goals of High-Quality Code

GoalQuestion to AskFailure Mode
It worksDoes it solve the problem correctly?Bugs, unhandled edge cases, unmet requirements
It keeps workingWill it survive changes around it?Brittle dependencies, no tests, hidden assumptions
It's adaptableCan requirements change without a rewrite?Rigid coupling, over-engineering, hardcoded assumptions
It doesn't reinvent the wheelAre we reusing proven solutions?Custom code for solved problems, duplicated effort

The Six Pillars of Code Quality

PillarWhat It MeansKey Technique
ReadableOther engineers can understand it quicklyDescriptive names, clean layers, consistent style
No surprisesBehavior matches expectations (POLA)Explicit contracts, no magic values, no hidden side effects
Hard to misuseDifficult to use incorrectlyType safety, immutability, validated construction
ModularComponents can be swapped independentlyInterfaces, DI, separation of concerns, cohesion
Reusable & generalizableSolves problems broadly, not just one caseFocused parameters, generics, avoiding assumptions
TestableCan be verified in isolationModularity enables testability; design for it from the start

POLA = Principle of Least Astonishment. If a function, API, or class does something a reasonable caller wouldn't expect, it's a bug in the design, even if it "works."

Modern Additions (2024-2026 Industry Consensus)

The six pillars remain sound. Modern practice elevates three additional concerns:

ConcernWhy It's Now ExplicitOriginal Coverage
SecureShift-left security; threat model at design timeImplicit in "works"
EfficientResource optimization is a design choice, not afterthoughtSubsumed under "works"
ReliableExplicit error recovery and graceful degradationSubsumed under "keeps working"

Goals in Depth

Code Should Work

The most basic goal, but deceptively hard to fully achieve. "Working" includes:

  • Meeting all stated requirements (including edge cases)
  • Performance constraints (latency, throughput) if specified
  • Security, privacy, and resource constraints

Tradeoff: Thoroughness vs. speed. Covering every edge case takes time. Assess risk: what's the cost of a specific edge case failing in production?

Code Should Keep Working

Code doesn't exist in isolation. It breaks when:

  • Dependencies are updated or changed
  • New features require modifications nearby
  • The problem domain itself evolves (business rules, user expectations, technology)

Code that works today but breaks tomorrow is not high quality. This goal drives testability, defensive coding, and explicit error handling.

Code Should Be Adaptable

Two failure modes to avoid:

ExtremeProblemSymptom
Over-engineeringPredicting every future requirement3-month project takes 12 months; predictions are wrong anyway
Under-engineeringIgnoring that requirements will changeFirst change requires a complete rewrite

The sweet spot: Use generally applicable techniques (modularity, clean interfaces, loose coupling) that make code adaptable without predicting specific changes. You can't know how requirements will change, but you can be certain they will.

Code Should Not Reinvent the Wheel

Reuse existing solutions because they:

  • Save time — using a library vs. building from scratch
  • Have fewer bugs — battle-tested code has been debugged by many
  • Carry existing expertise — maintainers understand the domain deeply
  • Are recognizable — other engineers know standard approaches instantly

This applies in both directions: use others' code when it exists, and structure your own code so others can reuse it.

Pillars in Detail

Make Code Readable

Readability is the foundation pillar — every other quality depends on engineers understanding what the code does. Unreadable code:

  • Gets misinterpreted during review, letting bugs through
  • Accumulates new bugs when modified by someone who doesn't understand it
  • Resists debugging because the original intent is unclear

Key practices:

  • Descriptive names (avoid abbreviations that aren't universally known)
  • Comments explain why, not what (the code explains what)
  • Consistent style (adopt and follow a style guide)
  • Shallow nesting (restructure conditionals, extract functions)
  • Functions that read like single sentences

(see code-readability -> Naming and Structure) (see code-readability -> Comment Strategy)

Avoid Surprises (POLA)

Code should behave the way a reasonable caller expects based on its name, types, and documentation. Common surprises:

  • Returning magic values instead of errors, nulls, or empty collections
  • Unexpected side effects (mutating inputs, writing to disk, network calls)
  • Functions that silently ignore invalid input instead of failing
  • Enum handling that breaks silently when new values are added

Test: If you explained the behavior to another engineer, would they say "that's weird"? Then it's a surprise.

(see code-antipatterns -> Surprise Antipatterns)

Make Code Hard to Misuse

Design APIs so incorrect usage fails at compile time, not at runtime:

  • Immutability — prevent unintended mutation of shared data
  • Dedicated types — use EmailAddress not String, Timestamp not Int
  • Single source of truth — for both data and logic, avoid duplication
  • Constructor validation — invalid objects simply can't exist

Analogy: TV sockets have different shapes so you can't plug the power cord into the HDMI port. Design your code the same way — make wrong usage structurally impossible.

(see code-antipatterns -> Misuse Antipatterns)

Make Code Modular

Modularity means components can be independently understood, tested, and replaced. Key concepts:

  • Cohesion — things that belong together stay together (sequential, functional)
  • Separation of concerns — each module handles one distinct problem
  • Dependency injection — pass dependencies in, don't hard-code them
  • Interfaces over concrete types — depend on abstractions

A class that's "too big" isn't about line count. It's about how many concepts it handles. If a class solves multiple distinct subproblems, it should be split — even if each subproblem only takes 20 lines.

(see code-readability -> Function Decomposition)

Make Code Reusable & Generalizable

ConceptMeaningExample
ReusableSame solution, different scenariosA drill works in walls, floors, and ceilings
GeneralizableSolves similar problemsA drill also drives screws (rotating things)

Practices:

  • Keep function parameters focused (take only what's needed)
  • Avoid unnecessary assumptions in low-level code
  • Beware of global state — it makes reuse unsafe
  • Consider generics when type-specific code limits applicability

Less code in a codebase means fewer bugs. Making code reusable reduces total volume.

Make Code Testable

Testability is a design constraint, not an afterthought. If code is hard to test, it's probably too tightly coupled or handling too many concerns.

The test pyramid:

  • Unit tests — individual functions/classes, fast feedback (seconds)
  • Integration tests — component interactions, broader coverage (minutes)
  • E2E tests — full user journeys, highest confidence but slowest (minutes-hours)

Many unit tests, fewer integration tests, fewest E2E tests. Balance speed of feedback with confidence in system correctness.

TDD note: Because testing is so integral to quality, some engineers advocate writing tests before the code (test-driven development). Whether or not you practice TDD strictly, asking "How will I test this?" during design improves the code.

(see code-testing-quality -> Test Strategy)

Layers of Abstraction

Why Layers Matter

Complex problems become manageable when broken into subproblems. Each layer:

  • Deals with only a few concepts at a time
  • Hides implementation details from the layer above
  • Can be independently tested, replaced, and understood

Example: Sending a message to a server is three lines of code — but involves HTTP, TCP, network protocols, and radio signals underneath. You don't need to know any of that because clean layers of abstraction hide it.

Layers and the Pillars

PillarHow Layers Help
ReadableEach layer deals with few concepts — engineers grok one layer at a time
ModularSwap implementations without affecting higher layers
ReusableSolutions to subproblems generalize to other contexts
TestableEach layer can be verified independently

APIs vs. Implementation Details

Every piece of code exposes a mini API:

  • API (public): Class names, public functions, parameters, return types, documentation
  • Implementation details (private): Internal algorithms, private functions, dependency choices

Rule: If an implementation detail leaks into the API (via parameter types, return types, or behavior), your layers of abstraction are not clean. Return types should be appropriate to the caller's layer, not the layer below.

Building Blocks

ConstructRole in LayeringGuidance
FunctionsSmallest unit of abstractionShould read like a sentence; do one thing or compose other functions
ClassesGroup cohesive data and behaviorOne concept per class; cohesive with a clear public API
InterfacesDefine contracts between layersDepend on interfaces, not concrete implementations

The One-Sentence Test

A well-structured function should translate into a single, readable sentence. If describing what a function does requires "and" or multiple independent clauses, it needs to be split.

<details> <summary>When layers get too thin</summary>

Over-splitting is real. If every function is one line that delegates to another one-line function, you've created fragmentation that hurts readability.

Signs of over-splitting:

  • Functions that only call one other function
  • Class names ending in "-Helper", "-Utils", "-Manager" with one method
  • You need to read 5+ files to understand one operation
  • Indirection without abstraction (layers that don't hide meaningful complexity)

Each layer should represent a genuine level of abstraction — a meaningful subproblem — not just a mechanical decomposition. The Clean Code debate (2024-2025) highlighted that dogmatic function-size rules can reduce comprehension when taken to extremes.

</details>

Tradeoff Thinking

Every Decision Has Consequences

Every design decision involves tradeoffs. Going in one direction limits the possibility to evolve in another. The longer systems live, the harder it is to reverse decisions.

The tradeoff analysis approach:

  1. Identify the decision and its context (threading, team size, SLAs)
  2. List alternatives (at least two)
  3. For each alternative, identify pros and cons
  4. Choose — accepting the cons of your choice
  5. Document why (for your future self and teammates)

Quality vs. Speed — A False Dichotomy

Writing high-quality code may seem slower initially, but:

  • Short term: Slightly more thought and effort upfront
  • Medium term: Far fewer bugs, easier debugging, faster feature delivery
  • Long term: The "hacky" approach compounds into technical debt

Analogy: Gluing a shelf to the wall takes 10 minutes vs. 30 for proper brackets. But when you need to move the shelf, repaint, or the shelf falls and damages the wall, you spend hours. "Less haste, more speed."

Context Changes Everything

A pattern that's optimal in one context may be harmful in another.

Example — Singleton pattern:

ContextBehaviorTradeoff
Single-threadedSimple, no contentionWorks great
Multi-threaded (synchronized)Thread-safe but ~120x slowerPerformance bottleneck
Multi-threaded (double-checked lock)Fast, thread-safeMore complex code
Thread confinementPer-thread instance, no contentionNot a true singleton anymore

Lesson: Always ask "In what context?" before judging a design decision. Validate assumptions with measurement, not intuition.

Common Quality Tradeoffs

TradeoffTensionGuidance
Speed vs. qualityShipping fast vs. getting it rightQuality pays off beyond throwaway code
Flexibility vs. simplicityFuture-proofing vs. YAGNIPrefer generally applicable techniques over speculative features
Abstraction vs. directnessClean layers vs. straightforward codeAbstract when the same subproblem appears twice
DRY vs. couplingRemoving duplication vs. creating dependenciesWrong abstraction is worse than duplication
Unit vs. integration testsFast feedback vs. holistic validationYou need both; the question is proportion

Testing Tradeoffs

DimensionUnit TestsIntegration TestsE2E Tests
SpeedFast (seconds)Medium (minutes)Slow (minutes-hours)
IsolationHighMediumLow (holistic)
Integration confidenceLowMediumHigh
Cost to createLowMediumHigh
Debugging easePinpoints exact failureNarrower than E2EHard to isolate cause

Plan testing strategy upfront. Deciding test proportions is an everyday tradeoff — not something to leave until the end.

<details> <summary>Measuring code quality</summary>

Use metrics as signals, not targets:

MetricHealthy RangeWhat It Indicates
Code coverage>80% of critical pathsTests exercise important logic
Cyclomatic complexity<15 per functionLogic isn't overly branched
Code churn<10%Code isn't constantly rewritten

Warning: Goodhart's Law applies — when a metric becomes a target, it ceases to be a good metric. 100% coverage achieved by testing trivial getters is worse than 80% coverage of critical business logic.

</details>

Decision Tables

"Should I Abstract This?"

SignalAction
Same logic appears in 2+ placesExtract to shared function/class
Function can't be described in one sentenceSplit into smaller functions
Class has method groups that use different fieldsConsider splitting into separate classes
Changing one feature requires touching many filesImprove modularity and layer boundaries
Only one use case existsWait — don't abstract prematurely

"Where Should I Invest Quality Effort?"

SituationFocus OnRationale
Greenfield projectModularity, clean interfacesFoundation decisions compound; structure early
Legacy codebaseTesting, readabilityUnderstand before changing safely
High-traffic serviceReliability, efficiency, testingProduction failures are expensive
Prototype / spikeWorking code, speedValidate the idea; plan to rewrite
Shared library / APIHard to misuse, no surprisesYou can't predict how consumers will use it
Security-sensitive codeSecurity, testabilityBreaches are catastrophic and trust-destroying

Checklists

Before Writing Code

  • Problem clearly understood (requirements, constraints, edge cases)
  • Existing solutions checked (libraries, shared code, prior art)
  • Testing strategy considered (how will this be verified?)
  • Key tradeoffs identified (what are we choosing, and what are we giving up?)

Code Quality Self-Review

  • Functions translate to single sentences
  • No surprises: behavior matches names and types
  • Hard to misuse: invalid states are unrepresentable where possible
  • Modular: changes are localized, not scattered
  • Testable: can be unit tested without complex setup
  • Clean abstractions: API doesn't leak implementation details

See Also

  • code-readability — Naming conventions, comments, structure, nesting (see code-readability -> Naming and Structure)
  • code-antipatterns — Common quality failures and how to spot them (see code-antipatterns -> Pattern Recognition)
  • refactoring-patterns — Techniques for improving existing code (see refactoring-patterns -> When to Refactor)
  • code-testing-quality — Test strategy, test pyramid, TDD (see code-testing-quality -> Test Strategy)
  • software-tradeoffs — Deep dives into specific software tradeoffs (see software-tradeoffs -> Analysis Framework)
  • code-review — Evaluating code quality during review (see code-review -> Review Checklist)