ConstructiCat Logo
CodeBust.
Browse section ▾

Shallow Abstraction.

An AI assistant wraps code in extra functions, classes, or interfaces that add a layer of indirection without hiding any complexity or enabling reuse — abstractions that merely forward to a single call site.

##Signs and Symptoms

A reviewer recognizes Shallow Abstraction when a new function/class/interface adds a name and a layer but no leverage: its body restates its signature, it forwards straight through to one underlying call, it is invoked exactly once, or it hides no decision, invariant, or variation. The code looks layered and "enterprise-grade," but you have to read through every layer to understand anything — the abstraction encapsulates nothing.

Tell-tale signs:

  • Pass-through functions/methods whose body is a single delegating call with no added behavior.
  • Wrapper classes (FooServiceFooRepositorydb) where each layer just calls the next.
  • Single-implementation interfaces (IClock with only SystemClock) introduced speculatively "for testability" with no second impl or fake.
  • utils/helpers functions used in exactly one place.
  • Redundant aliasing/renaming that forwards a value unchanged.

This is the classic Middle Man / Lazy Class / Speculative Generality smell, and the flip side of OX Security's "over-specification" finding (hyper-specific, single-use solutions instead of generalizable components).

// AI added three "layers" that each forward to the next.
function getUserName(id: string) {
  return fetchUserName(id);            // adds nothing
}
function fetchUserName(id: string) {
  return userRepository.getName(id);   // adds nothing
}
class UserRepository {
  getName(id: string) {
    return db.query("SELECT name FROM users WHERE id = ?", [id]);
  }
}
// getUserName is the only call site, used once.
// Two of the three layers add zero behavior — pure Middle Man.

##Reasons for the Problem

Why models produce it

  • Imitating the shape of good architecture. Training corpora are saturated with tutorials and enterprise scaffolding — service/repository/factory layers, DI, one-impl interfaces. The model pattern-matches the ceremony of abstraction without the substance, because a named helper is locally plausible and "looks professional." Next-token generation optimizes for resembling well-structured code, not for whether a layer earns its keep.
  • No repo context. Generating locally, the model can't see that the helper is called once, or that a canonical abstraction already exists elsewhere — so it invents fresh shallow layers instead of reusing them. This is exactly GitClear's measured trend: refactored ("moved") lines fell from ~24% (2020) to under 10% (2024) while copy/pasted lines rose and code clones grew ~4x. Models add structure but never consolidate it.
  • By-the-book fixation + over-specification. OX Security found AI over-specifies in 80–90% of generated code, producing "narrow solutions that cannot be reused," and avoids refactors in 80–90% ("stops at good enough"). Shallow wrappers are the visible residue.
  • Prompt-literalism and sycophancy. Told to "add a service layer" or "make it clean/modular," the model adds layers literally even when YAGNI applies — the over-engineering / "god agent" tendency practitioners describe in the code-smells-for-AI-agents discussion.
  • Training-cutoff staleness. It will wrap a deprecated API in a thin adapter rather than migrate off it.

Why it hurts

  • Maintainability & review load. Every change must thread through dead layers; readers inline-read through Middle Men. OX's "Army of Juniors" point: reviewers can't keep pace with code that looks structured but has no real seams.
  • Correctness & security. arXiv 2509.20491 and SonarSource both note that aliasing and wrappers "obscure the decisive call sites," degrading data/control-flow and taint analysis — a wrapper can silently swallow an error or hide a sink from a scanner.
  • Tech-debt accrual. Speculative one-impl interfaces ossify; and because each shallow abstraction is single-use, the next variation gets copy-pasted instead of parameterized — directly feeding the clone growth GitClear measured. You pay for the indirection without ever collecting the reuse it promised.

##Treatment

Review & prompting tactics

  • Apply the Rule of Three: "Don't add a function/class/interface unless it's used in ≥2 places or hides a real decision/invariant. Inline single-use helpers (YAGNI)."
  • Force reuse: "Before adding a helper/service/interface, search the repo for an existing one and reuse it." Point the model at the canonical module.
  • Make it justify each layer: "List every new function/class and state what variation or invariant it hides; delete any that just forward."
  • Require it to run the linter (no-useless-constructor, no-useless-rename) and a duplication scan (jscpd) and fix what they flag.

Refactorings (name them): Inline Function, Inline Class, Remove Middle Man for pass-throughs; Collapse Hierarchy / drop the interface for single-impl interfaces. When duplication is the real driver, do Extract Function once into a genuinely shared, parameterized helper — not a wrapper per call site.

Before — Middle Man:

function getUserName(id: string) { return fetchUserName(id); }
function fetchUserName(id: string) { return userRepository.getName(id); }

After — Inline Function / Remove Middle Man (one call site, no behavior added):

const name = userRepository.getName(id);

Before — Speculative one-impl interface:

interface IClock { now(): number; }
class SystemClock implements IClock { now() { return Date.now(); } }

After — Collapse Hierarchy (use the concrete type; reintroduce the interface only when a second impl, e.g. a test fake, actually exists):

class SystemClock { now() { return Date.now(); } }

##Detected by