Files
meshcore-gui/docs/SOLID_ANALYSIS.md
2026-03-09 17:53:29 +01:00

10 KiB

SOLID Analysis — MeshCore GUI

1. Reference: standard Python OOP project conventions

Convention Norm This project
Package with subpackage when widgets emerge widgets/ subpackage (6 classes)
One class per module every module ≤1 class
Entry point outside package meshcore_gui.py beside package
__init__.py with version only __version__
Constants in own module config.py
No circular imports acyclic dependency tree
Type hints on public API 84/84 methods typed
Private methods with _ prefix consistent
Docstrings on modules and classes present everywhere
PEP 8 import order stdlib → third-party → local

Dependency tree (acyclic)

config        protocols
  ↑               ↑
shared_data    worker
  ↑            main_page  →  widgets/*
  ↑            route_builder  ←  route_page
  ↑
meshcore_gui.py  (only place that knows the concrete SharedData)

No circular dependencies. config and protocols are leaf nodes; everything points in one direction. Widgets depend only on config (for constants) and NiceGUI — they have zero knowledge of SharedData or protocols.


2. SOLID assessment per principle

S — Single Responsibility Principle

"A class should have only one reason to change."

Module Class Responsibility Verdict
config.py (no class) Constants and debug helper Single purpose
protocols.py (Protocol classes) Interface contracts Single purpose
shared_data.py SharedData Thread-safe data store See note
ble/worker.py SerialWorker Serial communication thread Single purpose
main_page.py DashboardPage Dashboard layout orchestrator See note
route_builder.py RouteBuilder Route data construction (pure logic) Single purpose
route_page.py RoutePage Route page rendering Single purpose
widgets/device_panel.py DevicePanel Header, device info, actions Single purpose
widgets/map_panel.py MapPanel Leaflet map with markers Single purpose
widgets/contacts_panel.py ContactsPanel Contacts list + DM dialog Single purpose
widgets/message_input.py MessageInput Message input + channel select Single purpose
widgets/message_list.py MessageList Message feed + channel filter Single purpose
widgets/rx_log_panel.py RxLogPanel RX log table Single purpose

SharedData: 15 public methods in 5 categories (device updates, status, collections, snapshots, lookups). This is deliberate design: SharedData is the single source of truth between two threads. Splitting it would spread lock logic across multiple objects, making thread-safety harder. The responsibility is "thread-safe data access" — that is one reason to change.

DashboardPage: After the widget decomposition, DashboardPage is now 148 lines with only 4 methods. It is a thin orchestrator that composes six widgets into a layout and drives the update timer. All rendering and data-update logic has been extracted into the widget classes. The previous ⚠️ for DashboardPage is resolved.

Conclusion SRP: No violations. All classes have a single, well-defined responsibility.


O — Open/Closed Principle

"Open for extension, closed for modification."

Scenario How to extend Existing code modified?
Add new page New module + @ui.page in entry point Only entry point (1 line)
Add new command _handle_command() case Only ble/worker.py
Add new contact type TYPE_ICONS/NAMES/LABELS in config Only config.py
Add new dashboard widget New widget class + compose in DashboardPage Only main_page.py
Add new route info Extend RouteBuilder.build() Only route_builder.py

Where not ideal: _handle_command() in SerialWorker is an if/elif chain. In a larger project, a Command pattern or dict-dispatch would be more appropriate. For 4 commands this is pragmatically correct.

Conclusion OCP: Good. Extensions touch only one module.


L — Liskov Substitution Principle

"Subtypes must be substitutable for their base types."

There is no inheritance in this project. All classes are concrete and standalone. This is correct for the project scale — there is no reason for a class hierarchy.

Where LSP does apply: The Protocol interfaces (SharedDataWriter, SharedDataReader, ContactLookup, SharedDataReadAndLookup) define contracts that SharedData implements. Any object that satisfies these protocols can be substituted — for example a test stub. This is LSP via structural subtyping.

Conclusion LSP: Satisfied via Protocol interfaces. No violations.


I — Interface Segregation Principle

"Clients should not be forced to depend on interfaces they do not use."

Client Protocol Methods visible SharedData methods not visible
SerialWorker SharedDataWriter 10 5 (snapshot, flags, GUI commands)
DashboardPage SharedDataReader 4 11 (all write methods)
RouteBuilder ContactLookup 1 14 (everything else)
RoutePage SharedDataReadAndLookup 5 10 (all write methods)
Widget classes (none — receive Dict/callback) 0 15 (all methods)

Each consumer sees only the methods it needs. The protocols enforce this at the type level. Widget classes go even further: they have zero knowledge of SharedData and receive only plain dictionaries and callbacks.

Conclusion ISP: Satisfied. Each consumer depends on a narrow, purpose-built interface.


D — Dependency Inversion Principle

"Depend on abstractions, not on concretions."

Dependency Before (protocols) After (protocols)
SerialWorker → SharedData Concrete ⚠️ Protocol (SharedDataWriter)
DashboardPage → SharedData Concrete ⚠️ Protocol (SharedDataReader)
RouteBuilder → SharedData Concrete ⚠️ Protocol (ContactLookup)
RoutePage → SharedData Concrete ⚠️ Protocol (SharedDataReadAndLookup)
Widget classes → SharedData N/A No dependency at all
meshcore_gui.py → SharedData Concrete Concrete (composition root)

The composition root (meshcore_gui.py) is the only place that knows the concrete SharedData class. All other modules depend on protocols or receive plain data. This is standard DIP practice: the wiring layer knows the concretions, the business logic knows only abstractions.

Conclusion DIP: Satisfied. Constructor injection was already present; now the abstractions are explicit.


3. Protocol interface design

Why typing.Protocol and not abc.ABC?

Python offers two approaches for defining interfaces:

Aspect abc.ABC (nominal) typing.Protocol (structural)
Subclassing required Yes (class Foo(MyABC)) No
Duck typing compatible No Yes
Runtime checkable Yes Optional (@runtime_checkable)
Python version 3.0+ 3.8+

Protocol was chosen because SharedData does not need to inherit from an abstract base class. Any object that has the right methods automatically satisfies the protocol — this is idiomatic Python (duck typing with type safety).

Interface map

SharedDataWriter (SerialWorker)
├── update_from_appstart()
├── update_from_device_query()
├── set_status()
├── set_connected()
├── set_contacts()
├── set_channels()
├── add_message()
├── add_rx_log()
├── get_next_command()
└── get_contact_name_by_prefix()

SharedDataReader (DashboardPage)
├── get_snapshot()
├── clear_update_flags()
├── mark_gui_initialized()
└── put_command()

ContactLookup (RouteBuilder)
└── get_contact_by_prefix()

SharedDataReadAndLookup (RoutePage)
├── get_snapshot()
├── clear_update_flags()
├── mark_gui_initialized()
├── put_command()
└── get_contact_by_prefix()

4. Summary

Principle Before protocols With protocols With widgets Change
SRP Good Good Good Widget extraction resolved DashboardPage size
OCP Good Good Good Widgets are easy to add
LSP N/A Satisfied via Protocol Satisfied via Protocol
ISP ⚠️ Acceptable Good Good Widgets have zero SharedData dependency
DIP ⚠️ Acceptable Good Good

Changes: Protocol interfaces

# Change Files affected
1 Added protocols.py with 4 Protocol interfaces New file
2 SerialWorker depends on SharedDataWriter ble/worker.py
3 DashboardPage depends on SharedDataReader main_page.py
4 RouteBuilder depends on ContactLookup route_builder.py
5 RoutePage depends on SharedDataReadAndLookup route_page.py
6 No consumer imports shared_data.py directly All consumer modules

Changes: Widget decomposition

# Change Files affected
1 Added widgets/ subpackage with 6 widget classes New directory (7 files)
2 MeshCoreGUI (740 lines) replaced by DashboardPage (148 lines) + 6 widgets main_page.py, widgets/*.py
3 DashboardPage is now a thin orchestrator main_page.py
4 Widget classes depend only on config and NiceGUI widgets/*.py
5 Maximum decoupling: widgets have zero SharedData knowledge All widget modules

Metrics

Metric Monolith With protocols With widgets
Files 1 8 16
Total lines 1,395 ~1,500 ~1,955
Largest class (lines) MeshCoreGUI (740) MeshCoreGUI (740) SharedData (263)
Typed methods 51 (partial) 51 (partial) 90/90
Protocol interfaces 0 4 4