# 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 ```