chore: bootstrap skills library — 19 skills + installer + CI auto-tag
Some checks failed
release / tag (push) Has been cancelled
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]
This commit is contained in:
282
clean-code/SKILL.md
Normal file
282
clean-code/SKILL.md
Normal file
@@ -0,0 +1,282 @@
|
||||
---
|
||||
name: clean-code
|
||||
description: Write code that humans can read, understand, and safely change. Apply SOLID, GRASP, and Clean Code principles during the REFACTOR phase of TDD.
|
||||
---
|
||||
|
||||
# Clean Code
|
||||
|
||||
## Core Philosophy
|
||||
|
||||
Write code for humans first, computers second. Every piece of code you write will be read many more times than it is written. Optimize for clarity and simplicity above all else.
|
||||
|
||||
Code has three consumers:
|
||||
1. **Users** — get their needs met
|
||||
2. **Customers** — make or save money
|
||||
3. **Developers** — must maintain it
|
||||
|
||||
Developers read code 10x more than they write it. Design for them.
|
||||
|
||||
## When to Apply This Skill
|
||||
|
||||
- During the **REFACTOR phase** of TDD (after the test is green)
|
||||
- When reviewing code (load `code-review` skill too)
|
||||
- When designing new types or packages
|
||||
|
||||
**Do not apply during the GREEN phase.** Ugly code that passes is correct. Clean it after.
|
||||
|
||||
## Naming
|
||||
|
||||
Naming is the most important thing in code. In priority order:
|
||||
|
||||
1. **Consistency** — Same concept = same name everywhere. One name per concept.
|
||||
2. **Understandability** — Domain language, not technical jargon.
|
||||
3. **Specificity** — Precise, not vague. Avoid `data`, `info`, `manager`, `handler`, `processor`, `utils`.
|
||||
4. **Brevity** — Short but not cryptic.
|
||||
5. **Searchability** — Unique, greppable names.
|
||||
6. **Pronounceability** — You should be able to say it in conversation.
|
||||
|
||||
```go
|
||||
// Bad: inconsistent, vague, cryptic
|
||||
func getUsrById(id string) {}
|
||||
func fetchCustomerByID(id string) {}
|
||||
func retrieveClientById(id string) {}
|
||||
|
||||
// Good: consistent, specific
|
||||
func GetUser(ctx context.Context, id UserID) (User, error) {}
|
||||
func GetOrder(ctx context.Context, id OrderID) (Order, error) {}
|
||||
```
|
||||
|
||||
## Functions/Methods
|
||||
|
||||
- Do one thing, do it well, do it only
|
||||
- Operate at a single level of abstraction
|
||||
- Prefer fewer parameters — zero is ideal, one or two common, three should be rare
|
||||
- Avoid side effects — if a function has a side effect, name it accordingly
|
||||
- Prefer pure functions where possible
|
||||
|
||||
```go
|
||||
// Bad: does too much, mixed abstraction levels
|
||||
func processOrder(o *Order, db *sql.DB, mailer Mailer) error {
|
||||
if o.Items == nil || len(o.Items) == 0 {
|
||||
return errors.New("empty order")
|
||||
}
|
||||
total := 0.0
|
||||
for _, item := range o.Items {
|
||||
total += item.Price * float64(item.Qty)
|
||||
}
|
||||
if _, err := db.Exec("INSERT INTO orders ...", o.ID, total); err != nil {
|
||||
return fmt.Errorf("save order: %w", err)
|
||||
}
|
||||
if err := mailer.Send(o.Customer.Email, "confirmed"); err != nil {
|
||||
return fmt.Errorf("send confirmation: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Good: single level of abstraction, composed from focused functions
|
||||
func ProcessOrder(ctx context.Context, o *Order, repo OrderRepository, mailer Mailer) error {
|
||||
if err := validateOrder(o); err != nil {
|
||||
return fmt.Errorf("validate: %w", err)
|
||||
}
|
||||
total := calculateTotal(o.Items)
|
||||
if err := repo.Save(ctx, o, total); err != nil {
|
||||
return fmt.Errorf("save: %w", err)
|
||||
}
|
||||
if err := mailer.SendConfirmation(ctx, o.Customer.Email); err != nil {
|
||||
return fmt.Errorf("notify: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling (Go-Specific)
|
||||
|
||||
In Go, error returns are a first-class feature, not a smell. Follow these rules:
|
||||
|
||||
```go
|
||||
// Always wrap with context
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse config: %w", err)
|
||||
}
|
||||
|
||||
// Never: naked return without context
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Never: log and return (pick one)
|
||||
if err != nil {
|
||||
log.Error("failed", "err", err)
|
||||
return err // caller gets logged error AND returns it — double-counted
|
||||
}
|
||||
|
||||
// Never: swallow errors silently
|
||||
_ = doSomething()
|
||||
```
|
||||
|
||||
Error wrapping builds a call-chain narrative: `"http handler: parse request: validate email: invalid format"`. This is valuable, not verbose.
|
||||
|
||||
## Comments
|
||||
|
||||
**The best comment is code that doesn't need one.** Before writing a comment, ask whether you could make the code itself clearer.
|
||||
|
||||
When comments are necessary, explain **why**, not what. The code shows what happens; comments explain decisions and intent.
|
||||
|
||||
```go
|
||||
// Bad: explains what (redundant)
|
||||
// increment counter
|
||||
i++
|
||||
|
||||
// Good: explains why
|
||||
// Offset by 1 because the upstream API uses 1-based page numbering
|
||||
page := requestedPage + 1
|
||||
```
|
||||
|
||||
**Go adaptation:** `godoc` comments on exported identifiers ARE documentation, not a failure. The "comments are failure" rule applies to inline comments explaining bad code. Exported types and functions must have godoc comments.
|
||||
|
||||
```go
|
||||
// UserService handles user lifecycle operations.
|
||||
// It is safe for concurrent use.
|
||||
type UserService struct { ... }
|
||||
|
||||
// GetByEmail returns the user with the given email address.
|
||||
// Returns ErrNotFound if no user exists with that email.
|
||||
func (s *UserService) GetByEmail(ctx context.Context, email Email) (User, error) { ... }
|
||||
```
|
||||
|
||||
## SOLID Principles (Brief)
|
||||
|
||||
For full SOLID guidance with Go examples, load the `solid` skill.
|
||||
|
||||
| Principle | Quick test |
|
||||
|-----------|------------|
|
||||
| SRP | "Does this type have ONE reason to change?" |
|
||||
| OCP | "Can I add behavior by implementing an interface, not editing this code?" |
|
||||
| LSP | "Can callers substitute any implementation without knowing the difference?" |
|
||||
| ISP | "Is this interface the smallest it can be?" (`io.Reader` is a model) |
|
||||
| DIP | "Do I pass interfaces as parameters, not concrete types?" |
|
||||
|
||||
## GRASP Principles
|
||||
|
||||
**Information Expert:** Assign responsibility to the type that has the data needed to fulfil it.
|
||||
|
||||
**Low Coupling:** Minimize dependencies between packages. When one changes, few others should too.
|
||||
|
||||
**High Cohesion:** Keep related functionality together. Everything in a package should relate to its central purpose.
|
||||
|
||||
**Controller:** One type handles system events for a use case.
|
||||
|
||||
**Tell, Don't Ask:** Tell objects what to do rather than asking for their data and acting on it.
|
||||
|
||||
## Go-Specific Clean Code Rules
|
||||
|
||||
### Prefer interfaces over concrete types in function signatures
|
||||
|
||||
```go
|
||||
// Bad: tied to a concrete implementation
|
||||
func NewHandler(db *postgres.DB) *Handler { ... }
|
||||
|
||||
// Good: depends on the behavior needed
|
||||
func NewHandler(store UserStore) *Handler { ... }
|
||||
```
|
||||
|
||||
### Small, focused interfaces
|
||||
|
||||
```go
|
||||
// The io.Reader/io.Writer pattern is the Go ideal
|
||||
type Reader interface {
|
||||
Read(p []byte) (n int, err error)
|
||||
}
|
||||
|
||||
// If you need both, compose at the call site
|
||||
type ReadWriter interface {
|
||||
Reader
|
||||
Writer
|
||||
}
|
||||
```
|
||||
|
||||
### Embedding over inheritance
|
||||
|
||||
```go
|
||||
// Use embedding for composition, not to inherit behavior you'll override
|
||||
type LoggedHandler struct {
|
||||
Handler
|
||||
logger *slog.Logger
|
||||
}
|
||||
```
|
||||
|
||||
### Constructor injection for dependencies
|
||||
|
||||
```go
|
||||
type Service struct {
|
||||
repo Repository
|
||||
mailer Mailer
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewService(repo Repository, mailer Mailer, logger *slog.Logger) *Service {
|
||||
return &Service{repo: repo, mailer: mailer, logger: logger}
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid package-level state
|
||||
|
||||
Package-level variables are global state. They make tests unreliable and code hard to reason about.
|
||||
|
||||
```go
|
||||
// Bad
|
||||
var db *sql.DB
|
||||
|
||||
// Good: pass as dependency
|
||||
type Handler struct {
|
||||
db *sql.DB
|
||||
}
|
||||
```
|
||||
|
||||
## Code Smell Detection (Quick Reference)
|
||||
|
||||
During the REFACTOR phase, look for these smells and address them:
|
||||
|
||||
| Smell | Go Signal | Fix |
|
||||
|-------|-----------|-----|
|
||||
| Long function | > ~30 lines (Go can go longer, but be honest) | Extract function |
|
||||
| Too many parameters | > 3-4 parameters | Use a config struct or option pattern |
|
||||
| Divergent change | One type changes for multiple unrelated reasons | Split into focused types |
|
||||
| Feature envy | Method uses another type's data more than its own | Move method to that type |
|
||||
| Magic numbers | Bare literals | Named constants or typed config |
|
||||
| Deep nesting | > 3 levels of indentation | Early returns, extract helpers |
|
||||
|
||||
For comprehensive smell detection, load the `code-review` skill.
|
||||
|
||||
## Structural Principles
|
||||
|
||||
**Separation of concerns:** Business logic should know nothing of HTTP, SQL, or filesystems.
|
||||
|
||||
**Modularity:** Each package understandable in isolation.
|
||||
|
||||
**Command-Query Separation:** Functions either do something (command) or return something (query), not both.
|
||||
|
||||
**Principle of Least Surprise:** Code should behave the way readers expect.
|
||||
|
||||
**Boy Scout Rule:** Leave the code better than you found it.
|
||||
|
||||
## Pre-Refactor Checklist
|
||||
|
||||
Before cleaning up code:
|
||||
|
||||
- [ ] All tests pass
|
||||
- [ ] Race detector clean: `go test -race ./...`
|
||||
- [ ] I understand what the code does
|
||||
- [ ] I have a clear target design in mind
|
||||
|
||||
During refactoring:
|
||||
- [ ] Tests stay green after each change
|
||||
- [ ] Each change is small and focused
|
||||
- [ ] Commit after each meaningful improvement
|
||||
|
||||
## References
|
||||
|
||||
- `references/clean-code.md` — detailed naming rules, object calisthenics
|
||||
- `references/code-smells.md` — comprehensive smell catalog with examples
|
||||
- Load `solid` skill for SOLID principles with Go examples
|
||||
- Load `refactoring` skill for systematic refactoring techniques
|
||||
376
clean-code/references/clean-code.md
Normal file
376
clean-code/references/clean-code.md
Normal file
@@ -0,0 +1,376 @@
|
||||
# Clean Code Practices
|
||||
|
||||
## What is Clean Code?
|
||||
|
||||
Code that is:
|
||||
- **Easy to understand** - reveals intent clearly
|
||||
- **Easy to change** - modifications are localized
|
||||
- **Easy to test** - dependencies are injectable
|
||||
- **Simple** - no unnecessary complexity
|
||||
|
||||
## The Human-Centered Approach
|
||||
|
||||
Code has THREE consumers:
|
||||
1. **Users** - get their needs met
|
||||
2. **Customers** - make or save money
|
||||
3. **Developers** - must maintain it
|
||||
|
||||
Design for all three, but remember: **developers read code 10x more than they write it.**
|
||||
|
||||
## Naming Principles
|
||||
|
||||
### 1. Consistency & Uniqueness (HIGHEST PRIORITY)
|
||||
Same concept = same name everywhere. One name per concept.
|
||||
|
||||
```typescript
|
||||
// BAD: Inconsistent names for same concept
|
||||
getUserById(id)
|
||||
fetchCustomerById(id)
|
||||
retrieveClientById(id)
|
||||
|
||||
// GOOD: Consistent
|
||||
getUser(id)
|
||||
getOrder(id)
|
||||
getProduct(id)
|
||||
```
|
||||
|
||||
### 2. Understandability
|
||||
Use domain language, not technical jargon.
|
||||
|
||||
```typescript
|
||||
// BAD: Technical
|
||||
const arr = users.filter(u => u.isActive);
|
||||
|
||||
// GOOD: Domain language
|
||||
const activeCustomers = users.filter(user => user.isActive);
|
||||
```
|
||||
|
||||
### 3. Specificity
|
||||
Avoid vague names: `data`, `info`, `manager`, `handler`, `processor`, `utils`
|
||||
|
||||
```typescript
|
||||
// BAD: Vague
|
||||
class DataManager { }
|
||||
function processInfo(data) { }
|
||||
|
||||
// GOOD: Specific
|
||||
class OrderRepository { }
|
||||
function validatePayment(payment) { }
|
||||
```
|
||||
|
||||
### 4. Brevity (but not at cost of clarity)
|
||||
Short names are good only if meaning is preserved.
|
||||
|
||||
```typescript
|
||||
// BAD: Too cryptic
|
||||
const usrLst = getUsrs();
|
||||
|
||||
// BAD: Unnecessarily long
|
||||
const listOfAllActiveUsersInTheSystem = getActiveUsers();
|
||||
|
||||
// GOOD: Brief but clear
|
||||
const activeUsers = getActiveUsers();
|
||||
```
|
||||
|
||||
### 5. Searchability
|
||||
Names should be unique enough to grep/search.
|
||||
|
||||
```typescript
|
||||
// BAD: Common word, hard to search
|
||||
const data = fetch();
|
||||
|
||||
// GOOD: Unique, searchable
|
||||
const orderSummary = fetchOrderSummary();
|
||||
```
|
||||
|
||||
### 6. Pronounceability
|
||||
You should be able to say it in conversation.
|
||||
|
||||
```typescript
|
||||
// BAD
|
||||
const genymdhms = generateYearMonthDayHourMinuteSecond();
|
||||
|
||||
// GOOD
|
||||
const timestamp = generateTimestamp();
|
||||
```
|
||||
|
||||
### 7. Austerity
|
||||
Avoid unnecessary filler words.
|
||||
|
||||
```typescript
|
||||
// BAD: Redundant
|
||||
const userData = user; // 'Data' adds nothing
|
||||
class UserClass { } // 'Class' adds nothing
|
||||
|
||||
// GOOD
|
||||
const user = user;
|
||||
class User { }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Object Calisthenics (9 Rules)
|
||||
|
||||
Exercises to improve OO design. Follow strictly during practice, relax slightly in production.
|
||||
|
||||
### 1. One Level of Indentation per Method
|
||||
|
||||
```typescript
|
||||
// BAD: Multiple levels
|
||||
function process(orders: Order[]) {
|
||||
for (const order of orders) {
|
||||
if (order.isValid()) {
|
||||
for (const item of order.items) {
|
||||
if (item.inStock) {
|
||||
// process...
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Extract methods
|
||||
function process(orders: Order[]) {
|
||||
orders.filter(o => o.isValid()).forEach(processOrder);
|
||||
}
|
||||
|
||||
function processOrder(order: Order) {
|
||||
order.items.filter(i => i.inStock).forEach(processItem);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Don't Use the ELSE Keyword
|
||||
|
||||
Use early returns, guard clauses, or polymorphism.
|
||||
|
||||
```typescript
|
||||
// BAD: else
|
||||
function getDiscount(user: User): number {
|
||||
if (user.isPremium) {
|
||||
return 20;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Early return
|
||||
function getDiscount(user: User): number {
|
||||
if (user.isPremium) return 20;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Wrap All Primitives and Strings
|
||||
|
||||
Primitives should be wrapped in domain objects when they have meaning.
|
||||
|
||||
```typescript
|
||||
// BAD: Primitive obsession
|
||||
function createUser(email: string, age: number) { }
|
||||
|
||||
// GOOD: Value objects
|
||||
class Email {
|
||||
constructor(private value: string) {
|
||||
if (!this.isValid(value)) throw new InvalidEmail();
|
||||
}
|
||||
private isValid(email: string): boolean { ... }
|
||||
}
|
||||
|
||||
class Age {
|
||||
constructor(private value: number) {
|
||||
if (value < 0 || value > 150) throw new InvalidAge();
|
||||
}
|
||||
}
|
||||
|
||||
function createUser(email: Email, age: Age) { }
|
||||
```
|
||||
|
||||
### 4. First-Class Collections
|
||||
|
||||
Any class with a collection should have no other instance variables.
|
||||
|
||||
```typescript
|
||||
// BAD: Collection mixed with other state
|
||||
class Order {
|
||||
items: OrderItem[] = [];
|
||||
customerId: string;
|
||||
total: number;
|
||||
}
|
||||
|
||||
// GOOD: Collection is its own class
|
||||
class OrderItems {
|
||||
constructor(private items: OrderItem[] = []) {}
|
||||
|
||||
add(item: OrderItem): void { ... }
|
||||
total(): Money { ... }
|
||||
isEmpty(): boolean { ... }
|
||||
}
|
||||
|
||||
class Order {
|
||||
constructor(
|
||||
private items: OrderItems,
|
||||
private customerId: CustomerId
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. One Dot per Line (Law of Demeter)
|
||||
|
||||
Don't chain through object graphs.
|
||||
|
||||
```typescript
|
||||
// BAD: Train wreck
|
||||
const city = order.customer.address.city;
|
||||
|
||||
// GOOD: Tell, don't ask
|
||||
const city = order.getShippingCity();
|
||||
```
|
||||
|
||||
### 6. Don't Abbreviate
|
||||
|
||||
If a name is too long to type, the class is doing too much.
|
||||
|
||||
```typescript
|
||||
// BAD
|
||||
const custRepo = new CustRepo();
|
||||
const ord = new Ord();
|
||||
|
||||
// GOOD
|
||||
const customerRepository = new CustomerRepository();
|
||||
const order = new Order();
|
||||
```
|
||||
|
||||
### 7. Keep All Entities Small
|
||||
|
||||
- Classes: < 50 lines
|
||||
- Methods: < 10 lines
|
||||
- Files: < 100 lines
|
||||
|
||||
If larger, it's probably doing too much. Split it.
|
||||
|
||||
### 8. No Classes with More Than Two Instance Variables
|
||||
|
||||
Forces small, focused classes.
|
||||
|
||||
```typescript
|
||||
// BAD: Too many variables
|
||||
class Order {
|
||||
id: string;
|
||||
customerId: string;
|
||||
items: Item[];
|
||||
total: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
// GOOD: Composed of smaller objects
|
||||
class Order {
|
||||
constructor(
|
||||
private id: OrderId,
|
||||
private details: OrderDetails
|
||||
) {}
|
||||
}
|
||||
|
||||
class OrderDetails {
|
||||
constructor(
|
||||
private customer: Customer,
|
||||
private lineItems: LineItems
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
### 9. No Getters/Setters/Properties
|
||||
|
||||
Objects should have behavior, not just data. Tell objects what to do.
|
||||
|
||||
```typescript
|
||||
// BAD: Data bag with getters
|
||||
class Account {
|
||||
getBalance(): number { return this.balance; }
|
||||
setBalance(value: number) { this.balance = value; }
|
||||
}
|
||||
|
||||
// Caller does the work
|
||||
if (account.getBalance() >= amount) {
|
||||
account.setBalance(account.getBalance() - amount);
|
||||
}
|
||||
|
||||
// GOOD: Behavior-rich object
|
||||
class Account {
|
||||
withdraw(amount: Money): WithdrawResult {
|
||||
if (!this.canWithdraw(amount)) {
|
||||
return WithdrawResult.insufficientFunds();
|
||||
}
|
||||
this.balance = this.balance.subtract(amount);
|
||||
return WithdrawResult.success();
|
||||
}
|
||||
}
|
||||
|
||||
// Caller tells, object decides
|
||||
const result = account.withdraw(amount);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Comments
|
||||
|
||||
### When to Write Comments
|
||||
|
||||
**Only write comments to explain WHY, not WHAT or HOW.**
|
||||
|
||||
Code explains what and how. Comments explain business reasons, non-obvious decisions, or warnings.
|
||||
|
||||
```typescript
|
||||
// BAD: Explains what (redundant)
|
||||
// Add 1 to counter
|
||||
counter++;
|
||||
|
||||
// GOOD: Explains why
|
||||
// Compensate for 0-based indexing in legacy API
|
||||
counter++;
|
||||
```
|
||||
|
||||
### Prefer Self-Documenting Code
|
||||
|
||||
Instead of commenting, rename to make intent clear.
|
||||
|
||||
```typescript
|
||||
// BAD: Comment needed
|
||||
// Check if user can access premium features
|
||||
if (user.subscriptionLevel >= 2 && !user.isBanned) { }
|
||||
|
||||
// GOOD: Self-documenting
|
||||
if (user.canAccessPremiumFeatures()) { }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Formatting
|
||||
|
||||
### Vertical Spacing
|
||||
- Related code together
|
||||
- Blank lines between concepts
|
||||
- Most important/public at top
|
||||
|
||||
### Horizontal Spacing
|
||||
- Consistent indentation
|
||||
- Space around operators
|
||||
- Max line length ~80-120 characters
|
||||
|
||||
### Storytelling
|
||||
Code should read top-to-bottom like a story. High-level at top, details below.
|
||||
|
||||
```typescript
|
||||
class OrderProcessor {
|
||||
// Public API first
|
||||
process(order: Order): ProcessResult {
|
||||
this.validate(order);
|
||||
this.calculateTotals(order);
|
||||
return this.save(order);
|
||||
}
|
||||
|
||||
// Supporting methods below, in order of appearance
|
||||
private validate(order: Order): void { ... }
|
||||
private calculateTotals(order: Order): void { ... }
|
||||
private save(order: Order): ProcessResult { ... }
|
||||
}
|
||||
```
|
||||
334
clean-code/references/code-smells.md
Normal file
334
clean-code/references/code-smells.md
Normal file
@@ -0,0 +1,334 @@
|
||||
# Code Smells & Anti-Patterns
|
||||
|
||||
## What Are Code Smells?
|
||||
|
||||
Indicators that something MAY be wrong. Not bugs, but design problems that make code hard to understand, change, or test.
|
||||
|
||||
## The Five Categories
|
||||
|
||||
### 1. Bloaters
|
||||
Code that has grown too large.
|
||||
|
||||
| Smell | Symptom | Refactoring |
|
||||
|-------|---------|-------------|
|
||||
| **Long Method** | > 10 lines | Extract Method |
|
||||
| **Large Class** | > 50 lines, multiple responsibilities | Extract Class |
|
||||
| **Long Parameter List** | > 3 parameters | Introduce Parameter Object |
|
||||
| **Data Clumps** | Same group of variables appear together | Extract Class |
|
||||
| **Primitive Obsession** | Primitives instead of small objects | Wrap in Value Object |
|
||||
|
||||
### 2. Object-Orientation Abusers
|
||||
Misuse of OO principles.
|
||||
|
||||
| Smell | Symptom | Refactoring |
|
||||
|-------|---------|-------------|
|
||||
| **Switch Statements** | Type checking, large switch/if-else | Replace with Polymorphism |
|
||||
| **Parallel Inheritance** | Adding subclass requires adding another | Merge Hierarchies |
|
||||
| **Refused Bequest** | Subclass doesn't use parent methods | Replace Inheritance with Delegation |
|
||||
| **Alternative Classes** | Different interfaces, same concept | Rename, Extract Superclass |
|
||||
|
||||
### 3. Change Preventers
|
||||
Code that makes changes difficult.
|
||||
|
||||
| Smell | Symptom | Refactoring |
|
||||
|-------|---------|-------------|
|
||||
| **Divergent Change** | One class changed for many reasons | Extract Class (SRP) |
|
||||
| **Shotgun Surgery** | One change touches many classes | Move Method/Field together |
|
||||
| **Parallel Inheritance** | (see above) | Merge Hierarchies |
|
||||
|
||||
### 4. Dispensables
|
||||
Code that can be removed.
|
||||
|
||||
| Smell | Symptom | Refactoring |
|
||||
|-------|---------|-------------|
|
||||
| **Comments** | Explaining bad code | Rename, Extract Method |
|
||||
| **Duplicate Code** | Copy-paste | Extract Method, Pull Up Method |
|
||||
| **Dead Code** | Unreachable code | Delete |
|
||||
| **Speculative Generality** | "Just in case" code | Delete (YAGNI) |
|
||||
| **Lazy Class** | Class that does almost nothing | Inline Class |
|
||||
|
||||
### 5. Couplers
|
||||
Excessive coupling between classes.
|
||||
|
||||
| Smell | Symptom | Refactoring |
|
||||
|-------|---------|-------------|
|
||||
| **Feature Envy** | Method uses another class's data extensively | Move Method |
|
||||
| **Inappropriate Intimacy** | Classes know too much about each other | Move Method, Extract Class |
|
||||
| **Message Chains** | `a.getB().getC().getD()` | Hide Delegate |
|
||||
| **Middle Man** | Class only delegates | Inline Class |
|
||||
|
||||
---
|
||||
|
||||
## The Seven Most Common Code Smells
|
||||
|
||||
### 1. Long Method
|
||||
|
||||
**Symptom:** Method > 10 lines, doing multiple things.
|
||||
|
||||
```typescript
|
||||
// SMELL
|
||||
function processOrder(order: Order) {
|
||||
// Validate
|
||||
if (!order.items.length) throw new Error('Empty');
|
||||
if (!order.customer) throw new Error('No customer');
|
||||
|
||||
// Calculate
|
||||
let total = 0;
|
||||
for (const item of order.items) {
|
||||
total += item.price * item.quantity;
|
||||
if (item.discount) {
|
||||
total -= item.discount;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply tax
|
||||
const taxRate = getTaxRate(order.customer.state);
|
||||
total = total * (1 + taxRate);
|
||||
|
||||
// Save
|
||||
db.orders.insert({ ...order, total });
|
||||
|
||||
// Notify
|
||||
emailService.send(order.customer.email, 'Order confirmed');
|
||||
}
|
||||
|
||||
// REFACTORED
|
||||
function processOrder(order: Order) {
|
||||
validateOrder(order);
|
||||
const total = calculateTotal(order);
|
||||
saveOrder(order, total);
|
||||
notifyCustomer(order);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Large Class
|
||||
|
||||
**Symptom:** Class with many responsibilities, > 50 lines.
|
||||
|
||||
```typescript
|
||||
// SMELL: God class
|
||||
class User {
|
||||
// User data
|
||||
name: string;
|
||||
email: string;
|
||||
|
||||
// Authentication
|
||||
login() { }
|
||||
logout() { }
|
||||
resetPassword() { }
|
||||
|
||||
// Preferences
|
||||
setTheme() { }
|
||||
setLanguage() { }
|
||||
|
||||
// Notifications
|
||||
sendEmail() { }
|
||||
sendSMS() { }
|
||||
|
||||
// Billing
|
||||
charge() { }
|
||||
refund() { }
|
||||
}
|
||||
|
||||
// REFACTORED: Separate classes
|
||||
class User { name: string; email: string; }
|
||||
class AuthService { login(); logout(); resetPassword(); }
|
||||
class UserPreferences { setTheme(); setLanguage(); }
|
||||
class NotificationService { sendEmail(); sendSMS(); }
|
||||
class BillingService { charge(); refund(); }
|
||||
```
|
||||
|
||||
### 3. Feature Envy
|
||||
|
||||
**Symptom:** Method uses another class's data more than its own.
|
||||
|
||||
```typescript
|
||||
// SMELL: Order envies Customer
|
||||
class Order {
|
||||
calculateShipping(customer: Customer): number {
|
||||
if (customer.country === 'US') {
|
||||
if (customer.state === 'CA') return 10;
|
||||
return 15;
|
||||
}
|
||||
return 25;
|
||||
}
|
||||
}
|
||||
|
||||
// REFACTORED: Move to Customer
|
||||
class Customer {
|
||||
getShippingCost(): number {
|
||||
if (this.country === 'US') {
|
||||
if (this.state === 'CA') return 10;
|
||||
return 15;
|
||||
}
|
||||
return 25;
|
||||
}
|
||||
}
|
||||
|
||||
class Order {
|
||||
calculateShipping(): number {
|
||||
return this.customer.getShippingCost();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Primitive Obsession
|
||||
|
||||
**Symptom:** Using primitives for domain concepts.
|
||||
|
||||
```typescript
|
||||
// SMELL
|
||||
function createUser(email: string, age: number, zipCode: string) {
|
||||
// No validation, easy to pass wrong values
|
||||
if (!email.includes('@')) throw new Error();
|
||||
if (age < 0) throw new Error();
|
||||
}
|
||||
|
||||
// REFACTORED: Value objects
|
||||
class Email {
|
||||
constructor(private value: string) {
|
||||
if (!value.includes('@')) throw new InvalidEmail();
|
||||
}
|
||||
}
|
||||
|
||||
class Age {
|
||||
constructor(private value: number) {
|
||||
if (value < 0 || value > 150) throw new InvalidAge();
|
||||
}
|
||||
}
|
||||
|
||||
function createUser(email: Email, age: Age, address: Address) {
|
||||
// Type system prevents invalid data
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Switch Statements
|
||||
|
||||
**Symptom:** Switching on type, repeated across codebase.
|
||||
|
||||
```typescript
|
||||
// SMELL
|
||||
function getArea(shape: Shape): number {
|
||||
switch (shape.type) {
|
||||
case 'circle': return Math.PI * shape.radius ** 2;
|
||||
case 'rectangle': return shape.width * shape.height;
|
||||
case 'triangle': return 0.5 * shape.base * shape.height;
|
||||
}
|
||||
}
|
||||
|
||||
function getPerimeter(shape: Shape): number {
|
||||
switch (shape.type) { // Same switch again!
|
||||
case 'circle': return 2 * Math.PI * shape.radius;
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// REFACTORED: Polymorphism
|
||||
interface Shape {
|
||||
getArea(): number;
|
||||
getPerimeter(): number;
|
||||
}
|
||||
|
||||
class Circle implements Shape {
|
||||
constructor(private radius: number) {}
|
||||
getArea(): number { return Math.PI * this.radius ** 2; }
|
||||
getPerimeter(): number { return 2 * Math.PI * this.radius; }
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Inappropriate Intimacy
|
||||
|
||||
**Symptom:** Classes know too much about each other's internals.
|
||||
|
||||
```typescript
|
||||
// SMELL
|
||||
class Order {
|
||||
process() {
|
||||
const inventory = new Inventory();
|
||||
// Reaching into inventory's internals
|
||||
for (const item of this.items) {
|
||||
const stock = inventory.stockLevels[item.sku];
|
||||
if (stock.quantity < item.quantity) {
|
||||
throw new Error('Out of stock');
|
||||
}
|
||||
inventory.stockLevels[item.sku].quantity -= item.quantity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// REFACTORED: Tell, don't ask
|
||||
class Inventory {
|
||||
reserve(items: OrderItem[]): ReserveResult {
|
||||
// Inventory manages its own state
|
||||
for (const item of items) {
|
||||
if (!this.canReserve(item)) {
|
||||
return ReserveResult.outOfStock(item);
|
||||
}
|
||||
}
|
||||
this.deductStock(items);
|
||||
return ReserveResult.success();
|
||||
}
|
||||
}
|
||||
|
||||
class Order {
|
||||
process(inventory: Inventory) {
|
||||
const result = inventory.reserve(this.items);
|
||||
if (!result.isSuccess()) {
|
||||
throw new OutOfStockError(result.failedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Speculative Generality
|
||||
|
||||
**Symptom:** "Just in case" abstractions that aren't used.
|
||||
|
||||
```typescript
|
||||
// SMELL: Over-engineered for hypothetical needs
|
||||
interface PaymentProcessor {
|
||||
process(): void;
|
||||
rollback(): void;
|
||||
audit(): void;
|
||||
generateReport(): void;
|
||||
scheduleRecurring(): void;
|
||||
}
|
||||
|
||||
class StripeProcessor implements PaymentProcessor {
|
||||
process() { /* actual code */ }
|
||||
rollback() { throw new Error('Not implemented'); }
|
||||
audit() { throw new Error('Not implemented'); }
|
||||
generateReport() { throw new Error('Not implemented'); }
|
||||
scheduleRecurring() { throw new Error('Not implemented'); }
|
||||
}
|
||||
|
||||
// REFACTORED: YAGNI
|
||||
interface PaymentProcessor {
|
||||
process(): void;
|
||||
}
|
||||
|
||||
class StripeProcessor implements PaymentProcessor {
|
||||
process() { /* actual code */ }
|
||||
}
|
||||
// Add other methods when actually needed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prevention Strategies
|
||||
|
||||
1. **Follow Object Calisthenics** - Rules prevent most smells
|
||||
2. **Practice TDD** - Tests reveal design problems early
|
||||
3. **Review in pairs** - Fresh eyes catch smells
|
||||
4. **Refactor continuously** - Don't let smells accumulate
|
||||
5. **Apply SOLID** - Prevents structural smells
|
||||
6. **Use static analysis** - Tools catch common issues
|
||||
|
||||
---
|
||||
|
||||
## When You Find a Smell
|
||||
|
||||
1. **Confirm it's a problem** - Not all smells need fixing
|
||||
2. **Ensure test coverage** - Before refactoring
|
||||
3. **Refactor in small steps** - Keep tests passing
|
||||
4. **Commit frequently** - Easy to revert if needed
|
||||
Reference in New Issue
Block a user