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]
9.4 KiB
name, description
| name | description |
|---|---|
| refactoring | 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
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:
- Architectural smells — Shotgun Surgery, Divergent Change, God Objects
- Design smells — Long Methods, Feature Envy, Primitive Obsession
- 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
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.
// 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.
// 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.
// 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.
// 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
// Before
if retries > 3 { ... }
// After
const maxRetries = 3
if retries > maxRetries { ... }
Simplifying Conditionals
Decompose Conditional When: complex conditionals obscure intent.
// 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.
// 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.
// 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.
// 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.
// 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:
- Extract Variable → then Extract Function → then Move Function
- Encapsulate Field → before other data refactorings
- Extract Function → before Move Function
- Rename → before Extract Interface
Go-Specific Refactoring Notes
Extracting interfaces
In Go, define the interface at the point of use, not at the implementation:
// 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:
// 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:
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 processOrderrefactor: replace string email with Email value typerefactor: rename UserData to UserProfile
Never combine: feat: add discount + refactor: extract discount calculator
Verification Checklist
After each refactoring session:
go test ./...passesgo 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-codeskill for naming conventions - When introducing interfaces: load
solidskill for DIP and ISP guidance - When detecting smells: load
code-reviewskill - Source: Martin Fowler, Refactoring: Improving the Design of Existing Code (2nd ed.)
- Full catalog: https://refactoring.guru/refactoring