Skip to content

Commit 8dd8ba3

Browse files
committed
docs: add spec for #208 constructor injection / Ctrl decomposition
Two-phase Ctrl init: constructor keeps only config, new initServices() method builds all action objects with narrow sub-interface params. Scope: 7 classes from #207 + ScanEntries. Commands/providers out of scope. #208
1 parent c012a48 commit 8dd8ba3

1 file changed

Lines changed: 116 additions & 0 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
---
2+
issue: "#208"
3+
date: 2026-05-17
4+
slug: 208-constructor-injection
5+
plan: docs/plans/2026-05-17-208-constructor-injection.md
6+
---
7+
8+
# Spec: Decompose `Ctrl` god object via constructor injection (#208)
9+
10+
## Goal
11+
12+
Replace `constructor(public ctrl: JournalController)` in the six action/UI modules with explicit narrow-interface parameters so each class declares exactly which services it needs.
13+
14+
## Why now
15+
16+
`#207` gave every class a typed abstraction (`JournalController`) but they still receive the entire controller object. This preserves all the blast-radius and hidden-dependency problems identified in the issue: a change to any service on `Ctrl` potentially recompiles every downstream class, and it is impossible to tell from a constructor signature alone which services a class actually uses. Now that sub-interfaces are defined, the next logical step is to wire them directly.
17+
18+
## In scope
19+
20+
Narrow constructor injection for the six modules updated in #207 plus `ScanEntries`:
21+
22+
| Class | Current | After |
23+
|---|---|---|
24+
| `Inject` | `ctrl: JournalController` | `config: IConfiguration, logger: ILogger` |
25+
| `Parser` | `ctrl: JournalController` | `config: IConfiguration, logger: ILogger` |
26+
| `Writer` | `ctrl: JournalController` | `config: IConfiguration, logger: ILogger, inject: IInject` |
27+
| `Reader` | `ctrl: JournalController` | `config: IConfiguration, logger: ILogger, writer: IWriter, ui: IDialogues` |
28+
| `Dialogues` | `ctrl: JournalController` | `config: IConfiguration, logger: ILogger, parser: IParser` |
29+
| `ConsoleLogger` | `ctrl: JournalController, channel` | `config: IConfiguration, channel` |
30+
| `ScanEntries` | `ctrl: JournalController` | `config: IConfiguration, logger: ILogger` |
31+
32+
`Ctrl` becomes a two-phase object:
33+
- **Phase 1 (constructor):** creates `Configuration` only; all other fields `undefined`
34+
- **Phase 2 (`initServices(logger, channel)`):** creates all service instances in dependency order, passing narrow sub-interfaces; `Ctrl` fields become fully populated
35+
36+
`Startup.registerLoggingChannel` triggers Phase 2 by calling `ctrl.initServices(channel)`. Internally `Ctrl.initServices` creates `ConsoleLogger` first (since `Configuration` is already available), then creates each action class with its specific dependencies.
37+
38+
The `logger` getter on `Ctrl` retains its "throw if unset" guard — behaviour is unchanged; logger must be initialized before any commands run.
39+
40+
## Out of scope
41+
42+
- `src/provider/commands/*` — these receive `ctrl: Ctrl` (concrete); narrowing them is separate Phase 2.3 work
43+
- `src/ext/startup.ts` API surface — `run()`, `registerCommands()` etc. signatures unchanged
44+
- No behavior changes; pure structural refactor
45+
46+
## Acceptance criteria
47+
48+
1. None of the seven updated classes has a `ctrl` constructor parameter or field
49+
2. `Ctrl` has an `initServices(channel: vscode.OutputChannel): void` method; its constructor no longer instantiates action classes
50+
3. `Startup.registerLoggingChannel` calls `ctrl.initServices(channel)` instead of `ctrl.logger = new ConsoleLogger(ctrl, channel)`
51+
4. `npm run compile` zero errors
52+
5. `npm run lint` zero new warnings
53+
6. `npm test` 126 tests pass (no regressions)
54+
7. Grep: `grep -rn "this\.ctrl" src/actions/ src/ext/dialogues.ts src/util/logger.ts src/provider/features/scan-entries.ts` → no output
55+
56+
## Entities / contracts
57+
58+
### `Ctrl` constructor + `initServices`
59+
60+
```typescript
61+
// Phase 1 — constructor (called by Startup constructor)
62+
constructor(vscodeConfig: vscode.WorkspaceConfiguration) {
63+
this._config = new Configuration(vscodeConfig);
64+
}
65+
66+
// Phase 2 — called by Startup.registerLoggingChannel after channel exists
67+
initServices(channel: vscode.OutputChannel): void {
68+
const logger = new ConsoleLogger(this._config, channel);
69+
const inject = new Inject(this._config, logger);
70+
const writer = new Writer(this._config, logger, inject);
71+
const parser = new Parser(this._config, logger);
72+
const ui = new Dialogues(this._config, logger, parser);
73+
const reader = new Reader(this._config, logger, writer, ui);
74+
this._logger = logger;
75+
this._inject = inject;
76+
this._writer = writer;
77+
this._parser = parser;
78+
this._ui = ui;
79+
this._reader = reader;
80+
}
81+
```
82+
83+
`ScanEntries` is not constructed in `Ctrl`; it continues to be created inside `Dialogues.constructor` (now as `new ScanEntries(config, logger)`).
84+
85+
### Updated constructors (bare signatures)
86+
87+
```typescript
88+
new Inject(config: IConfiguration, logger: ILogger)
89+
new Writer(config: IConfiguration, logger: ILogger, inject: IInject)
90+
new Parser(config: IConfiguration, logger: ILogger)
91+
new Reader(config: IConfiguration, logger: ILogger, writer: IWriter, ui: IDialogues)
92+
new Dialogues(config: IConfiguration, logger: ILogger, parser: IParser)
93+
new ConsoleLogger(config: IConfiguration, channel: vscode.OutputChannel)
94+
new ScanEntries(config: IConfiguration, logger: ILogger)
95+
```
96+
97+
### `Ctrl` field access by consumers
98+
99+
Getters (`ctrl.config`, `ctrl.logger`, `ctrl.parser`, etc.) remain unchanged. Providers and commands that receive `ctrl: Ctrl` continue to compile without modification.
100+
101+
## Constraints
102+
103+
- `Startup` must not be changed except in `registerLoggingChannel` (swap `new ConsoleLogger(ctrl, channel)` for `ctrl.initServices(channel)`) to keep the PR diff minimal
104+
- `Ctrl` constructor must remain callable with only `vscode.WorkspaceConfiguration``Startup` constructor does `new Ctrl(this.config)` today
105+
- All field types on `Ctrl` that were private become `private` (not public) — getters are the public API
106+
- TypeScript `strict` + `noImplicitReturns` — all getter guards remain
107+
108+
## Open questions
109+
110+
None.
111+
112+
## Related issues
113+
114+
- Builds on: #207 (introduced `JournalController` interface and sub-interfaces)
115+
- Related: #201 (barrel removal in `controller.ts` — independent track)
116+
- See `docs/PLAN.md` Phase 2 (DI replacement)

0 commit comments

Comments
 (0)