This plan transfers proven refactoring patterns from UnityHiroChallenges to UnityHiroEventLeaderboards. The changes improve testability, resource management, error handling, and code clarity.
Purpose: Separate Unity lifecycle from business logic.
Changes:
- Create new
EventLeaderboardsViewBehaviour.csthat:- Is a MonoBehaviour attached to the scene
- Creates the Controller and View when coordinator is ready
- Manages cleanup in
OnDestroy() - Acts as glue between Unity and business logic
Pattern from Challenges:
public class EventLeaderboardsViewBehaviour : MonoBehaviour
{
private EventLeaderboardsController _controller;
private EventLeaderboardsView _view;
private void Start()
{
// Wait for coordinator, then create controller/view
}
private void OnDestroy()
{
_view?.Dispose();
}
}Purpose: Enable unit testing without MonoBehaviour.
Changes:
- Remove MonoBehaviour inheritance
- Add constructor injection for dependencies
- Add
ArgumentNullExceptionvalidation for all dependencies
Before:
public class EventLeaderboardsController : MonoBehaviour
{
// Uses this.GetSystem<T>() at runtime
}After:
public class EventLeaderboardsController
{
private readonly NakamaSystem _nakamaSystem;
private readonly IEventLeaderboardsSystem _eventLeaderboardsSystem;
public EventLeaderboardsController(
NakamaSystem nakamaSystem,
IEventLeaderboardsSystem eventLeaderboardsSystem)
{
_nakamaSystem = nakamaSystem ?? throw new ArgumentNullException(nameof(nakamaSystem));
_eventLeaderboardsSystem = eventLeaderboardsSystem ?? throw new ArgumentNullException(nameof(eventLeaderboardsSystem));
}
}Purpose: Proper resource cleanup and event unsubscription.
Changes:
- Add
IDisposableinterface - Add thread-safe disposal pattern with lock
- Unsubscribe all events in
Dispose()
Purpose: Cancel async operations on dispose/account switch.
Add to View:
private CancellationTokenSource _cts = new();
private readonly object _disposeLock = new();
private volatile bool _disposed;
private void ThrowIfDisposedOrCancelled()
{
if (_disposed)
throw new ObjectDisposedException(nameof(EventLeaderboardsView));
_cts.Token.ThrowIfCancellationRequested();
}Pattern:
public void Dispose()
{
lock (_disposeLock)
{
if (_disposed)
return;
_disposed = true;
}
_cts.Cancel();
_cts.Dispose();
// Unsubscribe all events
AccountSwitcher.AccountSwitched -= OnAccountSwitched;
_leaderboardList.selectionChanged -= OnLeaderboardSelectionChanged;
// ... other event unsubscriptions
}Purpose: Prevent silent failures in async event handlers.
Pattern for all async void handlers:
private async void OnAccountSwitched()
{
try
{
ThrowIfDisposedOrCancelled();
ShowSpinner();
await RefreshEventLeaderboardsAsync();
}
catch (OperationCanceledException)
{
// Expected on dispose - ignore
}
catch (Exception e)
{
ShowError(e.Message);
Debug.LogException(e);
}
}Purpose: Proper stack trace logging.
Files to update:
EventLeaderboardsView.cs- all catch blocksEventLeaderboardsController.cs- all catch blocksHiroEventLeaderboardsCoordinator.cs- error handling
Purpose: Fail-fast when UI elements are missing.
Add extension:
public static class VisualElementExtensions
{
public static T RequireElement<T>(this VisualElement parent, string name) where T : VisualElement
{
var element = parent.Q<T>(name);
if (element == null)
throw new InvalidOperationException($"Required UI element '{name}' of type {typeof(T).Name} not found");
return element;
}
}Before:
_submitScoreButton = rootElement.Q<Button>("submit-score-button");After:
_submitScoreButton = rootElement.RequireElement<Button>("submit-score-button");Add extensions:
public static void Show(this VisualElement element) =>
element.style.display = DisplayStyle.Flex;
public static void Hide(this VisualElement element) =>
element.style.display = DisplayStyle.None;
public static void SetDisplay(this VisualElement element, bool visible) =>
element.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;Purpose: Decouple account switching from view references.
Create/Update AccountSwitcher.cs:
public static class AccountSwitcher
{
public static event Action AccountSwitched;
public static async Task<ISession> SwitchAccountAsync(...)
{
var newSession = await AuthenticateAndStoreAccountAsync(...);
AccountSwitched?.Invoke();
return newSession;
}
}In View constructor/initialization:
AccountSwitcher.AccountSwitched += OnAccountSwitched;In Dispose:
AccountSwitcher.AccountSwitched -= OnAccountSwitched;Check EventLeaderboardTimeUtility.cs for:
.Secondsshould be.TotalSecondsfor duration comparisons.Minutesshould be.TotalMinutesfor duration comparisons
Example fix:
// WRONG
if (timeRemaining.Seconds > 0)
// CORRECT
if (timeRemaining.TotalSeconds > 0)Purpose: Reduce EventLeaderboardsView.cs size (currently 770 lines).
Options:
- Extract each modal to separate classes (e.g.,
SubmitScoreModal,EventInfoModal) - Or create a generic
ModalManagerclass
Audit and update any public fields that should be properties:
// Before
public List<IEventLeaderboard> EventLeaderboards;
// After
public List<IEventLeaderboard> EventLeaderboards { get; } = new();Add summaries to main classes:
/// <summary>
/// Controller/Presenter for the Event Leaderboards system.
/// Handles business logic and coordinates with Hiro systems.
/// </summary>
public class EventLeaderboardsController { ... }| Priority | Phase | Estimated Scope | Risk |
|---|---|---|---|
| 1 | Phase 3.2 | Small | Low - logging only |
| 2 | Phase 6.1 | Small | Medium - bug fix |
| 3 | Phase 4 | Medium | Low - extensions |
| 4 | Phase 2 | Medium | Medium - disposal |
| 5 | Phase 3.1 | Medium | Low - error handling |
| 6 | Phase 1 | Large | High - architecture |
| 7 | Phase 5 | Medium | Medium - account switching |
| 8 | Phase 7 | Optional | Low - cleanup |
| File | Changes |
|---|---|
EventLeaderboardsController.cs |
Remove MonoBehaviour, add constructor injection |
EventLeaderboardsView.cs |
Add IDisposable, disposal pattern, CTS, error handling |
HiroEventLeaderboardsCoordinator.cs |
Update to create ViewBehaviour |
EventLeaderboardTimeUtility.cs |
Audit TotalSeconds usage |
New EventLeaderboardsViewBehaviour.cs |
MonoBehaviour lifecycle management |
New VisualElementExtensions.cs |
RequireElement, Show/Hide |
- After each phase: Run existing tests, verify no regressions
- Phase 1 completion: Add unit tests for Controller (now testable)
- Phase 2 completion: Add disposal tests
- Phase 5 completion: Test account switching scenarios
- Each phase can be committed separately
- Phase 1 is the largest change - consider breaking into smaller commits
- Phases 2-4 can be done in parallel after Phase 1
- Phase 7 is optional polish