Service Layer Architecture ========================== Framework-agnostic business logic extraction enabling cross-framework compatibility. Overview -------- The service layer architecture emerged from a practical problem: pyqt-reactive supports both PyQt6 and Textual UIs, but the business logic for parameter forms was duplicated between them. Every time we fixed a bug or added a feature, we had to implement it twice. UI frameworks naturally encourage mixing presentation logic with business logic. PyQt6 widgets know how to display themselves, so it's tempting to put parameter analysis logic directly in the widget creation code. But this creates tight coupling and makes code reuse impossible. The solution extracts all business logic into framework-agnostic service classes. UI frameworks become thin presentation layers that consume services. The same service can power both PyQt6 and Textual implementations. This pattern eliminated duplicated code and reduced parameter form bugs since fixes only need to be made once. Core Service Pattern -------------------- The core pattern is simple: create classes that contain only business logic, with no dependencies on UI frameworks. These services take data as input and return data as output, making them easy to test and reuse. Framework-Agnostic Services ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Services focus purely on the "what" and "how" of business logic, leaving the "where to display it" to UI frameworks. .. code-block:: python class ParameterFormService: """Framework-agnostic service for parameter form business logic.""" def __init__(self, debug_config: Optional[DebugConfig] = None): self.debugger = get_debugger(debug_config) self._field_id_generator = FieldIdGenerator() self._type_utils = ParameterTypeUtils() self._name_formatter = ParameterNameFormatter() def analyze_parameters(self, parameters: Dict[str, Any], parameter_types: Dict[str, Type], field_id: str) -> FormStructure: """Analyze parameters and create framework-agnostic form structure.""" # Business logic separated from UI framework structure = FormStructure(field_id=field_id) for param_name, param_type in parameter_types.items(): param_info = self._analyze_single_parameter( param_name, param_type, parameters.get(param_name) ) structure.parameters.append(param_info) return structure Service Integration Pattern --------------------------- PyQt6 Integration ~~~~~~~~~~~~~~~~~ .. code-block:: python class PyQt6ParameterFormManager: """PyQt6-specific UI implementation using shared service.""" def __init__(self): self.service = ParameterFormService() # Inject service self.widget_strategies = PyQt6WidgetStrategies() def build_form(self, parameters: Dict[str, Any], parameter_types: Dict[str, Type], field_id: str): # Use service for business logic structure = self.service.analyze_parameters(parameters, parameter_types, field_id) # UI framework handles only presentation for param_info in structure.parameters: widget = self._create_widget(param_info) self.layout.addWidget(widget) Textual Integration ~~~~~~~~~~~~~~~~~~~ .. code-block:: python class TextualParameterFormManager: """Textual-specific UI implementation using same service.""" def __init__(self): self.service = ParameterFormService() # Same service self.widget_strategies = TextualWidgetStrategies() def compose(self, parameters: Dict[str, Any], parameter_types: Dict[str, Type], field_id: str): # Identical business logic via service structure = self.service.analyze_parameters(parameters, parameter_types, field_id) # Different UI framework, same logic for param_info in structure.parameters: widget = self._create_textual_widget(param_info) yield widget Shared Infrastructure --------------------- Utility Class Integration ~~~~~~~~~~~~~~~~~~~~~~~~~ Services coordinate multiple utility classes: .. code-block:: python class ParameterFormService: def __init__(self): # Compose utility classes for shared functionality self._type_utils = ParameterTypeUtils() self._name_formatter = ParameterNameFormatter() self._field_id_generator = FieldIdGenerator() self._enum_formatter = EnumDisplayFormatter() def get_parameter_display_info(self, param_name: str, param_type: Type) -> ParameterDisplayInfo: """Coordinate utilities for parameter analysis.""" return ParameterDisplayInfo( name=param_name, display_name=self._name_formatter.to_display_name(param_name), is_optional=self._type_utils.is_optional_dataclass(param_type), field_id=self._field_id_generator.generate_field_id(param_name), enum_options=self._enum_formatter.get_enum_options(param_type) if self._is_enum(param_type) else None ) Context-Aware Behavior ~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python def get_reset_values(self, dataclass_type: Type, current_config: Any, is_global_config_editing: Optional[bool] = None) -> Dict[str, Any]: """Context-driven reset behavior.""" # Auto-detect editing mode if not explicitly provided if is_global_config_editing is None: is_global_config_editing = not LazyDefaultPlaceholderService.has_lazy_resolution(dataclass_type) if is_global_config_editing: # Global config editing: Use actual default values return self._get_static_defaults(dataclass_type) else: # Lazy config editing: Use None to show placeholder text return {field.name: None for field in dataclasses.fields(dataclass_type)} Cross-Framework Compatibility ----------------------------- Shared Data Structures ~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python @dataclass class FormStructure: """Framework-agnostic form structure.""" field_id: str parameters: List[ParameterInfo] nested_forms: Dict[str, 'FormStructure'] = field(default_factory=dict) @dataclass class ParameterInfo: """Framework-agnostic parameter information.""" name: str display_name: str param_type: Type is_optional: bool default_value: Any field_id: str Strategy Pattern Integration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python # Framework-specific strategies consume service output class PyQt6WidgetStrategies: def create_widget_for_parameter(self, param_info: ParameterInfo) -> QWidget: # Use service-provided parameter info for widget creation if param_info.param_type == bool: widget = QCheckBox() widget.setChecked(param_info.default_value) elif param_info.is_optional: widget = self._create_optional_widget(param_info) return widget Benefits -------- - **Framework Independence**: Same business logic works across PyQt6 and Textual - **Code Reuse**: Eliminates duplication between UI implementations - **Testability**: Business logic can be unit tested without UI dependencies - **Maintainability**: Changes to logic don't require UI modifications - **Separation of Concerns**: Clear boundary between business logic and presentation - **Context Awareness**: Services adapt behavior based on usage context See Also -------- - :doc:`parameter_form_service_architecture` - Service-oriented refactoring of parameter forms (in development) - :doc:`configuration_framework` - Configuration system architecture used by services - :doc:`step-editor-generalization` - Step editors that use service layer patterns - :doc:`code_ui_interconversion` - Code/UI interconversion patterns