# Nw Code Design Oo

> OO code-design SSOT — the WHAT-to-design anti-smell catalog (Object Calisthenics, RPP smell taxonomy, effect isolation) shared by the solution architect (design-time) and the crafter (execution-time).

- Skill: `nwave-ai/nw-code-design-oo` (Agent Skill)
- Install (CLI): `npx skillmds@latest add nwave-ai/nw-code-design-oo`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nwave-ai/nw-code-design-oo/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: nWave-ai (https://skillmd.com/u/nwave-ai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/nwave-ai/nw-code-design-oo

---


# OO Code Design

Design-time anti-smell catalog for object-oriented work. The architect loads this
to design smell-free domain types and module boundaries BEFORE DISTILL authors
ATs; the crafter loads the same SSOT for the execution mechanics that enforce it.
This file is the WHAT-to-design subset. `nw-tdd-methodology` owns RED→GREEN
verification; `nw-refactor` owns behavior-preserving refactoring. Repository
runner and hooks own build, test, and commit execution.

## Object Calisthenics

9 design constraints for clean OO in the hexagonal core (Jeff Bay). Design domain
and application types to satisfy these; enforced at GREEN+COMMIT by the crafter.

| # | Rule | Design intent | Layer |
|---|------|---------------|-------|
| 1 | One indentation level per method | Forces decomposition | Domain, Application |
| 2 | No `else` keyword | Guard clauses, early returns | Domain, Application |
| 3 | Wrap all primitives and strings | Value objects at boundaries | Domain |
| 4 | First-class collections | Domain collection types | Domain |
| 5 | One dot per line | Law of Demeter | Domain, Application |
| 6 | No abbreviations | Intention-revealing names | All |
| 7 | Small entities (<50 LOC classes, <10 LOC methods) | SRP | Domain, Application |
| 8 | Max 2 instance variables per class | Promotes decomposition | Domain |
| 9 | No getters/setters | Tell, don't ask | Domain, Application |

**Rule 9 relaxation** — getters allowed for: DTOs/response objects at port
boundaries, CQRS read models, value objects with computed properties
(`Money.amount`), framework/ORM mapping. Rule 9 applies strictly to domain
entities and application services.

**Scope** — applies inside the hexagon (domain + application). Does NOT apply to
adapters, infrastructure, DTOs, or configuration.

## RPP Smell Taxonomy

Smell names an architect detects in a design before code is written. Detection is
design-time; the refactor execution (cascade planning, batch-then-verify) is
crafter-only in `nw-refactor`. Levels L1 Readability | L2 Complexity |
L3 Responsibilities | L4 Abstractions | L5 Design Patterns | L6 SOLID++.

| Level | Smell | Design-time implication |
|-------|-------|-------------------------|
| L3 | Primitive Obsession | Raw int/str carrying domain meaning → introduce a value object |
| L3 | Magic Numbers | Bare literal with hidden semantics → name it (enum / constant / value object) |
| L3 | Tell-Don't-Ask | Caller queries state then decides → move the decision onto the owning object |
| L3 | Feature Envy | Method reaches into another type's data → relocate behavior to that type |
| L3 | God Object | One type owns too many responsibilities → split along bounded responsibilities |
| L3 | Large Class | Class exceeds SRP / Calisthenics size → extract collaborators |
| L4 | Shotgun Surgery | One change forces edits across many sites → consolidate the concept into one home |

Design rule: a smell named here at design time must not survive into the design
artifact — resolve it in the type/boundary decisions, not at DELIVER.

## Effect Isolation

Design components so the bug class "side-effect-free function silently writes" is
non-representable, not testable-around. Push contract enforcement up the type
hierarchy as far as the language allows. Three levers:

- **Functional Core / Imperative Shell** (Bernhardt) — business logic in pure
  functions; effects only at a thin shell. Approximates IO-monad separation in
  any language.
- **Plan-value pattern** — dry-run / preview / validate functions return a `Plan`
  data value, never silent side effects (`dry_run(cfg) -> Plan` pure; `execute(plan)`
  the only impure call). Makes "preview wrote to disk" structurally impossible.
- **Capability injection** — pass restricted interfaces (`PlanRecorder`,
  `SafeFileSystem(root=tmp)`) at boundaries, never god-objects (`os`,
  `Path.home()`). Approximates capability typing via DI.

Per component, declare a **contract shape**: pure-function (return-only),
bounded-change (declared mutation set), or unbounded-preservation (must return a
Plan, never mutate). A driving port that "only reads" must not expose write
methods — split read/write into separate ports.

### Declared inputs at the boundary

The OO reading of `contract:declared-inputs-not-ambient-reads` (SSOT:
`nw-cross-cutting-invariants` — the gate list and the anchor live there, not
here). It is `Capability injection` above restated as a review question, because
that pattern is easy to agree with and easy not to apply.

> **What does this component read that nobody passed it?** For every gate the
> clause lists, decide at the boundary: a constructor parameter, an injected
> capability, or an explicit override — with the ambient lookup kept as a default
> the caller may state.

Resolve it **where the caller can see it**. A gate resolved lazily, at first use
deep inside a method, is read outside the window in which the caller — or a test
— controls the environment, so even a caller that wanted to declare it cannot.

Test-side mirror: the Algebraic Analysis Before the Scenario mandate
(`nw-test-design-mandates`), its declared-inputs question.

### Enumerate the outcomes before choosing the return type

> **How many outcomes does this operation have — and does its signature carry
> every one of them?** "It returns the value and raises on the bad case" is the
> wrong answer: that is N-1 outcomes in the type and one in the control flow.
> **If any outcome is not in the return type, put it there** — a value the caller
> branches on, with the illegal combinations unconstructible (enforce in the
> constructor: only the outcome that carries a payload may carry one, and it
> always does).

Why this is a design rule, not a style preference: `except` matches on class
**identity**, so a raised outcome couples the caller to the exact module object
the raiser was loaded from. Where the same package is reachable by more than one
path — a source tree plus an installed runtime, a harness that adjusts
`sys.path`, a plugin cache — that identity is not guaranteed. The `except` then
fails to match and a *handled* outcome escapes as a crash, in an environment
nobody was testing.

Keep `raise` for what it is good at: a **programming error** (a broken
precondition, an import-time drift guard) where crashing is the correct outcome
and no `except` is meant to cross a module boundary to catch it.

FP states the canonical form of the same rule — a total function into a sum type
(`nw-code-design-fp` § Railway, "Count the outcomes before you choose the return
type"). This is its OO translation.

Empirical anchor, 2026-08-06 (`des.cli.phases`): CI reported `UnknownPhaseName`
propagating out of `_resolve` from the line INSIDE its own `try`, with the
matching `except` on the next line. `resolve_phase` documented three outcomes,
returned two, threw the third. Returning all three closed the failure class
*without* establishing why the module loaded twice — a fix that does not depend
on that answer is a design fix rather than a patch.

## Cross-cutting invariants (load them — they are not restated here)

Paradigm- and role-independent rules live in ONE shipped home: `nw-cross-cutting-invariants`.
Load it alongside this skill and honour these clauses by id — they are NOT duplicated here:

- `data:consumer-known-before-produced` — a datum is produced only because a named consumer
  reads it, and you must name the JOIN KEY it will be related on. No reader, or no key → the
  datum is unjustified.
- `gate:self-explaining-what-why-how` — every rejection states WHAT / WHY / HOW.
- `gate:design-principles-gdp-1-9` — the canonical gate-design contract.

