17 KiB
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?
// ❌ 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
// ❌ 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.
// ❌ 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?
// ❌ 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:
// 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 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:
// 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 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
// ❌ 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
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:
// 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:
// 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:
// 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:
// 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:
- Extract functions for different branches
- Use early returns to reduce nesting
- Extract type with methods for primitive logic
- Simplify boolean expressions
Cognitive Complexity
Cause: Code hard to understand (nested logic, mixed abstractions)
Solutions:
- Storifying (clarify abstraction levels)
- Extract nested logic to named functions
- Use early returns
- Break into smaller, focused functions
Maintainability Index
Cause: Code difficult to maintain
Solutions:
- All of the above
- Improve naming
- Add comments for complex logic
- 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"
- Identify decision points (if, switch, loops)
- Extract branches to separate functions
- Consider early returns
- Check for primitive obsession (move logic to type)
Scenario 2: Function Feels Hard to Test
- Probably doing too much → Extract functions
- Might have hidden dependencies → Inject through constructor
- Might mix concerns → Separate responsibilities
Scenario 3: Code Duplicated Across Functions
- Extract common logic to shared function
- Consider if primitives should be types (with methods)
- Check if behavior belongs on existing type
Scenario 4: Can't Name Function Clearly
- Probably doing too much → Split responsibilities
- Might be at wrong abstraction level
- 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.