Skills move to skills/<name>/SKILL.md and the repo gains both manifests, so it is now a marketplace hosting exactly one plugin whose root is the repo root (source: "."). Claude Code installs it from git; no symlinks involved. Why: install.sh wired the library into every consuming repo as an ABSOLUTE symlink to $CHECKOUT_DIR. The checkout moved three times, most recently when local-dev@1966d5a dropped the embedded .skills/ copy, and nothing migrated the links — 243 dangling links across 12 repos, hidden from git by the **/.claude/skills/ ignore rule. Worse, ~/dev spans two path roots (/Users/mathias on the iCloud-shared Macs, /home/mathias on koala), so an absolute link stored there is broken on one side by construction. Re-running the installer cannot fix that; only removing the mechanism can. install.sh keeps the harnesses with no plugin system and drops from 42 links per harness to one directory link, via the convergent ~/.agents/skills convention that Pi and OpenCode both read: ~/.agents/skills -> $CHECKOUT_DIR/skills ~/.config/crush/skills -> ~/.agents/skills ~/.config/opencode/skills -> ~/.agents/skills wire_claude_repo is deleted outright rather than gated behind a flag — on a mixed-root fleet it cannot be made correct. Antigravity and Vibe targets go too; both unused. CHECKOUT_DIR keeps its host-local default deliberately: pointing it into ~/dev would put a git working tree inside iCloud with two concurrent writers. Taskfile drops five near-identical wiring loops (they had already drifted) and delegates to install.sh. `task check` gains frontmatter validation, a stray-root-entry gate, and manifest/tag version parity — the loose secret-hygiene.md sat unloadable for months because nothing checked layout. Verified: claude plugin validate . passes clean; a probe skill loaded as skills:zzprobe through --plugin-dir; task check green at 42 skills. Refs: #23 Bump-Type: minor Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
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
|