refactor: Split monolithic handler into focused files

Split internal/handlers/cv.go (1,001 lines) into 5 focused files:

Structure:
- cv.go (29 lines) - CVHandler struct + constructor
- cv_pages.go (290 lines) - Page handlers (Home, CVContent, DefaultCVShortcut)
- cv_pdf.go (153 lines) - PDF export handler (ExportPDF)
- cv_htmx.go (218 lines) - HTMX toggle handlers (Length, Icons, Language, Theme)
- cv_helpers.go (385 lines) - Helper functions (skills, dates, git, templates, cookies)

Benefits:
- Single Responsibility: Each file has one clear purpose
- Improved Discoverability: Easy to find specific functionality
- Reduced Cognitive Load: 200-400 lines per file vs 1,001
- Parallel Development: No conflicts when editing different concerns
- Better Organization: Clear section markers and grouping
- Maintainability: Trade +74 lines (+7.4%) for better organization

Testing:
- All Go tests pass (fileutil, handlers, lang, cv, ui)
- Server builds and runs correctly
- All HTTP endpoints functional
- No breaking changes

Documentation:
- Create _go-learning/refactorings/003-handler-split.md
- Document architecture, benefits, and trade-offs
- Explain WHY single package vs separate packages
This commit is contained in:
juanatsap
2025-11-20 17:01:50 +00:00
parent 29a00f432b
commit 4acde64c01
6 changed files with 1424 additions and 977 deletions
@@ -0,0 +1,373 @@
# Refactoring #3: Handler Split - From Monolith to Focused Files
**Date**: 2024-11-20
**Type**: Code Organization, Maintainability
## Problem Statement
After implementing shared utilities and validation (Refactoring #2), the handler file remained problematic:
- **Single Monolithic File**: `internal/handlers/cv.go` was 1,001 lines
- **Mixed Concerns**: Page rendering, PDF export, HTMX toggles, and helpers all in one file
- **Difficult Navigation**: Finding specific functionality required scrolling through hundreds of lines
- **Poor Separation**: No clear boundaries between different types of handlers
## Solution
Split the monolithic handler into focused files by responsibility:
1. **cv.go** (29 lines) - CVHandler struct + constructor only
2. **cv_pages.go** (290 lines) - Page rendering handlers
3. **cv_pdf.go** (153 lines) - PDF export handler
4. **cv_htmx.go** (218 lines) - HTMX toggle handlers
5. **cv_helpers.go** (385 lines) - Helper functions
## Architecture
### Before (Monolithic)
```
internal/handlers/cv.go (1,001 lines)
├── CVHandler struct
├── NewCVHandler()
├── Home() (page handler)
├── CVContent() (page handler)
├── DefaultCVShortcut() (page handler)
├── ExportPDF() (PDF handler)
├── ToggleLength() (HTMX handler)
├── ToggleIcons() (HTMX handler)
├── SwitchLanguage() (HTMX handler)
├── ToggleTheme() (HTMX handler)
├── splitSkills() (helper)
├── calculateYearsOfExperience() (helper)
├── calculateDuration() (helper)
├── processProjectDates() (helper)
├── findProjectRoot() (helper)
├── validateRepoPath() (helper)
├── getGitRepoFirstCommitDate() (helper)
├── prepareTemplateData() (helper)
├── getPreferenceCookie() (helper)
└── setPreferenceCookie() (helper)
```
### After (Focused Files)
```
internal/handlers/
├── cv.go (29 lines)
│ ├── CVHandler struct
│ └── NewCVHandler()
├── cv_pages.go (290 lines)
│ ├── Home() - Full CV page
│ ├── CVContent() - HTMX content swap
│ └── DefaultCVShortcut() - Shortcut PDF URLs
├── cv_pdf.go (153 lines)
│ └── ExportPDF() - PDF generation with options
├── cv_htmx.go (218 lines)
│ ├── ToggleLength() - Short/long toggle
│ ├── ToggleIcons() - Show/hide icons
│ ├── SwitchLanguage() - EN/ES switching
│ └── ToggleTheme() - Default/clean theme
└── cv_helpers.go (385 lines)
├── Skills helpers:
│ └── splitSkills()
├── Date/Duration helpers:
│ ├── calculateYearsOfExperience()
│ ├── calculateDuration()
│ └── processProjectDates()
├── Git helpers:
│ ├── findProjectRoot()
│ ├── validateRepoPath()
│ └── getGitRepoFirstCommitDate()
├── Template helpers:
│ └── prepareTemplateData()
└── Cookie helpers:
├── getPreferenceCookie()
└── setPreferenceCookie()
```
## Benefits
### 1. Single Responsibility Principle (SRP)
Each file now has ONE clear purpose:
**cv.go** - Defines the handler structure
```go
// CVHandler handles CV-related requests
// Methods are split across multiple files for better organization:
// - cv_pages.go: Page rendering (Home, CVContent, DefaultCVShortcut)
// - cv_pdf.go: PDF export (ExportPDF)
// - cv_htmx.go: HTMX toggles (ToggleLength, ToggleIcons, SwitchLanguage, ToggleTheme)
// - cv_helpers.go: Helper functions (skills, dates, git, templates, cookies)
type CVHandler struct {
templates *templates.Manager
pdfGenerator *pdf.Generator
serverAddr string
}
```
### 2. Improved Discoverability
**Easy to find functionality:**
- Need to modify page rendering? → `cv_pages.go`
- PDF generation issue? → `cv_pdf.go`
- HTMX toggle not working? → `cv_htmx.go`
- Helper function bug? → `cv_helpers.go`
### 3. Reduced Cognitive Load
**Before**: Navigate 1,001 lines to understand one feature
**After**: Open the relevant ~150-400 line file
### 4. Better Code Organization
**cv_helpers.go** groups helpers by category with clear section markers:
```go
// ==============================================================================
// SKILLS HELPERS
// ==============================================================================
// splitSkills splits skill categories between left and right sidebars
func splitSkills(skills []cvmodel.SkillCategory) (left, right []cvmodel.SkillCategory) {
// ...
}
// ==============================================================================
// DATE/DURATION HELPERS
// ==============================================================================
// calculateYearsOfExperience calculates years since April 1, 2005
func calculateYearsOfExperience() int {
// ...
}
```
### 5. Parallel Development
Multiple developers can now work on different handler concerns without conflicts:
- Developer A: Adds new HTMX toggle → edits `cv_htmx.go`
- Developer B: Modifies PDF export → edits `cv_pdf.go`
- Developer C: Adds page handler → edits `cv_pages.go`
No merge conflicts!
### 6. Testability
Each file can have focused tests:
- `cv_pages_test.go` - Page rendering tests
- `cv_pdf_test.go` - PDF generation tests
- `cv_htmx_test.go` - HTMX toggle tests
- `cv_helpers_test.go` - Helper function tests
### 7. Documentation Clarity
Each file's purpose is immediately clear from its name and can have targeted documentation.
## Implementation Details
### Why These Groupings?
**cv_pages.go** - All handlers that render full pages or page sections
- `Home()` - Complete HTML page
- `CVContent()` - HTMX content swap
- `DefaultCVShortcut()` - Special PDF shortcut URLs
**cv_pdf.go** - PDF generation is complex enough to warrant its own file
- Handles multiple query parameters (lang, length, icons, version)
- Manages PDF generation with chromedp
- Complex filename generation logic
**cv_htmx.go** - All HTMX interactivity handlers
- Similar patterns (toggle states, cookies, out-of-band swaps)
- All follow same structure: read state → toggle → save → render
**cv_helpers.go** - All supporting functions
- Organized by category with section markers
- Pure functions (no HTTP request/response handling)
- Reusable across handlers
### Go Package Benefits
All files are in the same package (`package handlers`), so:
- ✅ Methods can be split across files (Go allows this!)
- ✅ Helper functions accessible without imports
- ✅ No circular dependency issues
- ✅ Same namespace, better organization
## Code Metrics
### File Sizes
| File | Lines | Purpose | Complexity |
|------|-------|---------|------------|
| cv.go | 29 | Struct + constructor | Very Low |
| cv_pages.go | 290 | Page rendering | Medium |
| cv_pdf.go | 153 | PDF export | Medium |
| cv_htmx.go | 218 | HTMX toggles | Low |
| cv_helpers.go | 385 | Helper functions | Low-Medium |
| **Total** | **1,075** | | **Average** |
### Reduction Achievement
- **Original**: 1 file × 1,001 lines = **1,001 lines**
- **New**: 5 files × 215 lines avg = **1,075 lines**
- **Net Change**: +74 lines (+7.4%)
The slight increase is due to:
- Comments documenting each file's purpose
- Section markers in cv_helpers.go for better organization
- More descriptive comments at file level
**Trade-off**: +74 lines for dramatically improved maintainability and organization.
### Maintainability Index
**Before**:
- 1,001 lines to search through
- 19 functions mixed together
- No clear organization
**After**:
- 29-385 lines per file
- 3-9 functions per file (focused)
- Clear organization by responsibility
## Testing
### All Tests Pass
```bash
$ go test ./...
ok github.com/juanatsap/cv-site/internal/fileutil 0.432s
ok github.com/juanatsap/cv-site/internal/handlers 0.789s
ok github.com/juanatsap/cv-site/internal/lang 0.326s
ok github.com/juanatsap/cv-site/internal/models/cv 0.463s
ok github.com/juanatsap/cv-site/internal/models/ui 0.315s
```
### Verification
1. **Build**: ✅ `go build` succeeds
2. **Tests**: ✅ All unit tests pass
3. **Server**: ✅ Server starts and renders pages
4. **Endpoints**: ✅ All HTTP endpoints functional
## Why This Approach?
### Alternative Considered: Separate Packages
Could we split into separate packages?
```
internal/
├── handlers/pages/
├── handlers/pdf/
├── handlers/htmx/
└── handlers/helpers/
```
**Why NOT:**
- Creates circular dependencies (pages need helpers, helpers need CVHandler)
- More complex imports
- Breaks Go's "methods on types" pattern (can't split CVHandler methods across packages)
**Why Single Package:**
- ✅ Methods can be defined in any file
- ✅ Helpers accessible without imports
- ✅ Single namespace, no confusion
- ✅ Go's design encourages this pattern
### Go Best Practices
This approach follows **Go best practices**:
1. **Package organization by feature, not by layer**
- All CV handler code stays in `handlers` package
- Files split by sub-feature (pages, PDF, HTMX, helpers)
2. **Methods split across files**
- Go allows defining methods on a type in any file in the same package
- CVHandler methods spread across multiple files naturally
3. **Clear file naming**
- Prefix indicates grouping: `cv_pages.go`, `cv_pdf.go`, `cv_htmx.go`
- Easy to find related functionality
## Interview Talking Points
### 1. Code Organization
"I refactored a 1,001-line monolithic handler into 5 focused files (29-385 lines each), improving discoverability and maintainability while following Go's single-package-multiple-files pattern."
### 2. Single Responsibility Principle
"Each file now has one clear purpose: cv_pages handles page rendering, cv_pdf manages PDF export, cv_htmx handles interactivity, and cv_helpers provides reusable functions."
### 3. Maintainability Over Brevity
"I accepted a 7.4% line increase to gain dramatically improved organization. The trade-off of 74 extra lines for better maintainability was worth it."
### 4. Go Package Patterns
"I kept all files in one package to avoid circular dependencies and leverage Go's ability to split methods across files, rather than forcing artificial package boundaries."
### 5. Parallel Development
"The split enables multiple developers to work on different handler concerns without conflicts, improving team velocity."
### 6. Progressive Refactoring
"This is refactoring #3 in a series: #1 separated domain models, #2 added shared utilities and validation, #3 organized handlers. Each step builds on the previous, improving the codebase incrementally."
## Future Improvements
1. **Extract Duplicate Logic**: `Home()` and `CVContent()` have similar data preparation - could use `prepareTemplateData()`
2. **Handler Tests**: Add focused tests for each handler file
3. **Middleware Extraction**: Cookie handling could become middleware
4. **Request/Response Types**: Define structs for common request/response patterns
5. **Error Handling**: Centralize error response formatting
## Related Documentation
- [Refactoring #1: CV/UI Model Separation](./001-cv-model-separation.md)
- [Refactoring #2: Shared Utilities & Validation](./002-shared-utilities-validation.md)
- [Server Design: Why Goroutines?](../architecture/server-design.md)
## Commit Message
```
refactor: Split monolithic handler into focused files
Split internal/handlers/cv.go (1,001 lines) into 5 focused files:
Structure:
- cv.go (29 lines) - CVHandler struct + constructor
- cv_pages.go (290 lines) - Page handlers (Home, CVContent, DefaultCVShortcut)
- cv_pdf.go (153 lines) - PDF export handler (ExportPDF)
- cv_htmx.go (218 lines) - HTMX toggle handlers (Length, Icons, Language, Theme)
- cv_helpers.go (385 lines) - Helper functions (skills, dates, git, templates, cookies)
Benefits:
- Single Responsibility: Each file has one clear purpose
- Improved Discoverability: Easy to find specific functionality
- Reduced Cognitive Load: 200-400 lines per file vs 1,001
- Parallel Development: No conflicts when editing different concerns
- Better Organization: Clear section markers and grouping
- Maintainability: Trade +74 lines (+7.4%) for better organization
Testing:
- All Go tests pass (fileutil, handlers, lang, cv, ui)
- Server builds and runs correctly
- All HTTP endpoints functional
- No breaking changes
Documentation:
- Create _go-learning/refactorings/003-handler-split.md
- Document architecture, benefits, and trade-offs
- Explain WHY single package vs separate packages
```