Clean Fracanto

Clean Fracanto #2 — SOLID

SOLID — Five Principles, One Consistent Pattern

Five Principles, One Consistent Pattern

The SOLID principles were formulated for object-oriented languages, but their core — loose coupling, high cohesion, dependency inversion — is language-agnostic. The fracanto framework demonstrates that pure C can not only adhere to these principles but, through the explicitness of function pointers and opaque contexts, expresses them with particular clarity.

Single Responsibility Principle (SRP)

Each module should have exactly one reason to change.

In the fracanto framework, every component has a clearly delineated responsibility:

The consequence: When the SPI configuration of the DAC8552 changes, exactly one driver is affected. When a new message type is added, can_msg changes, but neither HAL, drivers, nor the state machine.

Open/Closed Principle (OCP)

Open for extension, closed for modification.

The vtable pattern is the OCP engine of the framework. To add a new HAL backend — say for the RP2040 with MCP2518FD — no existing code needs modification. One implements the eight functions of fracanto_hal_ops_t, packs them into a vtable, and passes it during initialization. The entire framework code above remains untouched.

The same applies to modules: A new synthesizer module implements the 16 callbacks of module_ops_t and thereby becomes part of the system without the framework itself being modified. The extension points are the vtables — they define what is extensible while simultaneously protecting existing code from modification.

Liskov Substitution Principle (LSP)

Subtypes must behave such that they can replace the supertype.

In fracanto there is no inheritance, but the LSP applies to vtable implementations. The SocketCAN HAL and the STM32-FDCAN HAL must behave identically as far as the eight HAL operations are concerned: send() sends a frame, set_filter() configures a receive filter, get_tick_ms() returns a monotonic timestamp. That SocketCAN operates poll-based and STM32-FDCAN interrupt-based is an implementation detail that the vtable conceals.

The 263 driver tests implicitly verify the LSP: The same driver code that runs with mock functions on macOS runs on the STM32 with real SPI, I2S, and GPIO functions — without changing a single line. If the mocks violated the LSP, the tests would pass on the host, but the hardware integration would fail.

Interface Segregation Principle (ISP)

No client should be forced to depend on interfaces it does not use.

Instead of a monolithic "hardware interface," fracanto defines five lean vtables for different concerns:

vtableOperationsPurpose
fracanto_hal_ops_t8CAN bus access
panel_ops_tEncoder, Buttons, LEDsPhysical controls
io_ops_tADC, DACAnalog I/O
audio_ops_tDMA Start/Stop, BufferReal-time audio
module_ops_t16 CallbacksModule logic

A module without an audio output implements no audio_ops_t. A module without a physical panel ignores panel_ops_t. The interfaces are cut so that each client sees only the operations it actually needs.

Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions.

The DIP is the foundation of the entire fracanto architecture. The node state machine (high-level) does not depend on the STM32-FDCAN implementation (low-level) but on the fracanto_hal_ops_t abstraction. The audio pipeline does not depend on the PCM5102A but on audio_ops_t. The parameter system does not depend on concrete encoders but on panel_ops_t.

The inversion is to be taken literally: In a naive design, the state machine would call stm32_fdcan_send() directly. In fracanto, it calls fracanto_hal_send(hal, &frame) — an inline wrapper that routes through the vtable. The dependency is inverted: The low-level implementation depends on the interface (it must implement it), not the other way around.