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 Class | Original Lines | Methods | Extracted Services |
|---|---|---|---|
| LiveScoringViewModel | 2,016 | 34 | 5+ services |
| HybridTournamentRepository | 1,878 | 50+ | Planned 6+ repos |
| TournamentDetailsViewModel | 1,200+ | 25+ | 2 services |
Campaign PRs
| PR | Date | Description | Impact |
|---|---|---|---|
| #357 | Dec 8 | FirebaseTournamentRepository test coverage | Foundation |
| #358 | Dec 8 | Extract services from FirebaseTournamentRepository | 400+ lines extracted |
| #359 | Dec 8 | RoundDisplayService to RoundManagementViewModel | 150+ lines extracted |
| #369 | Dec 9 | Remove duplicate code from LiveScoringViewModel | 200+ lines removed |
| #370 | Dec 9 | Extract TournamentRoundCreationService | 180+ lines extracted |
| #371 | Dec 14 | Extract ScoringViewModelDelegate | 300+ 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
| Category | Examples | Location |
|---|---|---|
| Domain Services | RoundDisplayService, StatisticsService | shared/domain/services/ |
| Repository Services | TournamentParticipationService | app/domain/services/ |
| ViewModel Delegates | ScoringViewModelDelegate | app/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
| Component | Before | After |
|---|---|---|
| RoundDisplayService | N/A | 100% |
| TournamentRoundCreationService | N/A | 95% |
| ScoringViewModelDelegate | N/A | 90% |
| CreatorAuthorizationService | N/A | 100% |
| TournamentParticipationService | N/A | 92% |
| LiveScoringViewModel | 45% | 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
| ViewModel | Before | After | Reduction |
|---|---|---|---|
| LiveScoringViewModel | 2,016 | ~1,400 | 31% |
| TournamentDetailsViewModel | 1,200 | ~850 | 29% |
| RoundManagementViewModel | 800 | ~600 | 25% |
Complexity Reduction
| Metric | Before | After |
|---|---|---|
| Avg. methods per ViewModel | 30+ | 15-20 |
| Max cyclomatic complexity | 25 | 12 |
| Avg. constructor dependencies | 8 | 4-5 |
Future Extraction Plans
HybridTournamentRepository Split (Planned)
The HybridTournamentRepository (1,878 lines) is planned for extraction into focused repositories:
| Repository | Lines | Priority |
|---|---|---|
| TournamentSettingsRepository | 80 | Week 22 |
| TournamentDiscoveryRepository | 280 | Week 22 |
| TournamentSyncRepository | 250 | Week 23 |
| TournamentRoundsRepository | 150 | Week 24 |
| TournamentModerationRepository | 100 | Week 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
*Servicefor domain logic (e.g.,RoundDisplayService)*Delegatefor ViewModel helpers (e.g.,ScoringViewModelDelegate)*Repositoryfor 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:
- Write tests for existing behavior
- Extract to service
- Verify tests still pass
- Add service-specific edge case tests
Related Documentation
- live-scoring-vm-analysis - Original analysis of LiveScoringViewModel
- technical-debt - Technical debt tracking
- data-validation-guard-rails - Validation system
- round-display-service - RoundDisplayService API reference