refactoring

NovusAevum's avatarfrom NovusAevum

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.

⭐1stars🔀0forks📁View on GitHub🕐Updated Nov 18, 2025

When & Why to Use This Skill

This Claude skill automates linter-driven refactoring to reduce code complexity and enhance maintainability. By applying proven patterns like storifying, type extraction, and function extraction, it helps developers resolve cyclomatic and cognitive complexity issues, transforming hard-to-read code into clean, professional-grade software.

Use Cases

  • Automated resolution of linter failures related to high cyclomatic, cognitive, or maintainability complexity scores.
  • Refactoring deeply nested conditional blocks into clean, readable code using early return patterns.
  • Decomposing large, multi-responsibility functions into smaller, testable units to improve modularity.
  • Eliminating 'primitive obsession' by encapsulating business logic and validation within custom domain types.
  • Standardizing code abstraction levels through 'storifying' to make complex logic flow like a readable narrative.
namerefactoring
descriptionLinter-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

  • Linter fails with complexity issues (cyclomatic, cognitive, maintainability)
  • Code feels hard to read or maintain
  • Functions are too long or deeply nested
  • Automatically invoked by @linter-driven-development when linter fails

Learning Resources

Choose your learning path:

  • Quick Start: Use the patterns below for common refactoring cases
  • Complete Reference: See reference.md for full decision tree and all patterns
  • Real-World Examples: See examples.md to learn the refactoring thought process
    • Example 1: Storifying and extracting a single leaf type
    • Example 2: Primitive obsession with multiple types and switch elimination

Refactoring Signals

Linter Failures

  • 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

Code Smells

  • Functions > 50 LOC
  • Nesting > 2 levels
  • Mixed abstraction levels
  • Unclear flow/purpose
  • Primitive obsession

Workflow

1. Interpret Linter Output

Run task lintwithfix and analyze failures:

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. Diagnose Root Cause

For each failure, ask (see reference.md):

  • Does this code read like a story? → Storifying needed
  • Can this be broken into smaller pieces? → Extract functions/types
  • Does logic run on primitives? → Primitive obsession
  • Is function long due to switch statement? → Categorize and extract

3. Apply Refactoring Pattern

Choose appropriate pattern:

  • Storifying: Extract helpers to clarify levels
  • Extract Type: Move primitive logic to custom type
  • Extract Function: Pull out complexity
  • Early Returns: Reduce nesting
  • Switch Extraction: Categorize cases

4. Verify Improvement

  • Re-run linter
  • Tests still pass
  • Code more readable?

Refactoring Patterns

Pattern 1: Storifying (Mixed Abstractions)

Signal: Function mixes high-level steps with low-level details

// ❌ 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

// ❌ Before - Primitive obsession
func ValidatePort(port int) error {
    if port <= 0 || port >= 9000 {
        return errors.New("invalid port")
    }
    return nil
}

func GetServiceURL(host string, port int) string {
    return fmt.Sprintf("%s:%d", host, port)
}

// ✅ After - Custom type
type Port int

func NewPort(p int) (Port, error) {
    if p <= 0 || p >= 9000 {
        return 0, errors.New("invalid port")
    }
    return Port(p), nil
}

func (p Port) Int() int {
    return int(p)
}

type ServiceAddress struct {
    host string
    port Port
}

func (a ServiceAddress) URL() string {
    return fmt.Sprintf("%s:%d", a.host, a.port)
}

⚠️ Warning: Not Every Primitive Needs a Type!

Create types when: >1 meaningful methods, validation, complex logic, or controlled mutation DON'T when: One trivial method, good naming is enough, adds ceremony without benefit

// ❌ Over-abstraction
type IsValidFlag bool
func (f IsValidFlag) IsValid() bool { return bool(f) }

// ✅ Good naming or private fields
isValid bool  // Clear enough!

→ See Example 2 for complete case study.

Pattern 3: Extract Function (Long Functions)

Signal: Function > 50 LOC or multiple responsibilities

// ❌ 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

// ❌ 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

// ❌ 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 { /* ... */ }

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

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

🔧 REFACTORING COMPLETE

Linter Issues Resolved:
✅ user/service.go:45 - Cyclomatic complexity (15 → 8)
✅ user/handler.go:23 - Cognitive complexity (25 → 12)

Refactoring Applied:
1. Storifying: Extracted validateUser, saveUser, notifyUser
2. Extract Type: Created Port and ServiceAddress types
3. Early Returns: Reduced nesting in ProcessItem

Files Modified:
- user/service.go (+30, -45 lines)
- user/port.go (new file, +25 lines)
- user/address.go (new file, +35 lines)

Next Steps:
1. Re-run linter: task lintwithfix → Should pass
2. Run tests: go test ./... → Should pass
3. If new types created → Consider @code-designing review
4. Proceed to @pre-commit-review phase

Learning from Examples

For real-world refactoring case studies that show the complete thought process:

Example 1: Storifying Mixed Abstractions

  • Transforms a 48-line fat function into lean orchestration + leaf type
  • Shows how to extract IPConfig type for collection and validation logic
  • Demonstrates achieving 100% unit test coverage without mocking

Example 2: Primitive Obsession with Multiple Types

  • Transforms a 60-line function into a 7-line story by extracting 4 leaf types
  • Shows the Type Alias Pattern for config-friendly types
  • Demonstrates eliminating switch statement duplication
  • Fixed misleading function name (validateCIDR → alignCIDRArgs)

See examples.md for complete case studies with thought process.

Integration with Other Skills

  • @code-designing: When refactoring creates new types, validate design
  • @testing: Ensure refactored code maintains test coverage
  • @pre-commit-review: Final validation before commit

See reference.md for complete refactoring patterns and decision tree.