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:
@@ -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
|
||||
```
|
||||
Reference in New Issue
Block a user