Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:44:41 +08:00
commit 785c24bc1c
22 changed files with 5497 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
## Software Design Principles Skill
Professional object-oriented design patterns and principles for maintainable, well-structured code.
## What This Skill Provides
Comprehensive design guidance including:
- **Object Calisthenics** - 9 rules for clean OO code
- **Feature Envy Detection** - Identifying and fixing misplaced methods
- **Dependency Inversion** - Injection over instantiation
- **Fail-Fast Error Handling** - Explicit validation over silent fallbacks
- **Intention-Revealing Naming** - Domain language over generic terms (no data/utils/helpers)
- **Type-Driven Design** - Making illegal states unrepresentable
## When to Use
### Auto-Activation
Activates during code refactoring, design reviews, or architecture discussions.
### Explicit Activation
- "Review this code's design"
- "Check for feature envy"
- "Apply object calisthenics"
- "Improve naming in this file"
## Key Principles
### Object Calisthenics (9 Rules)
1. One level of indentation per method
2. Don't use ELSE keyword
3. Wrap all primitives and strings
4. First class collections
5. One dot per line
6. Don't abbreviate
7. Keep all entities small
8. No more than two instance variables
9. No getters/setters/properties
### Dependency Inversion
`const service = new Service()` (tight coupling)
`this.service.doWork()` (injected dependency)
### Fail-Fast
`value ?? backup ?? 'unknown'` (silent fallback)
`if (!value) throw new Error(...)` (explicit validation)
### Naming
`data`, `utils`, `helpers`, `manager`, `processor`
✓ Use domain-specific, intention-revealing names
## Integration
### With TDD Process
Automatically applied during **REFACTOR** state:
- Checks object calisthenics compliance
- Detects feature envy
- Verifies dependency injection
- Validates naming conventions
TDD Process rules that implement these principles:
- **Rule #8**: Fail-fast error handling
- **Rule #9**: Dependency inversion
### Standalone
Use for design reviews, refactoring sessions, or architecture planning without TDD workflow.
## Example: Before and After
### Before (Violates Principles)
```typescript
// Feature envy, generic names, hard dependencies, silent fallbacks
class DataProcessor {
process(data: any): any {
const validator = new Validator() // Hard dependency
const result = data.value ?? 'unknown' // Silent fallback
return validator.process(result) // Feature envy
}
}
```
### After (Follows Principles)
```typescript
// Clear responsibilities, domain names, injected dependencies, fail-fast
class OrderTotalCalculator {
constructor(private taxCalculator: TaxCalculator) {} // Injected
calculateTotal(order: Order): Money {
if (!order.subtotal) {
throw new Error(
`Expected order.subtotal to exist, got ${order.subtotal}. ` +
`Order ID: ${order.id}`
)
}
return this.taxCalculator.applyTax(
order.subtotal,
order.taxRate
)
}
}
```
## Checklist
When reviewing code, verify:
- [ ] Object calisthenics: Code follows 9 rules
- [ ] Feature envy: Methods in correct classes
- [ ] Dependencies: Injected, not instantiated
- [ ] Errors: Fail-fast with clear messages
- [ ] Naming: Intention-revealing, domain-specific
- [ ] Types: Illegal states impossible, no `any`
## When NOT to Apply
Some exceptions are acceptable:
- Value objects/DTOs may have multiple fields
- Simple scripts don't need full dependency injection
- Configuration objects can have getters
- Test code can be less strict
- Library integration may need type assertions
Use judgment - principles serve quality, not dogma.
## Directory Structure
```
software-design-principles/
├── SKILL.md # Complete design principles guide
└── README.md # This file
```
## Installation
Symlink to Claude skills directory:
```bash
ln -s /path/to/claude-skillz/software-design-principles ~/.claude/skills/software-design-principles
```
## Version
1.0.0

View File

@@ -0,0 +1,630 @@
---
name: Software Design Principles
description: "Object-oriented design principles including object calisthenics, dependency inversion, fail-fast error handling, feature envy detection, and intention-revealing naming. Activates during code refactoring, design reviews, or when user requests design improvements."
version: 1.0.0
---
# Software Design Principles
Professional software design patterns and principles for writing maintainable, well-structured code.
## Critical Rules
🚨 **Fail-fast over silent fallbacks.** Never use fallback chains (`value ?? backup ?? 'unknown'`). If data should exist, validate and throw a clear error.
🚨 **No `any`. No `as`.** Type escape hatches defeat TypeScript's purpose. There's always a type-safe solution.
🚨 **Make illegal states unrepresentable.** Use discriminated unions, not optional fields. If a state combination shouldn't exist, make the type system forbid it.
🚨 **Inject dependencies, don't instantiate.** No `new SomeService()` inside methods. Pass dependencies through constructors.
🚨 **Intention-revealing names only.** Never use `data`, `utils`, `helpers`, `handler`, `processor`. Name things for what they do in the domain.
🚨 **No code comments.** Comments are a failure to express intent in code. If you need a comment to explain what code does, the code isn't clear enough—refactor it.
## When This Applies
- Writing new code (these are defaults, not just refactoring goals)
- Refactoring existing code
- Code reviews and design reviews
- During TDD REFACTOR phase
- When analyzing coupling and cohesion
## Core Philosophy
Well-designed, maintainable code is far more important than getting things done quickly. Every design decision should favor:
- **Clarity over cleverness**
- **Explicit over implicit**
- **Fail-fast over silent fallbacks**
- **Loose coupling over tight integration**
- **Intention-revealing over generic**
## Code Without Comments
**Principle:** Never write code comments - prefer to write expressive code.
Well-named functions, classes, and variables should make the code self-documenting. Comments are often a sign that the code isn't clear enough.
**Instead of comments, use:**
- Intention-revealing names
- Small, focused functions
- Clear structure
- Domain language
**Bad (needs comments):**
```typescript
// Check if user can access resource
function check(u: User, r: Resource): boolean {
// Admin users bypass all checks
if (u.role === 'admin') return true
// Check ownership
if (r.ownerId === u.id) return true
// Check shared access
return r.sharedWith.includes(u.id)
}
```
**Good (self-documenting):**
```typescript
function canUserAccessResource(user: User, resource: Resource): boolean {
if (user.isAdmin()) return true
if (resource.isOwnedBy(user)) return true
return resource.isSharedWith(user)
}
```
The code reads like prose - no comments needed.
## Object Calisthenics
Apply object calisthenics strictly to all code. Refactor existing code to comply with these principles:
### The Nine Rules
1. **One level of indentation per method**
- Improves readability
- Forces extraction of helper methods
- Makes code easier to test
2. **Don't use the ELSE keyword**
- Use early returns instead
- Reduces nesting
- Clarifies happy path
3. **Wrap all primitives and strings**
- Create value objects
- Encapsulate validation logic
- Make domain concepts explicit
4. **First class collections**
- Classes with collections should contain nothing else
- Enables collection-specific operations
- Improves cohesion
5. **One dot per line**
- Reduces coupling
- Prevents feature envy
- Honors Law of Demeter
6. **Don't abbreviate**
- Use full, descriptive names
- Code is read more than written
- Self-documenting code reduces comments
7. **Keep all entities small**
- Small classes (< 150 lines)
- Small methods (< 10 lines)
- Small packages/modules
- Easier to understand and maintain
8. **No classes with more than two instance variables**
- High cohesion
- Clear single responsibility
- Easier to test
9. **No getters/setters/properties**
- Tell, don't ask
- Objects should do work, not expose data
- Prevents feature envy
### When to Apply
**During refactoring:**
- Review code against all nine rules
- Identify violations
- Refactor to comply
- Verify tests still pass after each change
**During code review:**
- Check new code complies with calisthenics
- Suggest refactorings for violations
- Explain rationale using domain language
## Feature Envy Detection
**What is Feature Envy?**
When a method in one class is more interested in the data or behavior of another class than its own.
### Warning Signs
```typescript
// FEATURE ENVY - method is obsessed with Order's data
class InvoiceGenerator {
generate(order: Order): Invoice {
const total = order.getItems()
.map(item => item.getPrice() * item.getQuantity())
.reduce((sum, val) => sum + val, 0)
const tax = total * order.getTaxRate()
const shipping = order.calculateShipping()
return new Invoice(total + tax + shipping)
}
}
```
This method should live in the `Order` class - it's using Order's data extensively.
### How to Fix
**Move the method to the class it envies:**
```typescript
class Order {
calculateTotal(): number {
const subtotal = this.items
.map(item => item.getPrice() * item.getQuantity())
.reduce((sum, val) => sum + val, 0)
const tax = subtotal * this.taxRate
const shipping = this.calculateShipping()
return subtotal + tax + shipping
}
}
class InvoiceGenerator {
generate(order: Order): Invoice {
return new Invoice(order.calculateTotal())
}
}
```
### Detection Protocol
During refactoring:
1. For each method, count references to external objects vs own object
2. If external references > own references → likely feature envy
3. Consider moving method to the envied class
4. Re-run tests to verify behavior preserved
## Dependency Inversion Principle
**Principle:** Depend on abstractions, not concretions. Do not directly instantiate dependencies within methods.
### The Problem: Hard Dependencies
```typescript
// TIGHT COUPLING - hard dependency
class OrderProcessor {
process(order: Order): void {
const validator = new OrderValidator() // ❌ Direct instantiation
if (!validator.isValid(order)) {
throw new Error('Invalid order')
}
const emailer = new EmailService() // ❌ Direct instantiation
emailer.send(order.customerEmail, 'Order confirmed')
}
}
```
**Issues:**
- Hard to test (can't mock dependencies)
- Hard to change (coupled to concrete implementations)
- Hidden dependencies (not visible in constructor)
- Violates Single Responsibility (creates AND uses)
### The Solution: Dependency Inversion
```typescript
// LOOSE COUPLING - dependencies injected
class OrderProcessor {
constructor(
private validator: OrderValidator,
private emailer: EmailService
) {}
process(order: Order): void {
if (!this.validator.isValid(order)) {
throw new Error('Invalid order')
}
this.emailer.send(order.customerEmail, 'Order confirmed')
}
}
```
**Benefits:**
- Easy to test (inject mocks)
- Easy to change (swap implementations)
- Explicit dependencies (visible in constructor)
- Single Responsibility (only uses, doesn't create)
### Application Rules
**NEVER do this:**
```typescript
const service = new SomeService() // ❌ Direct instantiation
const result = SomeUtil.staticMethod() // ❌ Static method call
```
**ALWAYS do this:**
```typescript
this.service.doWork() // ✓ Use injected dependency
this.util.calculate() // ✓ Delegate to dependency
```
### Enforcement
During refactoring:
- Scan for `new` keywords inside methods (except value objects)
- Scan for static method calls to other classes
- Extract to constructor parameters or method parameters
- Verify tests pass after injection
## Fail-Fast Error Handling
**Principle:** Do not fall back to whatever data is available when expected data is missing. FAIL FAST with clear errors.
### The Problem: Silent Fallbacks
```typescript
// SILENT FAILURE - hides the problem
function extractName(content: Content): string {
return content.eventType ?? content.className ?? 'Unknown'
}
```
**Issues:**
- You never know that `eventType` is missing
- 'Unknown' propagates through system
- Hard to debug when it causes issues later
- Masks real problems
### The Solution: Explicit Validation
```typescript
// FAIL FAST - immediate, clear error
function extractName(content: Content): string {
if (!content.eventType) {
throw new Error(
`Expected 'eventType' to exist in content, but it was not found. ` +
`Content keys: [${Object.keys(content).join(', ')}]`
)
}
return content.eventType
}
```
**Benefits:**
- Fails immediately at the source
- Clear error message shows exactly what's wrong
- Easy to debug (stack trace points to problem)
- Forces fixing the root cause
### Application Rules
**NEVER use fallback chains:**
```typescript
value ?? backup ?? default ?? 'unknown' // ❌
```
**ALWAYS validate and fail explicitly:**
```typescript
if (!value) {
throw new Error(`Expected value, got ${value}. Context: ${debug}`)
}
return value // ✓
```
### When to Apply
**During implementation:**
- When accessing data that "should" exist
- When preconditions must be met
- When invariants must hold
**Error message format:**
```typescript
throw new Error(
`Expected [what you expected]. ` +
`Got [what you actually got]. ` +
`Context: [helpful debugging info like available keys, current state, etc.]`
)
```
## Naming Conventions
**Principle:** Use business domain terminology and intention-revealing names. Never use generic programmer jargon.
### Forbidden Generic Names
**NEVER use these names:**
- `data`
- `utils`
- `helpers`
- `common`
- `shared`
- `manager`
- `handler`
- `processor`
These names are meaningless - they tell you nothing about what the code actually does.
### Intention-Revealing Names
**Instead of generic names, use specific domain language:**
```typescript
// ❌ GENERIC - meaningless
class DataProcessor {
processData(data: any): any {
const utils = new DataUtils()
return utils.transform(data)
}
}
// ✓ INTENTION-REVEALING - clear purpose
class OrderTotalCalculator {
calculateTotal(order: Order): Money {
const taxCalculator = new TaxCalculator()
return taxCalculator.applyTax(order.subtotal, order.taxRate)
}
}
```
### Naming Checklist
**For classes:**
- Does the name reveal what the class is responsible for?
- Is it a noun (or noun phrase) from the domain?
- Would a domain expert recognize this term?
**For methods:**
- Does the name reveal what the method does?
- Is it a verb (or verb phrase)?
- Does it describe the business operation?
**For variables:**
- Does the name reveal what the variable contains?
- Is it specific to this context?
- Could someone understand it without reading the code?
### Refactoring Generic Names
When you encounter generic names:
1. **Understand the purpose**: What is this really doing?
2. **Ask domain experts**: What would they call this?
3. **Extract domain concept**: Is there a domain term for this?
4. **Rename comprehensively**: Update all references
5. **Verify tests**: Ensure behavior unchanged
### Examples
```typescript
// ❌ GENERIC
function getData(id: string): any
const userHelpers = new UserHelpers()
const result = processData(input)
// ✓ SPECIFIC
function findCustomerById(customerId: string): Customer
const addressValidator = new AddressValidator()
const confirmedOrder = confirmOrder(pendingOrder)
```
## Type-Driven Design
**Principle:** Follow Scott Wlaschin's type-driven approach to domain modeling. Express domain concepts using the type system.
### Make Illegal States Unrepresentable
Use types to encode business rules:
```typescript
// ❌ PRIMITIVE OBSESSION - illegal states possible
interface Order {
status: string // Could be any string
shippedDate: Date | null // Could be set when status != 'shipped'
}
// ✓ TYPE-SAFE - illegal states impossible
type UnconfirmedOrder = { type: 'unconfirmed', items: Item[] }
type ConfirmedOrder = { type: 'confirmed', items: Item[], confirmationNumber: string }
type ShippedOrder = { type: 'shipped', items: Item[], confirmationNumber: string, shippedDate: Date }
type Order = UnconfirmedOrder | ConfirmedOrder | ShippedOrder
```
### Avoid Type Escape Hatches
**STRICTLY FORBIDDEN without explicit user approval:**
- `any` type
- `as` type assertions (`as unknown as`, `as any`, `as SomeType`)
- `@ts-ignore` / `@ts-expect-error`
**Before using these, you MUST get user approval.**
There is always a better type-safe solution. These make code unsafe and defeat TypeScript's purpose.
### Use the Type System for Validation
```typescript
// ✓ TYPE-SAFE - validates at compile time
type PositiveNumber = number & { __brand: 'positive' }
function createPositive(value: number): PositiveNumber {
if (value <= 0) {
throw new Error(`Expected positive number, got ${value}`)
}
return value as PositiveNumber
}
// Can only be called with validated positive numbers
function calculateDiscount(price: PositiveNumber, rate: number): Money {
// price is guaranteed positive by type system
}
```
## Prefer Immutability
**Principle:** Default to immutable data. Mutation is a source of bugs—unexpected changes, race conditions, and difficult debugging.
### The Problem: Mutable State
```typescript
// MUTABLE - hard to reason about
function processOrder(order: Order): void {
order.status = 'processing' // Mutates input!
order.items.push(freeGift) // Side effect!
}
// Caller has no idea their object changed
const myOrder = getOrder()
processOrder(myOrder)
// myOrder is now different - surprise!
```
### The Solution: Return New Values
```typescript
// IMMUTABLE - predictable
function processOrder(order: Order): Order {
return {
...order,
status: 'processing',
items: [...order.items, freeGift]
}
}
// Caller controls what happens
const myOrder = getOrder()
const processedOrder = processOrder(myOrder)
// myOrder unchanged, processedOrder is new
```
### Application Rules
- Prefer `const` over `let`
- Prefer spread (`...`) over mutation
- Prefer `map`/`filter`/`reduce` over `forEach` with mutation
- If you must mutate, make it explicit and contained
## YAGNI - You Aren't Gonna Need It
**Principle:** Don't build features until they're actually needed. Speculative code is waste—it costs time to write, time to maintain, and is often wrong when requirements become clear.
### The Problem: Speculative Generalization
```typescript
// YAGNI VIOLATION - over-engineered for "future" needs
interface PaymentProcessor {
process(payment: Payment): Result
refund(payment: Payment): Result
partialRefund(payment: Payment, amount: Money): Result
schedulePayment(payment: Payment, date: Date): Result
recurringPayment(payment: Payment, schedule: Schedule): Result
// ... 10 more methods "we might need"
}
// Only ONE method is actually used today
```
### The Solution: Build What You Need Now
```typescript
// YAGNI RESPECTED - minimal interface for current needs
interface PaymentProcessor {
process(payment: Payment): Result
}
// Add refund() when you actually need refunds
// Add scheduling when you actually need scheduling
// Not before
```
### Application Rules
- Build the simplest thing that works
- Add capabilities when requirements demand them, not before
- "But we might need it" is not a requirement
- Unused code is a maintenance burden and a lie about the system
- Delete speculative code ruthlessly
## Integration with Other Skills
### With TDD Process
This skill is automatically applied during the **REFACTOR** state of the TDD process:
**TDD REFACTOR state post-conditions check:**
- ✓ Object calisthenics applied
- ✓ No feature envy
- ✓ Dependencies inverted
- ✓ Names are intention-revealing
**TDD Rule #8** enforces fail-fast error handling
**TDD Rule #9** enforces dependency inversion
### Standalone Usage
Activate when:
- Reviewing code for design quality
- Refactoring existing code
- Analyzing coupling and cohesion
- Planning architecture changes
## When Tempted to Cut Corners
- If you're about to use a fallback chain (`??` chains): STOP. Silent fallbacks hide bugs. When that `'unknown'` propagates through your system and causes a failure three layers later, you'll spend hours debugging. Fail fast with a clear error now.
- If you're about to use `any` or `as`: STOP. You're lying to the compiler to make an error go away. The error is telling you something—your types are wrong. Fix the types, not the symptoms.
- If you're about to instantiate a dependency inside a method: STOP. You're creating tight coupling that makes testing painful and changes risky. Take 30 seconds to inject it through the constructor.
- If you're about to name something `data`, `utils`, or `handler`: STOP. These names are meaningless. What does it actually DO? Name it for its purpose in the domain. Future you will thank present you.
- If you're about to add a getter just to access data: STOP. Ask why the caller needs that data. Can the object do the work instead? Tell, don't ask.
- If you're about to skip the refactor because "it works": STOP. Working code that's hard to change is technical debt. The refactor IS part of the work, not optional polish.
- If you're about to write a comment explaining code: STOP. Comments rot—they become lies as code changes. Instead, extract a well-named function, rename a variable, or restructure the code. If the code needs explanation, the code is the problem.
- If you're about to mutate a parameter: STOP. Return a new value instead. Mutation creates invisible dependencies—the caller doesn't know their data changed. Make data flow explicit.
- If you're about to build something "we might need later": STOP. You're guessing at future requirements, and you're probably wrong. Build what you need now. Add capabilities when they're actually required.
## When NOT to Apply
**Don't over-apply these principles:**
- **Value objects and DTOs** may violate object calisthenics (that's okay)
- **Simple scripts** don't need dependency inversion
- **Configuration objects** can have getters (they're data, not behavior)
- **Test code** can be less strict about calisthenics
- **Library integration code** may need type assertions
**Use judgment - principles serve code quality, not vice versa.**
## Summary Checklist
When refactoring or reviewing code, check:
- [ ] Object calisthenics: Does code follow the 9 rules?
- [ ] Feature envy: Do methods belong in the right classes?
- [ ] Dependencies: Are they injected, not instantiated?
- [ ] Error handling: Does code fail-fast with clear errors?
- [ ] Naming: Are names intention-revealing (no data/utils/helpers)?
- [ ] Types: Do types make illegal states impossible?
- [ ] Type safety: No `any`, No `as` assertions?