Files
gh-buzzdan-ai-coding-rules-…/skills/refactoring/SKILL.md
2025-11-29 18:02:42 +08:00

23 KiB

name, description
name description
refactoring 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 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

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

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

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

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:

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

  • 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

  • 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 (validateCIDRalignCIDRArgs)

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 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 for complete refactoring patterns and decision tree.