374 lines
12 KiB
Markdown
374 lines
12 KiB
Markdown
|
|
# 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
|
|||
|
|
```
|