God Class Refactoring Campaign (December 2025)

Major architectural improvements extracting focused services from oversized ViewModels and Repositories.

Status: ✅ Complete (PRs 357-371) Timeline: December 6-14, 2025


Overview

The refactoring campaign addressed code quality issues in several “god classes” - components that had accumulated too many responsibilities over time. The extraction pattern focuses on:

  1. Single Responsibility: Each service does one thing well
  2. Testability: Services can be unit tested in isolation
  3. Reusability: Services can be shared across ViewModels
  4. Maintainability: Smaller, focused classes are easier to understand and modify

Extracted Services

From FirebaseTournamentRepository (PR #358)

The FirebaseTournamentRepository was split into 6 focused services:

ServiceResponsibilityLOC
FirebaseTournamentLifecycleServiceCreate, delete, archive tournaments~500
FirebaseTournamentQueryServiceSearch and discovery operations~180
FirebaseParticipantServiceJoin, leave, participant management~340
FirebaseRoundServiceRound CRUD within tournaments~275
FirebaseScoringServiceScore submission and synchronization~470
FirebaseSecurityServicePermission checks, ownership validation~320

Location: app/src/main/java/com/archeryapprentice/data/repository/impl/services/

Architecture:

FirebaseTournamentRepository
    ├── FirebaseTournamentLifecycleService  (create, delete, status)
    ├── FirebaseTournamentQueryService      (search, discover)
    ├── FirebaseParticipantService          (join, leave, guests)
    ├── FirebaseRoundService                (round CRUD)
    ├── FirebaseScoringService              (scores, sync)
    └── FirebaseSecurityService             (permissions)

RoundDisplayService (PR #359)

Extracted from RoundManagementViewModel to handle all display data preparation:

Location: app/src/main/java/com/archeryapprentice/domain/services/RoundDisplayService.kt

Responsibilities:

  • Create display data for Historical and Details screens
  • Calculate MU-focused participant rankings
  • Aggregate scores and compute accuracy statistics
  • Resolve scoring subjects (individual vs team mode)

Key Methods:

suspend fun createRoundDisplayData(round: Round, settings: Settings?): RoundDisplayData
fun calculateAllParticipantRanks(round: Round, ...): Map<String, Int>
fun createParticipantScoreSummaries(round: Round, ...): List<ParticipantScoreSummary>
suspend fun recomputeRoundTotals(round: Round): RoundTotals

Resource Management:

  • Caches TournamentRepository instance to prevent coroutine leaks
  • Call cleanup() when service is no longer needed

TournamentRoundCreationService (PR #370)

Extracted from TournamentDetailsViewModel to centralize tournament round creation:

Location: app/src/main/java/com/archeryapprentice/domain/services/TournamentRoundCreationService.kt

Responsibilities:

  • Map TournamentParticipant to SessionParticipant with proper ownership
  • Calculate next round number for tournament
  • Create/resolve bow setup for tournament rounds
  • Create Round entity with proper tournament linkage

Ownership Rules:

when {
    participantId == currentUserId -> SessionParticipant.LocalUser(...)
    isGuest && addedBy == currentUserId -> SessionParticipant.GuestArcher(...)
    else -> SessionParticipant.NetworkUser(...)  // read-only
}

Key Methods:

fun mapParticipantsToSession(
    participants: List<TournamentParticipant>,
    currentUserId: String
): List<SessionParticipant>
 
suspend fun calculateNextRoundNumber(tournamentId: String): Int
 
suspend fun createTournamentRound(
    tournament: Tournament,
    participants: List<SessionParticipant>,
    numEnds: Int,
    arrowsPerEnd: Int,
    ...
): Round

ScoringViewModelDelegate (PR #371)

Coordination layer extracted from ActiveScoringScreen to manage ViewModel interactions:

Location: app/src/main/java/com/archeryapprentice/ui/roundScoring/ScoringViewModelDelegate.kt

Architecture Decision: This delegate coordinates between ViewModels but owns no state. All session state is owned by LiveScoringViewModel for scalability to tournament scenarios.

Responsibilities:

  • Abstract ViewModel calls for cleaner UI code
  • Coordinate session lifecycle between RoundViewModel and LiveScoringViewModel
  • Route scoring operations to appropriate ViewModel
  • Expose unified event flows to UI

Key Coordination Points:

// Session state flows through LiveScoringViewModel
val scoringSession get() = liveScoringViewModel.scoringSession
val liveLeaderboard get() = liveScoringViewModel.liveLeaderboard
 
// Arrow scoring operations
fun addArrowScore(score: Int, isX: Boolean)
fun addArrowScoreWithCoordinate(score: Int, isX: Boolean, coordinate: Offset)
fun editArrowScore(arrowNumber: Int, score: Int, isX: Boolean, coordinate: Offset?)
 
// Session lifecycle
suspend fun startScoringSession(roundId: Int)
suspend fun completeCurrentEnd()
suspend fun completeScoringSession()

LiveScoringViewModel Cleanup (PR #369)

Removed duplicate conflict resolution logic from LiveScoringViewModel:

Before:

  • determineConflictResolution() existed in both ViewModels
  • Confusion about which implementation to use

After:

  • Single implementation in appropriate location
  • Clear ownership of conflict resolution logic

Service Extraction Pattern

When extracting a service from a god class, follow this pattern:

1. Identify Cohesive Functionality

Look for methods that:

  • Share similar dependencies
  • Operate on the same data types
  • Are always called together
  • Have a clear, named responsibility

2. Create Service Class

class NewService(
    // Inject only the dependencies this service needs
    private val repository: Repository,
    private val logger: LoggingProvider = AndroidLoggingProvider()
) {
    // Move related methods here
    suspend fun doSomething(): Result { ... }
 
    // Add cleanup if holding resources
    fun cleanup() {
        // Release resources
    }
}

3. Update ViewModel

class MyViewModel(
    private val newService: NewService  // Inject service
) : ViewModel() {
 
    override fun onCleared() {
        super.onCleared()
        newService.cleanup()  // Release service resources
    }
 
    // Delegate to service
    fun doSomething() = viewModelScope.launch {
        newService.doSomething()
    }
}

4. Add Tests

class NewServiceTest {
    private lateinit var service: NewService
    private lateinit var mockRepository: Repository
 
    @BeforeTest
    fun setup() {
        mockRepository = mockk()
        service = NewService(mockRepository)
    }
 
    @Test
    fun `doSomething returns expected result`() = runTest {
        // Arrange
        every { mockRepository.getData() } returns testData
 
        // Act
        val result = service.doSomething()
 
        // Assert
        assertEquals(expected, result)
    }
}

Data Flow After Refactoring

Before (God Class)

UI → TournamentDetailsViewModel (2000+ LOC)
         ├── Create tournament
         ├── Delete tournament
         ├── Join tournament
         ├── Leave tournament
         ├── Create round
         ├── Score round
         ├── Sync scores
         └── Check permissions

After (Service Architecture)

UI → TournamentDetailsViewModel (~500 LOC)
         ├── TournamentRoundCreationService (create rounds)
         └── FirebaseTournamentRepository
                  ├── FirebaseTournamentLifecycleService
                  ├── FirebaseParticipantService
                  ├── FirebaseRoundService
                  ├── FirebaseScoringService
                  └── FirebaseSecurityService

Test Coverage Improvements

PR #357 added comprehensive test coverage for FirebaseTournamentRepository before the extraction:

Test CategoryTests Added
Tournament creation8
Participant management12
Round operations6
Score synchronization10
Security checks8
Total44

These tests were then used to validate the service extraction didn’t break functionality.


Benefits Achieved

Code Quality Metrics

MetricBeforeAfterImprovement
FirebaseTournamentRepository LOC2,400600-75%
TournamentDetailsViewModel LOC1,800500-72%
Average method count per class4512-73%
Test coverage35%78%+43pp

Developer Experience

  • Easier debugging: Issues are isolated to specific services
  • Faster onboarding: New developers understand smaller, focused classes
  • Safer refactoring: Changes are localized with clear boundaries
  • Better IDE support: Smaller files load and index faster

PRDescription
#357test(firebase): Comprehensive FirebaseTournamentRepository test coverage
#358refactor: Extract services from FirebaseTournamentRepository
#359refactor: Add RoundDisplayService to RoundManagementViewModel
#369refactor: Remove duplicate code from LiveScoringViewModel
#370refactor: Extract TournamentRoundCreationService
#371refactor: Extract ScoringViewModelDelegate from ActiveScoringScreen


Last Updated: 2025-12-21