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]
377 lines
7.5 KiB
Markdown
377 lines
7.5 KiB
Markdown
# 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 { ... }
|
|
}
|
|
```
|