Skip to content

Invert the Recipes <-> Plc dependency cycle (RecipeSession <-> PlcLifecycleManager) #110

Description

@mrcsin

Summary

The Recipes and Plc subsystems depend on each other, so neither can be understood, tested, or replaced alone, and the DI extension methods no longer describe module boundaries.

The cycle

  • Recipes/RecipeSession.cs:28, 624-630RecipeSession (Recipes) depends on IPlcSyncService (Plc) and pushes every accepted snapshot into PLC sync.
  • Plc/PlcLifecycleManager.cs:23, 229PlcLifecycleManager (Plc) depends back on RecipeSession and drives its LoadAsCurrent* methods.
  • Recipes/RecipeDi.cs:21AddRecipe() registers PlcLifecycleManager (a Plc class), cementing the cycle at registration level; enabling "Recipe" silently pulls the whole Plc graph.

Related defects on the same boundary

  1. Callback re-injection (property injection in disguise). PlcLifecycleManager.cs:208-225: RegisterReconnectApplyCallback(Func<Recipe, Task<Result>>) is a mutable register-exactly-once setter that the UI must call (RecipeCoordinator.cs:121); its doc says the callback "owns the UI-thread marshalling" — Core control flow designed around a UI concern. Violates the project's constructor-DI-only rule and creates a hidden call-order constraint with Initialize().
  2. Two-phase init. PlcLifecycleManager.cs:67-77: a separate Initialize() must be called before the object works; forgetting it silently disables connection-state handling.
  3. IPlcSyncService violates ISP and sits on the producer side. Plc/IPlcSyncService.cs:8-29: RecipeSession uses only IsSyncEnabled + NotifyRecipeChanged; the lifecycle/control surface (SetSyncEnabled, ResetForDisable, HandleConnectionLost, UpdateConnectionState, observables) is used only by PlcLifecycleManager. Project convention says interfaces belong on the consumer side.

Suggested fix

Small, mechanical inversions; dependency then flows one way (Plc → Recipes):

  • RecipeSession: replace the IPlcSyncService dependency with an event/IObservable<(Recipe, bool IsValid)> RecipeChanged; the Plc side subscribes.
  • PlcLifecycleManager: replace RegisterReconnectApplyCallback with an event/IObservable<Recipe> ReconnectRecipeAvailable — symmetric with the existing PlcRecipeConflictDetected. Fold Initialize() into construction or EnableSync.
  • Split IPlcSyncService: a narrow IRecipeSyncNotifier defined next to RecipeSession (or drop it entirely once the event exists); keep the control surface internal to Plc.
  • Move PlcLifecycleManager registration from RecipeDi into S7Di (or a dedicated AddPlc()).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions