Initial commit
This commit is contained in:
759
skills/refactoring/SKILL.md
Normal file
759
skills/refactoring/SKILL.md
Normal 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.
|
||||
147
skills/refactoring/code-design-ref.md
Normal file
147
skills/refactoring/code-design-ref.md
Normal 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.
|
||||
1677
skills/refactoring/examples.md
Normal file
1677
skills/refactoring/examples.md
Normal file
File diff suppressed because it is too large
Load Diff
745
skills/refactoring/reference.md
Normal file
745
skills/refactoring/reference.md
Normal 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.
|
||||
Reference in New Issue
Block a user