Some checks failed
release / tag (push) Has been cancelled
Phase 1 of mathias/skills extraction (infra#62 Track D — homelab next-step plan addendum). Imports ~/dev/.skills/ verbatim (19 skill dirs + SKILLS_INDEX.md) and adds the installation surface: - Taskfile.yml — install / update / list / release / check targets - install.sh — bootstrap installer for hosts without Task. Idempotent symlink wirer; default checkout at ~/.local/share/skills/ on every host; SKILLS_REF env var pins a tag (default: main). - .gitea/workflows/release.yml — auto-tag every push to main by Bump-Type footer (major/minor/patch, default patch). Skipped when commit contains [skip-release]. - README — usage, versioning, contribution flow, secret-hygiene rule. Phase 1 wires Claude Code only (~/.claude/skills/<name> global + <repo>/.claude/skills/<name> per-repo). Phase 2 adds Crush, opencode, antigravity, and gitea-resident agents (cobalt-dingo, agentsquad) once their skill conventions are researched. Public repo, markdown-only — no secrets, no client names. Verified via pre-push grep before initial push. [skip-release]
369 lines
9.4 KiB
Markdown
369 lines
9.4 KiB
Markdown
---
|
|
name: refactoring
|
|
description: Systematic refactoring methodology based on Martin Fowler's catalog. Apply during the REFACTOR phase of TDD, never while making code changes that modify behavior.
|
|
---
|
|
|
|
# Refactoring
|
|
|
|
## Core Principle
|
|
|
|
> "Refactoring is a controlled technique for improving the design of existing code. Its essence is applying a series of small behavior-preserving transformations." — Martin Fowler
|
|
|
|
**The First Rule:** Never refactor without tests. Tests are your safety net. If tests don't exist, write them first (load `tdd` skill).
|
|
|
|
**The Second Rule:** Each refactoring step must keep tests green. If a step breaks tests, undo it.
|
|
|
|
**Never mix refactoring and behavior change.** Separate commits for each.
|
|
|
|
## When to Refactor
|
|
|
|
- After the GREEN phase of TDD (REFACTOR phase)
|
|
- When code smell makes a feature hard to add
|
|
- When the Boy Scout Rule demands it — you're in the area, leave it better
|
|
- When technical debt is blocking velocity
|
|
|
|
**When NOT to refactor:**
|
|
- Code that works and won't change
|
|
- Code being replaced soon
|
|
- When you don't have tests
|
|
|
|
## The Process
|
|
|
|
### Step 1: Ensure Test Coverage
|
|
|
|
```bash
|
|
go test ./... # All tests pass first
|
|
go test -race ./... # Race detector clean
|
|
```
|
|
|
|
If tests don't exist, write them before refactoring. See the `tdd` skill.
|
|
|
|
### Step 2: Identify the Target
|
|
|
|
Load the `code-review` skill to detect smells. Prioritize:
|
|
1. **Architectural smells** — Shotgun Surgery, Divergent Change, God Objects
|
|
2. **Design smells** — Long Methods, Feature Envy, Primitive Obsession
|
|
3. **Readability smells** — naming, comments, duplication
|
|
|
|
### Step 3: Apply Techniques in Small Steps
|
|
|
|
Each technique below is one small step. Make the change. Run tests. Commit if green. Repeat.
|
|
|
|
### Step 4: Verify After Each Step
|
|
|
|
```bash
|
|
go test ./...
|
|
go test -race ./...
|
|
```
|
|
|
|
If red: undo the step (git checkout), understand why, try a smaller step.
|
|
|
|
## Technique Catalog
|
|
|
|
### Composing Methods
|
|
|
|
**Extract Function**
|
|
When: a code block can be named meaningfully; a function does multiple things.
|
|
```go
|
|
// Before
|
|
func processOrder(o *Order) {
|
|
// validate
|
|
if o.Items == nil { panic("no items") }
|
|
// calculate
|
|
total := 0.0
|
|
for _, item := range o.Items {
|
|
total += item.Price
|
|
}
|
|
// save
|
|
db.Save(o, total)
|
|
}
|
|
|
|
// After: each step extracted
|
|
func processOrder(o *Order) {
|
|
validateOrder(o)
|
|
total := calculateTotal(o.Items)
|
|
saveOrder(o, total)
|
|
}
|
|
```
|
|
Cross-reference: when naming extracted functions, load `clean-code` skill for naming conventions.
|
|
|
|
**Inline Function**
|
|
When: a function's body is as clear as its name; a function only does what its name says.
|
|
|
|
**Extract Variable**
|
|
When: a complex expression is hard to read inline.
|
|
```go
|
|
// Before
|
|
if user.Subscription.Level >= 2 && !user.IsBanned && user.VerifiedAt != nil { ... }
|
|
|
|
// After
|
|
canAccessPremium := user.Subscription.Level >= 2 && !user.IsBanned && user.VerifiedAt != nil
|
|
if canAccessPremium { ... }
|
|
```
|
|
|
|
**Replace Temp with Query**
|
|
When: a temporary variable holds the result of an expression used once.
|
|
|
|
### Moving Features Between Types
|
|
|
|
**Move Method**
|
|
When: Feature Envy detected — a method uses another type's data more than its own.
|
|
```go
|
|
// Before: Order envies Customer
|
|
func (o *Order) getShippingCost() float64 {
|
|
if o.Customer.Country == "SE" { return 0 }
|
|
return 50
|
|
}
|
|
|
|
// After: moved to Customer
|
|
func (c *Customer) ShippingCost() float64 {
|
|
if c.Country == "SE" { return 0 }
|
|
return 50
|
|
}
|
|
```
|
|
|
|
**Extract Type**
|
|
When: a cluster of methods and fields have a different responsibility than the rest of the type.
|
|
|
|
**Inline Type**
|
|
When: a type isn't doing enough to justify its existence (Lazy Class smell).
|
|
|
|
**Hide Delegate**
|
|
When: callers access objects through chains (`a.B().C()`).
|
|
|
|
**Remove Middle Man**
|
|
When: a type only delegates without adding value.
|
|
|
|
### Organizing Data
|
|
|
|
**Replace Data Value with Object**
|
|
When: Primitive Obsession detected — a string is being used for email, user ID, currency.
|
|
```go
|
|
// Before
|
|
type User struct {
|
|
Email string
|
|
}
|
|
|
|
// After: Email type carries validation and prevents misuse
|
|
type Email struct {
|
|
value string
|
|
}
|
|
|
|
func NewEmail(s string) (Email, error) {
|
|
if !strings.Contains(s, "@") {
|
|
return Email{}, fmt.Errorf("invalid email: %q", s)
|
|
}
|
|
return Email{value: s}, nil
|
|
}
|
|
|
|
type User struct {
|
|
Email Email
|
|
}
|
|
```
|
|
|
|
**Replace Magic Number with Constant**
|
|
```go
|
|
// Before
|
|
if retries > 3 { ... }
|
|
|
|
// After
|
|
const maxRetries = 3
|
|
if retries > maxRetries { ... }
|
|
```
|
|
|
|
### Simplifying Conditionals
|
|
|
|
**Decompose Conditional**
|
|
When: complex conditionals obscure intent.
|
|
```go
|
|
// Before
|
|
if plan.ExpiresAt.Before(time.Now()) && plan.GracePeriodDays > 0 { ... }
|
|
|
|
// After
|
|
if plan.IsExpiredWithGrace() { ... }
|
|
```
|
|
|
|
**Replace Nested Conditional with Guard Clauses**
|
|
When: deep nesting; use early returns instead.
|
|
```go
|
|
// Before: arrow code
|
|
func process(o *Order) error {
|
|
if o != nil {
|
|
if len(o.Items) > 0 {
|
|
if o.Customer != nil {
|
|
// actual logic
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// After: guard clauses
|
|
func process(o *Order) error {
|
|
if o == nil {
|
|
return errors.New("nil order")
|
|
}
|
|
if len(o.Items) == 0 {
|
|
return errors.New("empty order")
|
|
}
|
|
if o.Customer == nil {
|
|
return errors.New("no customer")
|
|
}
|
|
// actual logic
|
|
return nil
|
|
}
|
|
```
|
|
|
|
**Replace Conditional with Polymorphism**
|
|
When: repeated switch/if-else on a type string. Replace with an interface.
|
|
See the `solid` skill, OCP section for full example.
|
|
|
|
**Introduce Null Object**
|
|
When: repeated nil checks for a dependency.
|
|
|
|
### Simplifying Function Calls
|
|
|
|
**Rename Method/Variable**
|
|
The most powerful refactoring. When you understand what something does, give it a name that reflects that understanding.
|
|
Cross-reference: load `clean-code` skill for naming conventions.
|
|
|
|
**Introduce Parameter Object**
|
|
When: Long Parameter List smell — multiple parameters that always appear together.
|
|
```go
|
|
// Before
|
|
func Search(query string, page, pageSize int, sortBy string, ascending bool) Results { ... }
|
|
|
|
// After
|
|
type SearchOptions struct {
|
|
Query string
|
|
Page int
|
|
PageSize int
|
|
SortBy string
|
|
Ascending bool
|
|
}
|
|
|
|
func Search(opts SearchOptions) Results { ... }
|
|
```
|
|
|
|
**Separate Query from Modifier (Command-Query Separation)**
|
|
When: a function both changes state and returns data.
|
|
```go
|
|
// Bad: modifies AND returns
|
|
func (s *Stack) PopAndReturn() (Item, bool) {
|
|
// removes and returns
|
|
}
|
|
|
|
// Good: separate
|
|
func (s *Stack) Peek() (Item, bool) { ... } // query only
|
|
func (s *Stack) Pop() bool { ... } // command only
|
|
```
|
|
|
|
### Dealing with Generalization
|
|
|
|
**Extract Interface**
|
|
When: you have multiple implementations or want to enable testing with a test double.
|
|
```go
|
|
// Before: depends on concrete type
|
|
type Service struct {
|
|
db *postgres.DB
|
|
}
|
|
|
|
// After: depends on interface
|
|
type Store interface {
|
|
Save(ctx context.Context, u User) error
|
|
}
|
|
|
|
type Service struct {
|
|
store Store
|
|
}
|
|
```
|
|
|
|
**Replace Inheritance with Delegation (Go: Embedding → Explicit Delegation)**
|
|
When: embedding is used but only some methods of the embedded type are needed.
|
|
|
|
## Refactoring Sequence Dependencies
|
|
|
|
Apply in this order to avoid breaking changes:
|
|
|
|
1. **Extract Variable** → then Extract Function → then Move Function
|
|
2. **Encapsulate Field** → before other data refactorings
|
|
3. **Extract Function** → before Move Function
|
|
4. **Rename** → before Extract Interface
|
|
|
|
## Go-Specific Refactoring Notes
|
|
|
|
### Extracting interfaces
|
|
|
|
In Go, define the interface at the point of use, not at the implementation:
|
|
```go
|
|
// The service package defines what it needs
|
|
package service
|
|
|
|
type UserStore interface {
|
|
GetByID(ctx context.Context, id UserID) (User, error)
|
|
}
|
|
```
|
|
|
|
### Error handling during refactoring
|
|
|
|
When extracting functions, preserve the error wrapping chain:
|
|
```go
|
|
// Extracted function must wrap errors with its context
|
|
func validateOrder(o *Order) error {
|
|
if len(o.Items) == 0 {
|
|
return errors.New("order has no items")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Caller wraps the context
|
|
if err := validateOrder(o); err != nil {
|
|
return fmt.Errorf("process order: %w", err)
|
|
}
|
|
```
|
|
|
|
### Struct options pattern (for long parameter lists)
|
|
|
|
Go idiom for optional parameters:
|
|
```go
|
|
type ServerOption func(*Server)
|
|
|
|
func WithTimeout(d time.Duration) ServerOption {
|
|
return func(s *Server) { s.timeout = d }
|
|
}
|
|
|
|
func NewServer(addr string, opts ...ServerOption) *Server {
|
|
s := &Server{addr: addr, timeout: 30 * time.Second}
|
|
for _, opt := range opts {
|
|
opt(s)
|
|
}
|
|
return s
|
|
}
|
|
```
|
|
|
|
## Refactoring Commit Strategy
|
|
|
|
Refactoring commits should be separate from feature commits:
|
|
- `refactor: extract validateOrder from processOrder`
|
|
- `refactor: replace string email with Email value type`
|
|
- `refactor: rename UserData to UserProfile`
|
|
|
|
Never combine: `feat: add discount + refactor: extract discount calculator`
|
|
|
|
## Verification Checklist
|
|
|
|
After each refactoring session:
|
|
- [ ] `go test ./...` passes
|
|
- [ ] `go test -race ./...` passes
|
|
- [ ] No behavior change (same inputs produce same outputs)
|
|
- [ ] Committed with a `refactor:` prefix commit message
|
|
- [ ] Smells addressed are documented (if doing a review)
|
|
|
|
## Cross-References
|
|
|
|
- When applying Extract Function: load `clean-code` skill for naming conventions
|
|
- When introducing interfaces: load `solid` skill for DIP and ISP guidance
|
|
- When detecting smells: load `code-review` skill
|
|
- Source: Martin Fowler, *Refactoring: Improving the Design of Existing Code* (2nd ed.)
|
|
- Full catalog: https://refactoring.guru/refactoring
|