Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:02:42 +08:00
commit 34a2423d78
33 changed files with 12105 additions and 0 deletions

759
skills/refactoring/SKILL.md Normal file
View File

@@ -0,0 +1,759 @@
---
name: refactoring
description: Linter-driven refactoring patterns to reduce complexity and improve code quality. Use when linter fails with complexity issues (cyclomatic, cognitive, maintainability) or when code feels hard to read/maintain. Applies storifying, type extraction, and function extraction patterns.
---
# Refactoring
Linter-driven refactoring patterns to reduce complexity and improve code quality.
## When to Use
- **Automatically invoked** by @linter-driven-development when linter fails
- **Automatically invoked** by @pre-commit-review when design issues detected
- **Complexity failures**: cyclomatic, cognitive, maintainability index
- **Architectural failures**: noglobals, gochecknoinits, gochecknoglobals
- **Design smell failures**: dupl (duplication), goconst (magic strings), ineffassign
- Functions > 50 LOC or nesting > 2 levels
- Mixed abstraction levels in functions
- Manual invocation when code feels hard to read/maintain
**IMPORTANT**: This skill operates autonomously - no user confirmation needed during execution
## Learning Resources
Choose your learning path:
- **Quick Start**: Use the patterns below for common refactoring cases
- **Complete Reference**: See [reference.md](./reference.md) for full decision tree and all patterns
- **Real-World Examples**: See [examples.md](./examples.md) to learn the refactoring thought process
- [Example 1](./examples.md#example-1-storifying-mixed-abstractions-and-extracting-logic-into-leaf-types): Storifying and extracting a single leaf type
- [Example 2](./examples.md#example-2-primitive-obsession-with-multiple-types-and-storifying-switch-statements): Primitive obsession with multiple types and switch elimination
## Analysis Phase (Automatic)
Before applying any refactoring patterns, the skill automatically analyzes the context:
### System Context Analysis
```
AUTOMATICALLY ANALYZE:
1. Find all callers of the failing function
2. Identify which flows/features depend on it
3. Determine primary responsibility
4. Check for similar functions revealing patterns
5. Spot potential refactoring opportunities
```
### Type Discovery
Proactively identify hidden types in the code:
```
POTENTIAL TYPES TO DISCOVER:
1. Data being parsed from strings → Parse* types
Example: ParseCommandResult(), ParseLogEntry()
2. Scattered validation logic → Validated types
Example: Email, Port, IPAddress types
3. Data that always travels together → Aggregate types
Example: UserCredentials, ServerConfig
4. Complex conditions → State/status types
Example: DeploymentStatus with IsReady(), CanProceed()
5. Repeated string manipulation → Types with methods
Example: FilePath with Dir(), Base(), Ext()
```
### Analysis Output
The analysis produces a refactoring plan identifying:
- Function's role in the system
- Potential domain types to extract
- Recommended refactoring approach
- Expected complexity reduction
## Refactoring Signals
### Linter Failures
**Complexity Issues:**
- **Cyclomatic Complexity**: Too many decision points → Extract functions, simplify logic
- **Cognitive Complexity**: Hard to understand → Storifying, reduce nesting
- **Maintainability Index**: Hard to maintain → Break into smaller pieces
**Architectural Issues:**
- **noglobals/gochecknoglobals**: Global variable usage → Dependency rejection pattern
- **gochecknoinits**: Init function usage → Extract initialization logic
- **Static/singleton patterns**: Hidden dependencies → Inject dependencies
**Design Smells:**
- **dupl**: Code duplication → Extract common logic/types
- **goconst**: Magic strings/numbers → Extract constants or types
- **ineffassign**: Ineffective assignments → Simplify logic
### Code Smells
- Functions > 50 LOC
- Nesting > 2 levels
- Mixed abstraction levels
- Unclear flow/purpose
- Primitive obsession
- Global variable access scattered throughout code
## Workflow (Automatic)
### 1. Receive Linter Failures
Automatically receive failures from @linter-driven-development:
```
user/service.go:45:1: cyclomatic complexity 15 of func `CreateUser` is high (> 10)
user/handler.go:23:1: cognitive complexity 25 of func `HandleRequest` is high (> 15)
```
### 2. Automatic Root Cause Analysis
The skill automatically diagnoses each failure:
- Does this code read like a story? → Apply storifying
- Can this be broken into smaller pieces? → Extract functions/types
- Does logic run on primitives? → Check for primitive obsession
- Is function long due to switch statement? → Extract case handlers
### 3. Automatic Pattern Application
Applies patterns in priority order without user intervention:
- **Early Returns**: Try first (least invasive)
- **Extract Function**: Break up complexity
- **Storifying**: Improve abstraction levels
- **Extract Type**: Create domain types (if juicy)
- **Switch Extraction**: Categorize cases
### 4. Automatic Verification Loop
- Re-run linter automatically
- If still failing, try next pattern
- Continue until linter passes
- Report final results
## Automation Flow
This skill operates completely autonomously once invoked:
### Automatic Iteration Loop
```
AUTOMATED PROCESS:
1. Receive trigger:
- From @linter-driven-development (linter failures)
- From @pre-commit-review (design debt/readability debt)
2. Apply refactoring pattern (start with least invasive)
3. Run linter immediately (no user confirmation)
4. If linter still fails OR review finds more issues:
- Try next pattern in priority order
- Repeat until both linter and review pass
5. If patterns exhausted and still failing:
- Report what was tried
- Suggest file splitting or architectural changes
```
### Pattern Priority Order
Apply patterns based on failure type:
**For Complexity Failures** (cyclomatic, cognitive, maintainability):
```
1. Early Returns → Reduce nesting quickly
2. Extract Function → Break up long functions
3. Storifying → Improve abstraction levels
4. Extract Type → Create domain types (only if "juicy")
5. Switch Extraction → Categorize switch cases
```
**For Architectural Failures** (noglobals, singletons):
```
1. Dependency Rejection → Incremental bottom-up approach
2. Extract Type with dependency injection
3. Push global access up call chain one level
4. Iterate until globals only at entry points (main, handlers)
```
**For Design Smells** (dupl, goconst):
```
1. Extract Type → For repeated values or validation
2. Extract Function → For code duplication
3. Extract Constant → For magic strings/numbers
```
### No Manual Intervention
- **NO** asking for confirmation between patterns
- **NO** waiting for user input
- **NO** manual linter runs
- **AUTOMATIC** progression through patterns
- **ONLY** report results at the end
## Refactoring Patterns
### Pattern 1: Storifying (Mixed Abstractions)
**Signal**: Function mixes high-level steps with low-level details
```go
// ❌ Before - Mixed abstractions
func ProcessOrder(order Order) error {
// Validation
if order.ID == "" {
return errors.New("invalid")
}
// Low-level DB setup
db, err := sql.Open("postgres", connStr)
if err != nil { return err }
defer db.Close()
// SQL construction
query := "INSERT INTO..."
// ... many lines
return nil
}
// ✅ After - Story-like
func ProcessOrder(order Order) error {
if err := validateOrder(order); err != nil {
return err
}
if err := saveToDatabase(order); err != nil {
return err
}
return notifyCustomer(order)
}
func validateOrder(order Order) error { /* ... */ }
func saveToDatabase(order Order) error { /* ... */ }
func notifyCustomer(order Order) error { /* ... */ }
```
### Pattern 2: Extract Type (Primitive Obsession)
**Signal**: Complex logic operating on primitives OR unstructured data needing organization
#### Juiciness Test v2 - When to Create Types
**BEHAVIORAL JUICINESS** (rich behavior):
- ✅ Complex validation rules (regex, ranges, business rules)
- ✅ Multiple meaningful methods (≥2 methods)
- ✅ State transitions or transformations
- ✅ Format conversions (different representations)
**STRUCTURAL JUICINESS** (organizing complexity):
- ✅ Parsing unstructured data into fields
- ✅ Grouping related data that travels together
- ✅ Making implicit structure explicit
- ✅ Replacing map[string]interface{} with typed fields
**USAGE JUICINESS** (simplifies code):
- ✅ Used in multiple places
- ✅ Significantly simplifies calling code
- ✅ Makes tests cleaner and more focused
**Score**: Need "yes" in at least ONE category to create the type
#### Examples of Juicy vs Non-Juicy Types
```go
// ❌ NOT JUICY - Don't create type
func ValidateUserID(id string) error {
if id == "" {
return errors.New("empty id")
}
return nil
}
// Just use: if userID == ""
// ✅ JUICY (Behavioral) - Complex validation
type Email string
func ParseEmail(s string) (Email, error) {
if s == "" {
return "", errors.New("empty email")
}
if !emailRegex.MatchString(s) {
return "", errors.New("invalid format")
}
if len(s) > 255 {
return "", errors.New("too long")
}
return Email(s), nil
}
func (e Email) Domain() string { /* extract domain */ }
func (e Email) LocalPart() string { /* extract local */ }
func (e Email) String() string { return string(e) }
// ✅ JUICY (Structural) - Parsing complex data
type CommandResult struct {
FailedFiles []string
SuccessFiles []string
Message string
ExitCode int
Warnings []string
}
func ParseCommandResult(output string) (CommandResult, error) {
// Parse unstructured output into structured fields
// Making implicit structure explicit
}
// ✅ JUICY (Mixed) - Both behavior and structure
type ServiceEndpoint struct {
host string
port Port
}
func ParseEndpoint(s string) (ServiceEndpoint, error) {
// Parse "host:port/path" format
}
func (e ServiceEndpoint) URL() string { }
func (e ServiceEndpoint) IsSecure() bool { }
func (e ServiceEndpoint) WithPath(path string) string { }
```
**⚠️ Warning Signs of Over-Engineering:**
- Type with only one trivial method
- Simple validation (just empty check)
- Type that's just a wrapper without behavior
- Good variable naming would be clearer
**→ See [Example 2](./examples.md#first-refactoring-attempt-the-over-abstraction-trap)** for complete case study.
### Pattern 3: Extract Function (Long Functions)
**Signal**: Function > 50 LOC or multiple responsibilities
```go
// ❌ Before - Long function
func CreateUser(data map[string]interface{}) error {
// Validation (15 lines)
// ...
// Database operations (20 lines)
// ...
// Email notification (10 lines)
// ...
// Logging (5 lines)
// ...
return nil
}
// ✅ After - Extracted functions
func CreateUser(data map[string]interface{}) error {
user, err := validateAndParseUser(data)
if err != nil {
return err
}
if err := saveUser(user); err != nil {
return err
}
if err := sendWelcomeEmail(user); err != nil {
return err
}
logUserCreation(user)
return nil
}
```
### Pattern 4: Early Returns (Deep Nesting)
**Signal**: Nesting > 2 levels
```go
// ❌ Before - Deeply nested
func ProcessItem(item Item) error {
if item.IsValid() {
if item.IsReady() {
if item.HasPermission() {
// Process
return nil
} else {
return errors.New("no permission")
}
} else {
return errors.New("not ready")
}
} else {
return errors.New("invalid")
}
}
// ✅ After - Early returns
func ProcessItem(item Item) error {
if !item.IsValid() {
return errors.New("invalid")
}
if !item.IsReady() {
return errors.New("not ready")
}
if !item.HasPermission() {
return errors.New("no permission")
}
// Process
return nil
}
```
### Pattern 5: Switch Extraction (Long Switch)
**Signal**: Switch statement with complex cases
```go
// ❌ Before - Long switch in one function
func HandleRequest(reqType string, data interface{}) error {
switch reqType {
case "create":
// 20 lines of creation logic
case "update":
// 20 lines of update logic
case "delete":
// 15 lines of delete logic
default:
return errors.New("unknown type")
}
return nil
}
// ✅ After - Extracted handlers
func HandleRequest(reqType string, data interface{}) error {
switch reqType {
case "create":
return handleCreate(data)
case "update":
return handleUpdate(data)
case "delete":
return handleDelete(data)
default:
return errors.New("unknown type")
}
}
func handleCreate(data interface{}) error { /* ... */ }
func handleUpdate(data interface{}) error { /* ... */ }
func handleDelete(data interface{}) error { /* ... */ }
```
### Pattern 6: Dependency Rejection (Architectural Refactoring)
**Signal**: noglobals linter fails OR global/singleton usage detected
**Goal**: Create "islands of clean code" by incrementally pushing dependencies up the call chain
**Strategy**: Work from bottom-up, rejecting dependencies one level at a time
- DON'T do massive refactoring all at once
- Start at deepest level (furthest from main)
- Extract clean type with dependency injected
- Push global access up one level
- Repeat until global only at entry points
**Quick Example**:
```go
// ❌ Before - Global accessed deep in code
func PublishEvent(event Event) error {
conn, err := nats.Connect(env.Configs.NATsAddress)
// ... complex logic
}
// ✅ After - Dependency rejected up one level
type EventPublisher struct {
natsAddress string // injected, not global
}
func NewEventPublisher(natsAddress string) *EventPublisher {
return &EventPublisher{natsAddress: natsAddress}
}
func (p *EventPublisher) Publish(event Event) error {
conn, err := nats.Connect(p.natsAddress)
// ... same logic, now testable
}
// Caller pushed up (closer to main)
func SetupMessaging() *EventPublisher {
return NewEventPublisher(env.Configs.NATsAddress) // Global only here
}
```
**Result**: EventPublisher is now 100% testable without globals
**Key Principles**:
- **Incremental**: One type at a time, one level at a time
- **Bottom-up**: Start at deepest code, work toward main
- **Pragmatic**: Accept globals at entry points (main, handlers)
- **Testability**: Each extracted type is an island (testable in isolation)
**→ See [Example 3](./examples.md#example-3-dependency-rejection-pattern) for complete case study with config access patterns**
## Refactoring Decision Tree
When linter fails, ask these questions (see reference.md for details):
1. **Does this read like a story?**
- No → Extract functions for different abstraction levels
2. **Can this be broken into smaller pieces?**
- By responsibility? → Extract functions
- By task? → Extract functions
- By category? → Extract functions
3. **Does logic run on primitives?**
- Yes → Is this primitive obsession? → Extract type
4. **Is function long due to switch statement?**
- Yes → Extract case handlers
5. **Are there deeply nested if/else?**
- Yes → Use early returns or extract functions
## Testing Integration
### Automatic Test Creation
When creating new types or extracting functions during refactoring:
**ALWAYS invoke @testing skill** to write tests for:
- **Isolated types**: Types with injected dependencies (testable islands)
- **Value object types**: Types that may depend on other value objects but are still isolated
- **Extracted functions**: New functions created during refactoring
- **Parse functions**: Functions that transform unstructured data
### Island of Clean Code Definition
A type is an "island of clean code" if:
- ✅ Dependencies are explicit (injected via constructor)
- ✅ No global or static dependencies
- ✅ Can be tested in isolation
- ✅ Has 100% testable public API
**Examples of testable islands:**
- `NATSClient` with injected `natsAddress` string (no other dependencies)
- `Email` type with validation logic (no dependencies)
- `ServiceEndpoint` that uses `Port` value object (both are testable islands)
- `OrderService` with injected `Repository` and `EventPublisher` (all testable)
**Note**: Islands can depend on other value objects and still be isolated!
### Workflow
```
REFACTORING → TESTING:
1. Extract type during refactoring
2. Immediately invoke @testing skill
3. @testing skill writes appropriate tests:
- Table-driven tests for simple scenarios
- Testify suites for complex infrastructure
- Integration tests for orchestrating types
4. Verify tests pass
5. Continue refactoring
```
### Testing Delegation
- **Refactoring skill**: Makes code testable (creates islands)
- **@testing skill**: Writes all tests (structure, patterns, coverage)
**→ See @testing skill for test structure, patterns, and guidelines**
## Stopping Criteria
### When to Stop Refactoring
**STOP when ALL of these are met:**
```
✅ Linter passes
✅ All functions < 50 LOC
✅ Nesting ≤ 2 levels
✅ Code reads like a story
✅ No more "juicy" abstractions to extract
```
### Don't Over-Refactor
**Warning Signs of Over-Engineering:**
- Creating types with only one method
- Functions that just call another function
- More abstraction layers than necessary
- Code becomes harder to understand
- Diminishing returns on complexity reduction
**Pragmatic Approach:**
```
IF linter passes AND code is readable:
STOP - Don't extract more
EVEN IF you could theoretically extract more:
STOP - Avoid abstraction bloat
```
### Example Stopping Decision
```
Current State:
- Function: 45 LOC (was 120) ✅
- Complexity: 8 (was 25) ✅
- Nesting: 2 levels (was 4) ✅
- Created 2 juicy types (Email, PhoneNumber) ✅
Could extract UserID type but:
- Only validation is "not empty" ❌
- No other methods needed ❌
- Good naming is sufficient ❌
Decision: STOP HERE - Goals achieved, avoid bloat
```
## After Refactoring
### Verify
- [ ] Re-run `task lintwithfix` - Should pass
- [ ] Run tests - Should still pass
- [ ] Check coverage - Should maintain or improve
- [ ] Code more readable? - Get feedback if unsure
### May Need
- **New types created** → Use @code-designing to validate design
- **New functions added** → Ensure tests cover them
- **Major restructuring** → Consider using @pre-commit-review
## Output Format
```
🔍 CONTEXT ANALYSIS
Function: CreateUser (user/service.go:45)
Role: Core user creation orchestration
Called by:
- api/handler.go:89 (HTTP endpoint)
- cmd/user.go:34 (CLI command)
- test/fixtures.go:123 (test fixtures)
Potential types spotted:
- Email: Complex validation logic scattered
- UserID: Generation and validation logic
- UserCreationRequest: Multiple related fields
🔧 REFACTORING APPLIED
✅ Patterns Successfully Applied:
1. Early Returns: Reduced nesting from 4 to 2 levels
2. Storifying: Extracted validate(), save(), notify()
3. Extract Type: Created Email and PhoneNumber types
❌ Patterns Tried but Insufficient:
- Extract Function alone: Still too complex, needed types
🎯 Types Created (with Juiciness Score):
✅ Email type (JUICY - Behavioral + Usage):
- Behavioral: ParseEmail(), Domain(), LocalPart() methods
- Usage: Used in 5+ places across codebase
- Island: Testable in isolation
- → Invoke @testing skill to write tests
✅ PhoneNumber type (JUICY - Behavioral):
- Behavioral: Parse(), Format(), CountryCode() methods
- Validation: Complex international format rules
- Island: Testable in isolation
- → Invoke @testing skill to write tests
❌ Types Considered but Rejected (NOT JUICY):
- UserID: Only empty check, good naming sufficient
- Status: Just string constants, enum adequate
🏗️ ARCHITECTURAL REFACTORING (if applicable)
Trigger: noglobals linter failure
Global Dependencies Identified:
- env.Configs.NATsAddress: Used in 12 places
- env.Configs.DBHost: Used in 8 places
Dependency Rejection Applied:
✅ Level 1 (Bottom): Created NATSClient with injected address
✅ Level 2 (Middle): Created OrderService using clean types
⬆️ Pushed env.Configs to: main() and HTTP handlers (2 locations)
Islands of Clean Code Created:
- messaging/nats_client.go: Ready for testing (isolated, injected deps)
- order/service.go: Ready for testing (isolated, injected deps)
→ Invoke @testing skill to write tests for these islands
Progress:
- Before: 20 global accesses scattered throughout
- After: 2 global accesses (entry points only)
- Islands created: 2 new testable types
📊 METRICS
Complexity Reduction:
- Cyclomatic: 18 → 7 ✅
- Cognitive: 25 → 8 ✅
- LOC: 120 → 45 ✅
- Nesting: 4 → 2 ✅
📝 FILES MODIFIED
Modified:
- user/service.go (+15, -75 lines)
- user/handler.go (+5, -20 lines)
Created (Islands of Clean Code):
- user/email.go (new, +45 lines) → Ready for @testing skill
- user/phone_number.go (new, +38 lines) → Ready for @testing skill
Next: Invoke @testing skill to write tests for new islands
✅ AUTOMATION COMPLETE
Stopping Criteria Met:
✅ Linter passes (0 issues)
✅ All functions < 50 LOC
✅ Max nesting = 2 levels
✅ Code reads like a story
✅ No more juicy abstractions
Ready for: @pre-commit-review phase
```
## Learning from Examples
For real-world refactoring case studies that show the complete thought process:
**[Example 1: Storifying Mixed Abstractions](./examples.md#example-1-storifying-mixed-abstractions-and-extracting-logic-into-leaf-types)**
- Transforms a 48-line fat function into lean orchestration + isolated type
- Shows how to extract `IPConfig` type for collection and validation logic
- Demonstrates creating testable islands of clean code
**[Example 2: Primitive Obsession with Multiple Types](./examples.md#example-2-primitive-obsession-with-multiple-types-and-storifying-switch-statements)**
- Transforms a 60-line function into a 7-line story by extracting 4 isolated types
- Shows the Type Alias Pattern for config-friendly types
- Demonstrates eliminating switch statement duplication
- Fixed misleading function name (`validateCIDR``alignCIDRArgs`)
**[Example 3: Dependency Rejection Pattern](./examples.md#example-3-dependency-rejection-pattern)**
- Incremental elimination of global config access (`env.Configs.NATsAddress`)
- Shows bottom-up approach: create clean islands one level at a time
- Demonstrates testability benefits of dependency injection
- Pragmatic stopping point: globals only at entry points
See [examples.md](./examples.md) for complete case studies with thought process.
## Integration with Other Skills
### Invoked By (Automatic Triggering)
- **@linter-driven-development**: Automatically invokes when linter fails (Phase 3)
- **@pre-commit-review**: Automatically invokes when design issues detected (Phase 4)
### Iterative Loop
```
1. Linter fails → invoke @refactoring
2. Refactoring complete → re-run linter
3. Linter passes → invoke @pre-commit-review
4. Review finds design debt → invoke @refactoring again
5. Refactoring complete → re-run linter
6. Repeat until both linter AND review pass
```
### Invokes (When Needed)
- **@code-designing**: When refactoring creates new types, validate design
- **@testing**: Automatically invoked to write tests for new types/functions
- **@pre-commit-review**: Validates design quality after linting passes
See [reference.md](./reference.md) for complete refactoring patterns and decision tree.

View File

@@ -0,0 +1,147 @@
# Type Design Subset for Refactoring
Quick reference for type design principles when refactoring.
For complete type design guidance, see @code-designing skill.
## When Refactoring Reveals Need for Types
### Primitive Obsession Signal
During refactoring, if you find:
- Validation repeated across multiple functions
- Complex logic operating on primitives (string, int, float)
- Parameters passed around without type safety
→ Create a self-validating type
### Pattern: Self-Validating Type
```go
type TypeName underlyingType
func NewTypeName(input underlyingType) (TypeName, error) {
// Validate
if /* invalid */ {
return zero, errors.New("why invalid")
}
return TypeName(input), nil
}
// Add methods if behavior needed
func (t TypeName) SomeMethod() result {
// Type-specific logic
}
```
## Type Design Checklist
When creating types during refactoring:
- [ ] **Constructor validates** - Check in New* function
- [ ] **Fields are private** - Prevent invalid state
- [ ] **Methods trust validity** - No nil checks
- [ ] **Type has behavior** - Not just data container
- [ ] **Type in own file** - If it has logic
## Examples
### Example 1: Port Validation
```go
// Before refactoring - Validation scattered
func StartServer(port int) error {
if port <= 0 || port >= 9000 {
return errors.New("invalid port")
}
// ...
}
func ConnectTo(host string, port int) error {
if port <= 0 || port >= 9000 {
return errors.New("invalid port")
}
// ...
}
// After refactoring - Self-validating type
type Port int
func NewPort(p int) (Port, error) {
if p <= 0 || p >= 9000 {
return 0, errors.New("port must be 1-8999")
}
return Port(p), nil
}
func StartServer(port Port) error {
// No validation needed
// ...
}
func ConnectTo(host string, port Port) error {
// No validation needed
// ...
}
```
### Example 2: Parser Complexity
```go
// Before refactoring - One complex Parser
type Parser struct {
// Too many responsibilities
}
func (p *Parser) Parse(input string) (Result, error) {
// 100+ lines parsing headers, path, body, etc.
}
// After refactoring - Separate types by role
type HeaderParser struct { /* ... */ }
type PathParser struct { /* ... */ }
type BodyParser struct { /* ... */ }
func (p *HeaderParser) Parse(input string) (Header, error) {
// Focused logic for headers only
}
func (p *PathParser) Parse(input string) (Path, error) {
// Focused logic for path only
}
func (p *BodyParser) Parse(input string) (Body, error) {
// Focused logic for body only
}
```
## Quick Decision: Create Type or Extract Function?
### Create Type When:
- Logic operates on a primitive
- Validation is repeated
- Type represents domain concept
- Behavior is cohesive
### Extract Function When:
- Logic is procedural (no state needed)
- Different abstraction level
- One-time operation
- No validation required
## Integration with Refactoring
After creating types during refactoring:
1. Run tests - Ensure they pass
2. Run linter - Should reduce complexity
3. Consider @code-designing - Validate type design
4. Update tests - Ensure new types have 100% coverage
## File Organization
When creating types during refactoring:
```
package/
├── original.go # Original file
├── new_type.go # New type in own file (if has logic)
└── original_test.go # Tests
```
---
For complete type design principles, see @code-designing skill.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,745 @@
# Refactoring Patterns Reference
Complete guide for linter-driven refactoring with decision tree and patterns.
## Refactoring Decision Tree
When linter fails or code feels complex, use this decision tree:
### Question 1: Does this code read like a story?
**Check**: Does it mix different levels of abstractions?
```go
// ❌ No - Mixes abstractions
func CreatePizza(order Order) Pizza {
pizza := Pizza{Base: order.Size} // High-level
// Low-level temperature control
for oven.Temp < cookingTemp {
time.Sleep(checkOvenInterval)
oven.Temp = getOvenTemp(oven)
}
return pizza
}
// ✅ Yes - Story-like
func CreatePizza(order Order) Pizza {
pizza := prepare(order)
bake(pizza)
return pizza
}
```
**Action**: Break it down to same level of abstraction. Hide nitty-gritty details behind methods with proper names.
### Question 2: Can this be broken into smaller pieces?
**By what**: Responsibility? Task? Category?
Breaking down can be done at all levels:
- Extract a variable
- Extract a function
- Create a new type
- Create a new package
```go
// ❌ Multiple responsibilities
func HandleUserRequest(w http.ResponseWriter, r *http.Request) {
// Parse request
var user User
json.NewDecoder(r.Body).Decode(&user)
// Validate
if user.Email == "" { /* ... */ }
// Save to DB
db.Exec("INSERT INTO...")
// Send response
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
// ✅ Separated by responsibility
func HandleUserRequest(w http.ResponseWriter, r *http.Request) {
user, err := parseUser(r)
if err != nil {
respondError(w, err)
return
}
if err := validateUser(user); err != nil {
respondError(w, err)
return
}
if err := saveUser(user); err != nil {
respondError(w, err)
return
}
respondSuccess(w)
}
```
### Question 3: Does logic run on a primitive?
**Check**: Is this primitive obsession?
If logic operates on string/int/float, consider creating a type.
```go
// ❌ Primitive obsession
func ValidateEmail(email string) bool {
return strings.Contains(email, "@")
}
func SendEmail(email string, subject, body string) error {
if !ValidateEmail(email) {
return errors.New("invalid email")
}
// Send
}
// ✅ Custom type
type Email string
func NewEmail(s string) (Email, error) {
if !strings.Contains(s, "@") {
return "", errors.New("invalid email")
}
return Email(s), nil
}
func SendEmail(email Email, subject, body string) error {
// No validation needed - type guarantees validity
// Send
}
```
**Note**: Cohesion is more important than coupling. Put logic where it belongs, even if it creates dependencies.
### Question 4: Is function long due to switch statement?
**Check**: Can cases be categorized and extracted?
```go
// ❌ Long switch statement
func ProcessEvent(eventType string, data interface{}) error {
switch eventType {
case "user_created":
// 20 lines
case "user_updated":
// 25 lines
case "user_deleted":
// 15 lines
// ... more cases
}
}
// ✅ Extracted case handlers
func ProcessEvent(eventType string, data interface{}) error {
switch eventType {
case "user_created":
return handleUserCreated(data)
case "user_updated":
return handleUserUpdated(data)
case "user_deleted":
return handleUserDeleted(data)
default:
return errors.New("unknown event type")
}
}
func handleUserCreated(data interface{}) error { /* ... */ }
func handleUserUpdated(data interface{}) error { /* ... */ }
func handleUserDeleted(data interface{}) error { /* ... */ }
```
### Question 5: Types with logic?
**Rule**: Types with logic should be in their own file. Name file after type.
```
user/
├── user.go # User type
├── user_id.go # UserID type with logic
├── email.go # Email type with logic
└── service.go # UserService
```
---
## Detailed Refactoring Patterns
### 1. Storifying (Abstraction Levels)
**Signal:**
- Linter: High cognitive complexity
- Code smell: Mixed high-level and low-level code
**Pattern:**
```go
// Before
func ProcessOrder(order Order) error {
// Validation
if order.ID == "" { return errors.New("invalid") }
if len(order.Items) == 0 { return errors.New("no items") }
for _, item := range order.Items {
if item.Price < 0 { return errors.New("negative price") }
}
// Database
db, err := sql.Open("postgres", os.Getenv("DB_URL"))
if err != nil { return err }
defer db.Close()
tx, err := db.Begin()
if err != nil { return err }
// SQL queries
_, err = tx.Exec("INSERT INTO orders...")
// ... many more lines
// Email
smtp, err := mail.Dial("smtp.example.com:587")
// ... email sending logic
return nil
}
// After
func ProcessOrder(order Order) error {
if err := validateOrder(order); err != nil {
return err
}
if err := saveToDatabase(order); err != nil {
return err
}
if err := notifyCustomer(order); err != nil {
return err
}
return nil
}
```
**Benefits:**
- Clear flow (validate → save → notify)
- Each function single responsibility
- Easy to test
- Easy to modify
**Real-world example:** See [Example 1 in examples.md](./examples.md#example-1-storifying-mixed-abstractions-and-extracting-logic-into-leaf-types) for a production case of storifying mixed abstractions and extracting a leaf type for IP collection logic
### 2. Extract Type (Primitive Obsession)
**Signal:**
- Linter: High cyclomatic complexity (due to validation)
- Code smell: Validation repeated across codebase
**Pattern:**
```go
// Before: Validation scattered
func CreateServer(host string, port int) (*Server, error) {
if host == "" {
return nil, errors.New("host required")
}
if port <= 0 || port > 65535 {
return nil, errors.New("invalid port")
}
// ...
}
func ConnectToServer(host string, port int) error {
if host == "" {
return errors.New("host required")
}
if port <= 0 || port > 65535 {
return errors.New("invalid port")
}
// ...
}
// After: Self-validating types
type Host string
type Port int
func NewHost(s string) (Host, error) {
if s == "" {
return "", errors.New("host required")
}
return Host(s), nil
}
func NewPort(p int) (Port, error) {
if p <= 0 || p > 65535 {
return 0, errors.New("port must be 1-65535")
}
return Port(p), nil
}
type ServerAddress struct {
host Host
port Port
}
func NewServerAddress(host Host, port Port) ServerAddress {
// No validation needed - types are already valid
return ServerAddress{host: host, port: port}
}
func (a ServerAddress) String() string {
return fmt.Sprintf("%s:%d", a.host, a.port)
}
func CreateServer(addr ServerAddress) (*Server, error) {
// No validation needed
// ...
}
func ConnectToServer(addr ServerAddress) error {
// No validation needed
// ...
}
```
**Benefits:**
- Validation centralized
- Type safety
- Reduced complexity
- Self-documenting
**Real-world example:** See [Example 2 in examples.md](./examples.md#example-2-primitive-obsession-with-multiple-types-and-storifying-switch-statements) for extracting multiple types from a 60-line function with primitive obsession. Shows the Type Alias Pattern for creating config-friendly types and eliminating switch statement duplication.
---
### 2.5. The Over-Abstraction Trap ⚠️
**Critical**: Not every primitive needs a type. The goal is **clarity**, not **type proliferation**.
#### Quick Decision Checklist
**Create types when they**:
- ✅ Have multiple meaningful methods (>1) with real logic
- ✅ Enforce invariants/validation at construction
- ✅ Hide complex implementation
- ✅ Need controlled mutation → use **private fields**, NOT wrappers
**DON'T create types when they**:
- ❌ Just wrap primitives with one trivial method
- ❌ Add ceremony without benefit
- ❌ Good naming achieves same clarity
#### Bad vs Good: One Example
```go
// ❌ Bad: Trivial wrapper - 8 lines, no benefit
type CIDRPresence bool
func (p CIDRPresence) IsSet() bool { return bool(p) }
// ✅ Good: Private fields - same safety, less code
type CIDRConfig struct {
clusterCIDRSet bool // Only parser can set
serviceCIDRSet bool
}
func (c CIDRConfig) ClusterCIDRSet() bool { return c.clusterCIDRSet }
```
#### Complete Teaching & Examples
**→ See [Example 2: Over-Abstraction Section](./examples.md#first-refactoring-attempt-the-over-abstraction-trap)**
Full case study includes:
- Complete thought process & comparisons
- 6 questions before creating a type
- Balance diagram & decision tree
- When to stop refactoring
---
### 3. Early Returns (Reduce Nesting)
**Signal:**
- Linter: High cyclomatic complexity
- Code smell: Nesting > 2 levels
**Pattern:**
```go
// Before: Deep nesting
func ProcessRequest(req Request) error {
if req.IsValid() {
if req.HasAuth() {
if req.HasPermission() {
// Do work
result, err := doWork(req)
if err != nil {
return err
}
return saveResult(result)
} else {
return errors.New("no permission")
}
} else {
return errors.New("not authenticated")
}
} else {
return errors.New("invalid request")
}
}
// After: Early returns
func ProcessRequest(req Request) error {
if !req.IsValid() {
return errors.New("invalid request")
}
if !req.HasAuth() {
return errors.New("not authenticated")
}
if !req.HasPermission() {
return errors.New("no permission")
}
result, err := doWork(req)
if err != nil {
return err
}
return saveResult(result)
}
```
**Benefits:**
- Reduced nesting (max 1 level)
- Easier to read (guard clauses up front)
- Lower cyclomatic complexity
### 4. Extract Function (Long Functions)
**Signal:**
- Function > 50 LOC
- Multiple distinct concerns
**Pattern:**
```go
// Before: Long function (80 LOC)
func RegisterUser(data map[string]interface{}) error {
// Parsing (15 lines)
email, ok := data["email"].(string)
if !ok { return errors.New("email required") }
// ... more parsing
// Validation (20 lines)
if email == "" { return errors.New("email required") }
if !strings.Contains(email, "@") { return errors.New("invalid email") }
// ... more validation
// Database (25 lines)
db, err := getDB()
if err != nil { return err }
// ... DB operations
// Email (15 lines)
smtp := getSMTP()
// ... email sending
// Logging (5 lines)
log.Printf("User registered: %s", email)
// ...
return nil
}
// After: Extracted functions
func RegisterUser(data map[string]interface{}) error {
user, err := parseUserData(data)
if err != nil {
return err
}
if err := validateUser(user); err != nil {
return err
}
if err := saveUserToDB(user); err != nil {
return err
}
if err := sendWelcomeEmail(user); err != nil {
return err
}
logUserRegistration(user)
return nil
}
func parseUserData(data map[string]interface{}) (*User, error) {
// 15 lines
}
func validateUser(user *User) error {
// 20 lines
}
func saveUserToDB(user *User) error {
// 25 lines
}
func sendWelcomeEmail(user *User) error {
// 15 lines
}
func logUserRegistration(user *User) {
// 5 lines
}
```
**Guidelines:**
- Aim for functions under 50 LOC
- Each function single responsibility
- Top-level function reads like a story
### 5. Switch Statement Extraction
**Signal:**
- Long function due to switch statement
- Each case is complex
**Pattern:**
```go
// Before
func RouteHandler(action string, params map[string]string) error {
switch action {
case "create":
// Validate create params
if params["name"] == "" { return errors.New("name required") }
// ... 15 more lines
return db.Create(...)
case "update":
// Validate update params
if params["id"] == "" { return errors.New("id required") }
// ... 20 more lines
return db.Update(...)
case "delete":
// Validate delete params
// ... 12 more lines
return db.Delete(...)
default:
return errors.New("unknown action")
}
}
// After
func RouteHandler(action string, params map[string]string) error {
switch action {
case "create":
return handleCreate(params)
case "update":
return handleUpdate(params)
case "delete":
return handleDelete(params)
default:
return errors.New("unknown action")
}
}
func handleCreate(params map[string]string) error {
// All create logic (15 lines)
}
func handleUpdate(params map[string]string) error {
// All update logic (20 lines)
}
func handleDelete(params map[string]string) error {
// All delete logic (12 lines)
}
```
### 6. Defer Complexity Extraction
**Signal:**
- Linter: Defer function has cyclomatic complexity > 1
**Pattern:**
```go
// Before: Complex defer
func ProcessFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer func() {
if err := f.Close(); err != nil {
if !errors.Is(err, fs.ErrClosed) {
log.Printf("Error closing file: %v", err)
}
}
}()
// Process file
return nil
}
// After: Extracted cleanup function
func ProcessFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer closeFile(f)
// Process file
return nil
}
func closeFile(f *os.File) {
if err := f.Close(); err != nil {
if !errors.Is(err, fs.ErrClosed) {
log.Printf("Error closing file: %v", err)
}
}
}
```
---
## Linter-Specific Refactoring
### Cyclomatic Complexity
**Cause**: Too many decision points (if, switch, for, &&, ||)
**Solutions:**
1. Extract functions for different branches
2. Use early returns to reduce nesting
3. Extract type with methods for primitive logic
4. Simplify boolean expressions
### Cognitive Complexity
**Cause**: Code hard to understand (nested logic, mixed abstractions)
**Solutions:**
1. Storifying (clarify abstraction levels)
2. Extract nested logic to named functions
3. Use early returns
4. Break into smaller, focused functions
### Maintainability Index
**Cause**: Code difficult to maintain
**Solutions:**
1. All of the above
2. Improve naming
3. Add comments for complex logic
4. Reduce coupling
---
## Guidelines for Effective Refactoring
### Keep Functions Small
- Target: Under 50 LOC
- Max 2 nesting levels
- Single responsibility
### Prefer Simplicity
- Simple, straightforward solutions over complex ones
- Descriptive variable and function names
- Avoid magic numbers and strings
### Maintain Tests
- Tests should pass after refactoring
- Add tests for new functions if needed
- Maintain or improve coverage
### Avoid Global State
- No global variables
- Inject dependencies through constructors
- Keep state localized
---
## Common Refactoring Scenarios
### Scenario 1: Linter Says "Cyclomatic Complexity Too High"
1. Identify decision points (if, switch, loops)
2. Extract branches to separate functions
3. Consider early returns
4. Check for primitive obsession (move logic to type)
### Scenario 2: Function Feels Hard to Test
1. Probably doing too much → Extract functions
2. Might have hidden dependencies → Inject through constructor
3. Might mix concerns → Separate responsibilities
### Scenario 3: Code Duplicated Across Functions
1. Extract common logic to shared function
2. Consider if primitives should be types (with methods)
3. Check if behavior belongs on existing type
### Scenario 4: Can't Name Function Clearly
1. Probably doing too much → Split responsibilities
2. Might be at wrong abstraction level
3. Reconsider what the function should do
---
## After Refactoring Checklist
- [ ] Linter passes (`task lintwithfix`)
- [ ] Tests pass (`go test ./...`)
- [ ] Coverage maintained or improved
- [ ] Code more readable
- [ ] Functions under 50 LOC
- [ ] Max 2 nesting levels
- [ ] Each function has clear purpose
---
## Integration with Design Principles
Refactoring often reveals design issues. After refactoring, consider:
**Created new types?**
→ Use @code-designing to validate type design
**Changed architecture?**
→ Ensure still following vertical slice structure
**Extracted significant logic?**
→ Ensure tests cover new functions (100% for leaf types)
---
## Summary: Refactoring Decision Tree
```
Linter fails or code complex
1. Does it read like a story?
No → Extract functions for abstraction levels
2. Can it be broken into smaller pieces?
Yes → By responsibility/task/category?
Extract functions/types/packages
3. Does logic run on primitives?
Yes → Is this primitive obsession?
Create custom type with methods
4. Is it long due to switch statement?
Yes → Extract case handlers
5. Deeply nested if/else?
Yes → Early returns or extract functions
Re-run linter → Should pass
Run tests → Should pass
If new types → Validate with @code-designing
```
**Remember**: Cohesion > Coupling. Put logic where it belongs.