72 lines
2.2 KiB
Markdown
72 lines
2.2 KiB
Markdown
# Architecture Principles
|
|
|
|
## Standalone Subsystems
|
|
|
|
Each class should behave like a small standalone subsystem. It should own one clear responsibility, expose a narrow public interface, and avoid hidden dependencies on the internals of other classes.
|
|
|
|
Good subsystem boundaries:
|
|
- accept explicit inputs
|
|
- return explicit outputs
|
|
- keep internal state private
|
|
- avoid reaching into global state
|
|
- avoid performing unrelated work
|
|
- can be tested with recorded or fixture data
|
|
|
|
Examples:
|
|
- a weather client fetches forecast data
|
|
- a weather parser converts API payloads into forecast points
|
|
- a weather builder normalizes external forecast records for storage
|
|
- a storage class persists records
|
|
- Gibil makes decisions from snapshots
|
|
|
|
## Data Models Between Subsystems
|
|
|
|
Subsystems should communicate through shared data models rather than through source-specific payloads.
|
|
|
|
For example:
|
|
- Open-Meteo JSON should become `WeatherForecastRun`
|
|
- Modbus register reads should become `Observation`
|
|
- HASS entity state should become `Observation`
|
|
- Gibil should reason from `Snapshot`
|
|
|
|
This keeps the edges messy and the core calm.
|
|
|
|
## Side Effects At The Edges
|
|
|
|
Network calls, database writes, MQTT publishes, and filesystem writes should live at clear boundaries.
|
|
|
|
Core reasoning classes should generally be pure or nearly pure:
|
|
- input data in
|
|
- answer out
|
|
- no surprise I/O
|
|
|
|
Stateful classes are allowed, but their state should be deliberate and inspectable.
|
|
|
|
## Grow By Composition
|
|
|
|
Astrape should grow by connecting small subsystems together, not by building one large object that knows everything.
|
|
|
|
The desired shape is:
|
|
|
|
```text
|
|
source client -> parser -> model -> storage -> query/snapshot -> Gibil -> publisher
|
|
```
|
|
|
|
Each part should be replaceable without rewriting the others.
|
|
|
|
## Prefer Working Slices
|
|
|
|
Build one thin working path at a time. A thin slice may start with empty storage or recorded source data, but it should still follow the real subsystem boundaries.
|
|
|
|
For example, the weather slice can start with:
|
|
|
|
```text
|
|
Open-Meteo forecast run -> WeatherBuilder -> clean forecast records
|
|
```
|
|
|
|
Then grow into:
|
|
|
|
```text
|
|
Open-Meteo -> parser -> WeatherBuilder -> TimescaleDB -> weather_predictor.py
|
|
```
|