496303471d
---ci--- project: atelier phase: 7 milestone: v0.1 status: complete phase_role: final milestone_complete: true requirements: covered: [ATELIER-01, ATELIER-02, ATELIER-03, ATELIER-04, ATELIER-05, ATELIER-06, ATELIER-07, ATELIER-08, ATELIER-09, ATELIER-10, ATELIER-11, ATELIER-12, ATELIER-13, ATELIER-14, ATELIER-15, ATELIER-16, ATELIER-17, ATELIER-18, ATELIER-19, ATELIER-20, ATELIER-21, ATELIER-22, ATELIER-23, ATELIER-24, ATELIER-25, ATELIER-26, ATELIER-27, ATELIER-28, ATELIER-29, ATELIER-30, ATELIER-31, ATELIER-32, ATELIER-33, ATELIER-34, ATELIER-35] partial: [] ship: milestone: v0.1 type: NFR tag: v0.0.7 merge: milestone/v0.1-atelier -> main release: https://git.cloudinit.dev/cloudinit-bot/atelier/releases/tag/v0.0.7 ---/ci--- Milestone v0.1 — Initial Framework (NFR, complete). 8 core principles (C1-C8), 11 domains, 110 domain principles, 27 derived docs, 4 good + 3 bad examples, 4 language docs, full matrix, 3 review docs. All 35 requirements covered. 7 patches (v0.0.0 pre-execution through v0.0.7 final). v0.0.7 IS the v0.1.0 milestone release.
4.5 KiB
4.5 KiB
Bad Example: Leaky Abstraction
An abstraction that leaks its implementation details, violating Atelier principles. Each violation is cited.
The Code
// UserRepository — "abstracts" the database
class UserRepository {
async findAll(): Promise<UserRow[]> {
// Leaks: returns the raw DB row type, not a domain User
return db.query('SELECT id, email, password_hash, created_at, deleted_at FROM users');
}
async findByEmail(email: string): Promise<UserRow | null> {
// Leaks: the caller must know to filter deleted_at
const rows = await db.query('SELECT * FROM users WHERE email = $1', [email]);
return rows[0] || null;
}
async save(user: UserRow): Promise<void> {
// Leaks: the caller must know the column names and the SQL
await db.query(
'UPDATE users SET email = $1, password_hash = $2, updated_at = now() WHERE id = $3',
[user.email, user.password_hash, user.id]
);
}
}
// Usage — the leak is visible
const repo = new UserRepository();
const user = await repo.findByEmail('jane@example.com');
if (user && !user.deleted_at) { // caller must know about soft delete
user.password_hash = await hash(newPassword); // caller must know the column
await repo.save(user); // caller must know it's an UPDATE
}
Violations
C6 Composability (Core)
- The abstraction is supposed to hide the database. It does not.
- The caller must know: the row type (
UserRow), the soft-delete column (deleted_at), the password column (password_hash), the SQL operation (UPDATE). - The abstraction is a thin wrapper. It composes nothing; it leaks everything.
- Fix: Return a domain
User(nopassword_hash, nodeleted_at). Hide soft delete (the repo filters it). Hide persistence (the caller callssave, notUPDATE).
C2 Clarity (Core)
- The caller's code is unclear:
if (user && !user.deleted_at)— what isdeleted_at? Why does the caller check it? - The abstraction was supposed to clarify. It muddied.
- Fix:
repo.findByEmail()returnsUser | null(already filtered). The caller does not know soft delete exists.
API P2 Clarity (API, by analogy)
- The repo's API exposes the DB schema in its return types.
UserRowis a DB concept, not a domain concept. - The public contract (return type) leaks the private implementation (the table).
- Fix: The return type is
User, a domain type.UserRowis internal.
Data P8 Lifecycle Awareness (Data)
- The soft-delete lifecycle (
deleted_at) is the repo's concern. The caller should not manage it. - By exposing
deleted_at, the repo forces every caller to remember the filter. A forgotten filter is a soft-delete leak. - Fix: The repo filters
deleted_at IS NULLin every query. The caller never seesdeleted_at.
Security P9 Secret Hygiene (Security)
password_hashis in the returnedUserRow. The caller now has access to the password hash.- A caller that logs
userlogs the hash. A caller that serializesuserserializes the hash. - Fix:
Userdoes not includepassword_hash. OnlyUserRepositoryandAuthService(internal) see it.
C4 Locality (Core)
- The SQL is in the repo, but the column knowledge (
password_hash,deleted_at) is in the caller. - A column rename touches the repo AND every caller. Locality is violated.
- Fix: Column names are local to the repo. The caller knows only the domain
User.
C5 Reversibility (Core)
- Changing the database (e.g., from SQL to NoSQL, or renaming a column) requires touching every caller.
- The abstraction was supposed to make the change local. It does not.
- Fix: The repo's interface (
findByEmail,save) is stable. The implementation changes; the callers do not.
What This Example Reveals
The leaky abstraction is the false promise of encapsulation. The class is named UserRepository (suggesting it abstracts persistence), but it returns raw DB rows, exposes lifecycle columns, and leaks secret fields. The abstraction exists in name only.
The cost:
- Every caller must know the DB schema (C6 violated).
- A schema change touches every caller (C5 violated, C4 violated).
- Secret fields leak to callers (Security P9 violated).
- The lifecycle is the caller's burden (Data P8 violated).
The fix is always the same: the abstraction's public type is the domain type, not the implementation type. UserRepository.findByEmail() returns User | null, where User has id, email, name — and nothing else. password_hash, deleted_at, UserRow are internal. The caller knows nothing about the database.