diff --git a/CHANGELOG.md b/CHANGELOG.md index cabc91cd..b84ced8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,8 @@ Releases up to 2.63.0 are listed in template ([#2102]) - The frontend-static-site template is retired; its theme, trailing-slash and 404 rules move to frontend-quality and frontend-content ([#2103]) +- Object-oriented design rules leave the core tier; declare base-oop in Extras to + keep them ([#2104]) ### Added @@ -212,6 +214,7 @@ Releases up to 2.63.0 are listed in [#2101]: https://github.com/solid-ai-dev/solid-ai-templates/pull/2101 [#2102]: https://github.com/solid-ai-dev/solid-ai-templates/pull/2102 [#2103]: https://github.com/solid-ai-dev/solid-ai-templates/pull/2103 +[#2104]: https://github.com/solid-ai-dev/solid-ai-templates/pull/2104 ## [2.90.0] - 2026-09-12 diff --git a/docs/architecture/05-building-block-view.md b/docs/architecture/05-building-block-view.md index 5ad885ec..e55110ef 100644 --- a/docs/architecture/05-building-block-view.md +++ b/docs/architecture/05-building-block-view.md @@ -60,15 +60,14 @@ The core tier, as the manifest states it today: - `templates/core/docs.md` - `templates/core/testing.md` - `templates/core/review.md` -- `templates/core/oop.md` Measured: 8 `[DEPENDS ON]` tokens across 5 templates name one of these files, and 5 manifest entries name a core id in `depends_on`. Declaring one is redundant rather than forbidden — the seeding reaches the file either way, and the directive documents a dependency a reader would otherwise -infer. Reached by the seeding alone: `base-review`, `base-oop`. A chain -assembled by walking the directives does not reach that set and reports -nothing about it, because nothing declares what is absent. +infer. Reached by the seeding alone: `base-review`. A chain assembled by +walking the directives does not reach that set and reports nothing about it, +because nothing declares what is absent. Every top-level list of entries except `core` and `fork_core` is a section diff --git a/docs/decisions/046-object-oriented-design-guidance-is-core.md b/docs/decisions/046-object-oriented-design-guidance-is-core.md index 2f14f26b..755d2f27 100644 --- a/docs/decisions/046-object-oriented-design-guidance-is-core.md +++ b/docs/decisions/046-object-oriented-design-guidance-is-core.md @@ -1,10 +1,10 @@ --- id: "046" -status: Accepted -date: 2026-09-11 +status: Superseded +date: 2026-10-21 category: composition supersedes: [] -superseded_by: [] +superseded_by: ["057"] --- # ADR-046: Object-oriented design guidance is core diff --git a/docs/decisions/057-object-oriented-design-guidance-is-opt-in.md b/docs/decisions/057-object-oriented-design-guidance-is-opt-in.md new file mode 100644 index 00000000..da9791d8 --- /dev/null +++ b/docs/decisions/057-object-oriented-design-guidance-is-opt-in.md @@ -0,0 +1,84 @@ +--- +id: "057" +status: Accepted +date: 2026-10-21 +category: composition +supersedes: ["046"] +superseded_by: [] +--- + +# ADR-057: Object-oriented design guidance is opt-in + +## Context + +`base-oop` sits in the core tier, so every chain loads it, including chains +whose code has no classes to design. + +| Force | What it shows | +| ----- | ------------- | +| Reach | Measured on `main` on 2026-10-21: `templates/core/oop.md` resolved into all 41 roots, documentation-only and static-site roots among them | +| Fit | SOLID, class hierarchies and design patterns govern object-oriented code; a documentation repository or a static site applies none of it | +| Content | The file held a GoF catalogue and a categorical ban on AOP frameworks, preferences rather than rules every project needs | +| The freeze | From 3.0.0 core-tier membership is frozen until 4.0.0, so leaving the core is possible only before the tag | +| The earlier gap | The file once reached only service chains, by accident of the graph; library and service projects still need a way to load it | + +## Decision + +`base-oop` leaves the core tier and becomes an opt-in template that a +project declares through `Extras` or a stack names in `depends_on`. + +## Rationale + +- A chain whose code has no classes stops carrying rules it cannot apply, + which answers the reach and fit forces. +- A library or service project still loads the file by declaring one + template, so the earlier gap does not return by accident: reach becomes a + stated choice instead of a graph side effect. +- Doing it before 3.0.0 is the only window the freeze leaves. + +## Design + +1. **Membership**: the manifest's `core:` entry drops `base-oop`; its entry + stays under `base:` at `templates/core/oop.md` +2. **Reach**: a project declares `base-oop` in `Extras`; a stack for an + object-oriented language MAY name it in `depends_on` +3. **IDs**: `base-oop`, `base-oop-restraint` and `base-oop-aop` keep their + names; the AOP section is retitled "Cross-cutting concerns" +4. **Content**: the GoF catalogue gives way to one rule, use a named + pattern where it fits and name it in code; the AOP-framework ban becomes + a rule to keep cross-cutting behaviour visible at its call site + +## Alternatives considered + +| Alternative | Why not | +| ----------- | ------- | +| Keep it in core, trimmed | Every non-object-oriented chain still carries it, and the freeze then fixes that until 4.0.0 | +| Name it in each object-oriented stack's `depends_on` only | Upstream ships no stacks; a project without a fork stack would have no way in but `Extras`, which this design already gives | +| Drop the file | Library and service projects lose the only interface and abstraction rules the templates hold | + +## Consequences + +| Area | Change | +| ---- | ------ | +| Chains | Every chain that does not declare `base-oop` loses the file | +| Manifest | `core:` drops `base-oop` | +| Generated docs | The core-tier listing in the building-block view regenerates | +| Projects | A project that applies object-oriented design adds `base-oop` to `Extras` | +| Forks | A fork's object-oriented stacks add `base-oop` to `depends_on` | +| Migration guide | Lists the move | + +## Compatibility + +No section ID is renamed or removed. A chain that relied on the core tier +for these rules adds `base-oop` to `Extras`; a template extending or +overriding one of its sections adds it to `depends_on`. The change ships in +3.0.0, before the freeze, and the CHANGELOG lists it under `Breaking +changes`. + +## Security + +None. + +## Related + +- `templates/core/oop.md`, the file this record moves out of the core tier. diff --git a/docs/migrating-to-v3.md b/docs/migrating-to-v3.md index 68a69afe..f9e1cd85 100644 --- a/docs/migrating-to-v3.md +++ b/docs/migrating-to-v3.md @@ -67,6 +67,7 @@ receives nothing (ADR-051). | `backend-quality`: N+1, async I/O, API deprecation, circuit breaker, outbox | `backend-quality` | already in `backend-database`, `backend-concurrency`, `backend-api`, `backend-errors`, `backend-messaging` | | `frontend-static-site`: one stylesheet, BEM naming, system fonts, the `localStorage` theme snippet, the page table, lint and format files | `frontend-static-site` | removed with the template; a fork's static-site stack carries its choices | | `static-site-theme`, `static-site-trailing-slash`, the custom 404 page, build-time rendering by default | `frontend-static-site` | `frontend-quality` and `frontend-content`, IDs kept | +| `base-oop`: SOLID, object design, patterns, cross-cutting concerns | the core tier | opt-in by `Extras`; the GoF catalogue and the AOP-framework ban are cut | | The numbered-document check after concurrent merges, the first-CI-run rule for a check on its own change | `base-git` | this repository's playbook; the `check-heading-numbers` hook still ships | | The submodule update after a pin bump, the base64 bundle mask, the `.vscode` allowlist | `base-git` | removed; a neutral editor-settings rule remains | | Factory classmethods; `*-overlay.png` naming | `base-oop`, `base-measurement` | neutral rules remain | diff --git a/templates/core/oop.md b/templates/core/oop.md index d9237c22..96ddb967 100644 --- a/templates/core/oop.md +++ b/templates/core/oop.md @@ -4,89 +4,67 @@ ## SOLID principles -Apply SOLID at the class, module, and service level: +Apply SOLID at the class, module and service level: -- **S — Single Responsibility**: every class or module has exactly one reason - to change; split anything that serves more than one concern -- **O — Open/Closed**: extend behaviour by adding new code, not by modifying - existing code; use interfaces, abstract base classes, or composition -- **L — Liskov Substitution**: subtypes must be fully substitutable for their - base type without altering correctness; never override a method in a way - that weakens its contract -- **I — Interface Segregation**: prefer many small, focused interfaces over - one large general-purpose one; callers should not depend on methods they - do not use -- **D — Dependency Inversion**: depend on abstractions, not concretions; - inject dependencies rather than instantiating them inside a class +- **Single responsibility**: give each class or module one reason to + change; split one that serves two concerns +- **Open/closed**: extend behaviour by adding code, not by editing it, + through interfaces, abstract base classes or composition +- **Liskov substitution**: keep every subtype substitutable for its base; + never override a method so that it weakens the contract +- **Interface segregation**: prefer small, focused interfaces; never make a + caller depend on methods it does not use +- **Dependency inversion**: depend on abstractions, and inject dependencies + rather than constructing them inside a class -## OOP +## Object design -- Prefer **composition over inheritance** — inherit only to model a true - is-a relationship; compose for code reuse -- **Encapsulate** implementation details — expose behaviour through a public - interface, hide state and implementation -- Design to interfaces (or protocols / abstract base classes), not concrete +- Prefer composition over inheritance; inherit only for a true is-a + relationship +- Encapsulate state and implementation; expose behaviour through a public + interface +- Design to interfaces, protocols or abstract base classes, not concrete types -- Keep class hierarchies shallow — more than two levels of inheritance is a - signal to refactor towards composition -- **Closed type families are tagged unions** — where every variant is - known when the type is written, discriminate a union by a literal field - (`kind`, `type`) so the compiler checks every branch handles every - variant; keep class hierarchies for families callers extend -- **Internal identifiers must align with their layer** — a module or class - whose name implies a role it does not hold (an `endpoints.py` with no - routes, a `*RestApi` class that is not the REST API) misleads readers and - forces clarifying asides; when the layering and the name disagree, rename - to match the layer +- Treat a hierarchy deeper than two levels as a signal to refactor toward + composition +- Model a closed type family, every variant known when the type is + written, as a tagged union on a literal field (`kind`, `type`), so the + compiler checks every branch +- Keep class hierarchies for families callers extend +- Name a module or class for the role its layer gives it; rename an + `endpoints.py` with no routes or a `*RestApi` that is not the REST API ## When not to reach for a class [ID: base-oop-restraint] -SOLID and the patterns above bias toward classes; this is the -counterweight. Reach for a class to encapsulate state, not to house -stateless logic. +SOLID and the patterns below bias toward classes; these rules are the +counterweight. -- **Prefer a free function for stateless logic** — introduce a class - only to encapsulate state or attach behaviour to state it owns. A pure - transform (inputs → output, no retained state) stays a function; a - class only couples it to state it does not use -- **A class whose only public method is `run()` / `execute()` is usually - a function in disguise** — keep the free function unless the retained - state genuinely warrants an object -- **Put state-mutating behaviour on the object that owns the state** — - avoid the anemic model where an external function mutates a data bag; - this is the active-voice complement of "encapsulate state, expose - behaviour" above -- **Construct through named constructors or factory methods on the type** - rather than free functions that build and return it +- Write stateless logic as a free function; introduce a class only to + encapsulate state it owns +- Treat a class whose only public method is `run()` or `execute()` as a + function in disguise, unless its retained state warrants an object +- Put state-mutating behaviour on the object that owns the state, never in + an external function that mutates a data bag +- Construct through named constructors or factory methods on the type, + not free functions that build and return it ## Design patterns -- Apply established **GoF design patterns** where they fit the problem — - do not invent ad-hoc solutions for problems that have named solutions -- Favour **behavioural patterns** for algorithm variation: - Strategy, Command, Observer, Template Method -- Favour **structural patterns** for object composition: - Adapter, Decorator, Facade, Proxy -- Use **creational patterns** to decouple object creation: - Factory Method, Abstract Factory, Builder +- Use an established pattern where it fits the problem; never invent an + ad-hoc solution to a problem that has a named one - Use **Singleton** only for stateless services or infrastructure objects (logger, config) — never for mutable shared state - Name the pattern in code when you use one: a class named `OrderExportStrategy` communicates intent; a class named `OrderHelper` does not -## Aspect-Oriented Programming (AOP) +## Cross-cutting concerns [ID: base-oop-aop] -- **Do not use AOP frameworks** — hidden cross-cutting behaviour (method - interception, bytecode weaving, runtime proxies) makes code hard to read, - debug, and test -- Implement cross-cutting concerns explicitly: - - Logging: call the logger directly in the function - - Auth: explicit middleware or guard in the call chain - - Transactions: explicit context manager or decorator with visible - call site - - Validation: explicit call at the boundary -- Transparent decorators (a decorator that wraps and clearly delegates) are - acceptable; opaque interceptors that inject hidden behaviour are not +- Keep cross-cutting behaviour visible at its call site: a logger call, an + auth middleware or guard, a transaction context manager, a validation + call at the boundary +- Never inject behaviour through method interception, bytecode weaving or + an opaque runtime proxy — it is hard to read, debug and test +- Use a decorator only when it wraps and visibly delegates diff --git a/templates/manifest.yaml b/templates/manifest.yaml index ddd49c09..98fe157d 100644 --- a/templates/manifest.yaml +++ b/templates/manifest.yaml @@ -18,7 +18,7 @@ version: "1.0" -core: [base-quality, base-git, base-docs, base-testing, base-review, base-oop] +core: [base-quality, base-git, base-docs, base-testing, base-review] base: - id: base-git @@ -74,7 +74,7 @@ base: depends_on: [base-data-modeling] - id: base-oop file: templates/core/oop.md - description: SOLID, OOP, GoF design patterns, AOP guidance + description: SOLID, object design, design patterns, cross-cutting concerns; opt-in - id: base-quality-gates file: templates/core/quality-gates.md description: How a check is built - retrofitting a gate, pairing a constraint with its check, reading a verdict diff --git a/tests/baselines/template-quality.txt b/tests/baselines/template-quality.txt index 514b8f45..968ad08a 100644 --- a/tests/baselines/template-quality.txt +++ b/tests/baselines/template-quality.txt @@ -24,7 +24,7 @@ templates/core/docs.md 99.1 templates/core/embedded-checks.md 83.6 templates/core/git.md 89.4 templates/core/measurement.md 95.2 -templates/core/oop.md 82.8 +templates/core/oop.md 96.0 templates/core/quality-gates.md 92.5 templates/core/quality.md 98.7 templates/core/review.md 100.0