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:
- Single Responsibility: Each service does one thing well
- Testability: Services can be unit tested in isolation
- Reusability: Services can be shared across ViewModels
- Maintainability: Smaller, focused classes are easier to understand and modify
Extracted Services
From FirebaseTournamentRepository (PR #358)
The FirebaseTournamentRepository was split into 6 focused services:
| Service | Responsibility | LOC |
|---|---|---|
FirebaseTournamentLifecycleService | Create, delete, archive tournaments | ~500 |
FirebaseTournamentQueryService | Search and discovery operations | ~180 |
FirebaseParticipantService | Join, leave, participant management | ~340 |
FirebaseRoundService | Round CRUD within tournaments | ~275 |
FirebaseScoringService | Score submission and synchronization | ~470 |
FirebaseSecurityService | Permission 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): RoundTotalsResource 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,
...
): RoundScoringViewModelDelegate (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 Category | Tests Added |
|---|---|
| Tournament creation | 8 |
| Participant management | 12 |
| Round operations | 6 |
| Score synchronization | 10 |
| Security checks | 8 |
| Total | 44 |
These tests were then used to validate the service extraction didn’t break functionality.
Benefits Achieved
Code Quality Metrics
| Metric | Before | After | Improvement |
|---|---|---|---|
| FirebaseTournamentRepository LOC | 2,400 | 600 | -75% |
| TournamentDetailsViewModel LOC | 1,800 | 500 | -72% |
| Average method count per class | 45 | 12 | -73% |
| Test coverage | 35% | 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
Related PRs
| PR | Description |
|---|---|
| #357 | test(firebase): Comprehensive FirebaseTournamentRepository test coverage |
| #358 | refactor: Extract services from FirebaseTournamentRepository |
| #359 | refactor: Add RoundDisplayService to RoundManagementViewModel |
| #369 | refactor: Remove duplicate code from LiveScoringViewModel |
| #370 | refactor: Extract TournamentRoundCreationService |
| #371 | refactor: Extract ScoringViewModelDelegate from ActiveScoringScreen |
Related Documentation
Last Updated: 2025-12-21