God Class Refactoring Campaign

Major architectural improvements to the Archery Apprentice codebase through systematic extraction of services from god classes. This campaign (December 6-14, 2025) significantly improved testability, maintainability, and code organization.

Overview

Three god classes were identified with excessive responsibilities:

God ClassOriginal LinesMethodsExtracted Services
LiveScoringViewModel2,016345+ services
HybridTournamentRepository1,87850+Planned 6+ repos
TournamentDetailsViewModel1,200+25+2 services

Campaign PRs

PRDateDescriptionImpact
#357Dec 8FirebaseTournamentRepository test coverageFoundation
#358Dec 8Extract services from FirebaseTournamentRepository400+ lines extracted
#359Dec 8RoundDisplayService to RoundManagementViewModel150+ lines extracted
#369Dec 9Remove duplicate code from LiveScoringViewModel200+ lines removed
#370Dec 9Extract TournamentRoundCreationService180+ lines extracted
#371Dec 14Extract ScoringViewModelDelegate300+ lines extracted

Extracted Services

RoundDisplayService

Location: shared/domain/src/commonMain/kotlin/com/archeryapprentice/domain/services/RoundDisplayService.kt

Handles round display logic and formatting:

class RoundDisplayService {
    fun formatRoundTitle(round: Round): String
    fun formatRoundSubtitle(round: Round): String
    fun formatRoundProgress(round: Round, participant: Participant): String
    fun calculateRoundCompletion(round: Round): Float
    fun getRoundStatusIcon(status: RoundStatus): ImageVector
    fun getRoundStatusColor(status: RoundStatus): Color
}

Extracted From: RoundManagementViewModel Benefits:

  • Reusable across iOS and Android
  • 100% unit testable
  • Clear single responsibility

TournamentRoundCreationService

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

Handles round creation and validation logic:

class TournamentRoundCreationService(
    private val roundRepository: RoundRepository,
    private val tournamentRepository: TournamentRepository
) {
    suspend fun createRound(
        tournamentId: String,
        roundConfig: RoundConfiguration
    ): Result<Round>
 
    fun validateRoundConfiguration(config: RoundConfiguration): ValidationResult
 
    suspend fun getNextRoundNumber(tournamentId: String): Int
 
    suspend fun canCreateRound(tournamentId: String): Boolean
}

Extracted From: TournamentCreationViewModel, RoundCreationViewModel Benefits:

  • Centralized round creation logic
  • Consistent validation across creation flows
  • Easier to test business rules

ScoringViewModelDelegate

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

Handles active scoring state management:

class ScoringViewModelDelegate(
    private val scoringRepository: ScoringRepository,
    private val statisticsService: StatisticsAggregationService
) {
    val currentEndScores: StateFlow<List<Int>>
    val currentArrowNumber: StateFlow<Int>
    val endProgress: StateFlow<Float>
 
    suspend fun addScore(score: Int, isX: Boolean)
    suspend fun removeLastScore()
    suspend fun finalizeEnd()
    fun getCurrentEndTotal(): Int
    fun canUndo(): Boolean
}

Extracted From: ActiveScoringScreen (previously embedded logic) Benefits:

  • Separates scoring state from UI
  • Reusable between different scoring screens
  • Testable without Android dependencies

CreatorAuthorizationService

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

Handles tournament creator authorization checks:

class CreatorAuthorizationService(
    private val authProvider: AuthProvider,
    private val tournamentRepository: TournamentRepository
) {
    suspend fun isCurrentUserCreator(tournamentId: String): Boolean
    suspend fun canModifyTournament(tournamentId: String): Boolean
    suspend fun canManageParticipants(tournamentId: String): Boolean
    suspend fun canDeleteTournament(tournamentId: String): Boolean
}

Extracted From: TournamentDetailsViewModel Benefits:

  • Centralized authorization logic
  • Security rules in one place
  • Easy to audit and test

TournamentParticipationService

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

Handles participant join/leave operations:

class TournamentParticipationService(
    private val participantRepository: ParticipantRepository,
    private val validationRules: ValidationRules
) {
    suspend fun joinTournament(
        tournamentId: String,
        participant: TournamentParticipant
    ): Result<Unit>
 
    suspend fun leaveTournament(
        tournamentId: String,
        participantId: String
    ): Result<Unit>
 
    fun validateJoinRequest(
        tournament: Tournament,
        participant: TournamentParticipant
    ): ValidationResult
}

Extracted From: TournamentDetailsViewModel Benefits:

  • Self-validating join/leave operations
  • Consistent error handling
  • Reusable across ViewModels

Previously Extracted Services

These services were extracted in earlier refactoring efforts:

TournamentSyncService (~400 lines)

Real-time tournament synchronization with Firebase.

ScoreConflictResolutionService (~150 lines)

Handles score conflicts in multi-device scenarios.

EndCompletionService (~100 lines)

Manages end completion workflow.

TournamentRoundLifecycleService (~80 lines)

Handles round status transitions.

StatisticsAggregationService (~100 lines)

Calculates scoring statistics.


Architecture Patterns

Service Extraction Pattern

When extracting services from god classes:

// 1. Identify cohesive method groups
class GodClass {
    // Group A: Display formatting
    fun formatTitle(): String
    fun formatSubtitle(): String
    fun getStatusIcon(): Icon
 
    // Group B: Validation
    fun validateInput(): Boolean
    fun checkPermissions(): Boolean
}
 
// 2. Create focused service
class DisplayService {
    fun formatTitle(): String
    fun formatSubtitle(): String
    fun getStatusIcon(): Icon
}
 
// 3. Inject service into ViewModel
class RefactoredViewModel(
    private val displayService: DisplayService
) {
    fun getFormattedTitle() = displayService.formatTitle()
}

Service Categories

CategoryExamplesLocation
Domain ServicesRoundDisplayService, StatisticsServiceshared/domain/services/
Repository ServicesTournamentParticipationServiceapp/domain/services/
ViewModel DelegatesScoringViewModelDelegateapp/ui/*/

Data Flow After Refactoring

┌─────────────────────────────────────────────────────────────┐
│                         UI Layer                            │
│                  (Compose/SwiftUI Screens)                  │
└─────────────────────────────┬───────────────────────────────┘
                              │
┌─────────────────────────────▼───────────────────────────────┐
│                      ViewModel Layer                        │
│             (Thin orchestrators, state holders)             │
└─────────────────────────────┬───────────────────────────────┘
                              │
┌─────────────────────────────▼───────────────────────────────┐
│                      Service Layer                          │
│   DisplayService │ ValidationService │ AuthorizationService │
└─────────────────────────────┬───────────────────────────────┘
                              │
┌─────────────────────────────▼───────────────────────────────┐
│                     Repository Layer                        │
│         TournamentRepository │ RoundRepository              │
└─────────────────────────────────────────────────────────────┘

Test Coverage Improvements

ComponentBeforeAfter
RoundDisplayServiceN/A100%
TournamentRoundCreationServiceN/A95%
ScoringViewModelDelegateN/A90%
CreatorAuthorizationServiceN/A100%
TournamentParticipationServiceN/A92%
LiveScoringViewModel45%78%

Test Patterns Established

class RoundDisplayServiceTest {
    private lateinit var service: RoundDisplayService
 
    @BeforeEach
    fun setup() {
        service = RoundDisplayService()
    }
 
    @Test
    fun `formatRoundTitle returns correct format for standard round`() {
        val round = Round(name = "Practice", roundNumber = 1)
        val result = service.formatRoundTitle(round)
        assertEquals("Round 1: Practice", result)
    }
 
    @Test
    fun `calculateRoundCompletion returns 0 for empty round`() {
        val round = Round(ends = emptyList())
        val result = service.calculateRoundCompletion(round)
        assertEquals(0f, result)
    }
}

Metrics

Code Size Reduction

ViewModelBeforeAfterReduction
LiveScoringViewModel2,016~1,40031%
TournamentDetailsViewModel1,200~85029%
RoundManagementViewModel800~60025%

Complexity Reduction

MetricBeforeAfter
Avg. methods per ViewModel30+15-20
Max cyclomatic complexity2512
Avg. constructor dependencies84-5

Future Extraction Plans

HybridTournamentRepository Split (Planned)

The HybridTournamentRepository (1,878 lines) is planned for extraction into focused repositories:

RepositoryLinesPriority
TournamentSettingsRepository80Week 22
TournamentDiscoveryRepository280Week 22
TournamentSyncRepository250Week 23
TournamentRoundsRepository150Week 24
TournamentModerationRepository100Week 25

LiveScoringViewModel Further Extraction

Additional services planned:

  • ArrowScoringDomainService - Pure scoring business logic
  • ParticipantStateService - Multi-participant state management

Best Practices Established

1. Extract When You See

  • 3+ methods with shared prefix (e.g., formatX, calculateX)
  • Methods that don’t need ViewModel dependencies
  • Logic duplicated across ViewModels
  • Pure functions without side effects

2. Service Naming

  • *Service for domain logic (e.g., RoundDisplayService)
  • *Delegate for ViewModel helpers (e.g., ScoringViewModelDelegate)
  • *Repository for data access (e.g., TournamentDiscoveryRepository)

3. Dependency Injection

All extracted services use constructor injection:

class TournamentDetailsViewModel @Inject constructor(
    private val authorizationService: CreatorAuthorizationService,
    private val participationService: TournamentParticipationService,
    private val tournamentRepository: TournamentRepository
) : ViewModel()

4. Testing First

Before extraction:

  1. Write tests for existing behavior
  2. Extract to service
  3. Verify tests still pass
  4. Add service-specific edge case tests