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
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):
-
Does this read like a story?
- No → Extract functions for different abstraction levels
-
Can this be broken into smaller pieces?
- By responsibility? → Extract functions
- By task? → Extract functions
- By category? → Extract functions
-
Does logic run on primitives?
- Yes → Is this primitive obsession? → Extract type
-
Is function long due to switch statement?
- Yes → Extract case handlers
-
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:
NATSClientwith injectednatsAddressstring (no other dependencies)Emailtype with validation logic (no dependencies)ServiceEndpointthat usesPortvalue object (both are testable islands)OrderServicewith injectedRepositoryandEventPublisher(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
IPConfigtype 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 (
validateCIDR→alignCIDRArgs)
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.