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:
328
solid/references/object-design.md
Normal file
328
solid/references/object-design.md
Normal file
@@ -0,0 +1,328 @@
|
||||
# Object-Oriented Design
|
||||
|
||||
## Responsibility-Driven Design (RDD)
|
||||
|
||||
The key insight: **Objects are defined by their responsibilities, not their data.**
|
||||
|
||||
### Finding Objects
|
||||
|
||||
Start with:
|
||||
1. **Nouns** in requirements → candidate objects
|
||||
2. **Verbs** → candidate methods/behaviors
|
||||
3. **Domain concepts** → value objects
|
||||
|
||||
### Finding Responsibilities
|
||||
|
||||
Each object should answer:
|
||||
- What does this object **know**?
|
||||
- What does this object **do**?
|
||||
- What does this object **decide**?
|
||||
|
||||
### Object Stereotypes
|
||||
|
||||
Every class fits one (or maybe two) stereotypes:
|
||||
|
||||
| Stereotype | Purpose | Example |
|
||||
|------------|---------|---------|
|
||||
| **Information Holder** | Knows things, holds data | `User`, `Product`, `Address` |
|
||||
| **Structurer** | Maintains relationships | `OrderItems`, `UserGroup` |
|
||||
| **Service Provider** | Performs work | `PaymentProcessor`, `EmailSender` |
|
||||
| **Coordinator** | Orchestrates workflow | `OrderFulfillmentService` |
|
||||
| **Controller** | Makes decisions, delegates | `CheckoutController` |
|
||||
| **Interfacer** | Transforms between systems | `UserAPIAdapter`, `DatabaseMapper` |
|
||||
|
||||
### The Two Questions
|
||||
|
||||
For every class, ask:
|
||||
1. **"What pattern is this?"** - Which stereotype? Which design pattern?
|
||||
2. **"Is it doing too much?"** - Check object calisthenics rules
|
||||
|
||||
If you can't answer clearly, the class needs refactoring.
|
||||
|
||||
---
|
||||
|
||||
## Tell, Don't Ask
|
||||
|
||||
**Command objects to do work. Don't interrogate them and do the work yourself.**
|
||||
|
||||
```typescript
|
||||
// BAD: Asking, then doing
|
||||
if (account.getBalance() >= amount) {
|
||||
account.setBalance(account.getBalance() - amount);
|
||||
// more logic here...
|
||||
}
|
||||
|
||||
// GOOD: Telling
|
||||
const result = account.withdraw(amount);
|
||||
if (result.isSuccess()) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The object that has the data should have the behavior.
|
||||
|
||||
---
|
||||
|
||||
## Design by Contract (DbC)
|
||||
|
||||
Every method has:
|
||||
- **Preconditions** - What must be true BEFORE calling
|
||||
- **Postconditions** - What will be true AFTER calling
|
||||
- **Invariants** - What is ALWAYS true about the object
|
||||
|
||||
```typescript
|
||||
class BankAccount {
|
||||
private balance: Money;
|
||||
|
||||
// INVARIANT: balance is never negative
|
||||
|
||||
// PRECONDITION: amount > 0
|
||||
// POSTCONDITION: balance decreased by amount OR error returned
|
||||
withdraw(amount: Money): WithdrawResult {
|
||||
if (amount.isNegativeOrZero()) {
|
||||
return WithdrawResult.invalidAmount();
|
||||
}
|
||||
|
||||
if (this.balance.isLessThan(amount)) {
|
||||
return WithdrawResult.insufficientFunds();
|
||||
}
|
||||
|
||||
this.balance = this.balance.minus(amount);
|
||||
return WithdrawResult.success(this.balance);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Composition Over Inheritance
|
||||
|
||||
**Prefer composing objects over extending classes.**
|
||||
|
||||
### Why Inheritance is Problematic:
|
||||
- Tight coupling between parent and child
|
||||
- Fragile base class problem
|
||||
- Difficult to change parent without breaking children
|
||||
- Forces "is-a" relationship that may not fit
|
||||
|
||||
### When to Use Inheritance:
|
||||
- True "is-a" relationship (rare)
|
||||
- Framework requirements
|
||||
- Template Method pattern (intentional)
|
||||
|
||||
### Prefer Composition:
|
||||
```typescript
|
||||
// BAD: Inheritance
|
||||
class PremiumUser extends User {
|
||||
getDiscount(): number { return 20; }
|
||||
}
|
||||
|
||||
// GOOD: Composition
|
||||
class User {
|
||||
constructor(private discountPolicy: DiscountPolicy) {}
|
||||
|
||||
getDiscount(): number {
|
||||
return this.discountPolicy.calculate();
|
||||
}
|
||||
}
|
||||
|
||||
// Now discount behavior is pluggable
|
||||
new User(new PremiumDiscount());
|
||||
new User(new StandardDiscount());
|
||||
new User(new NoDiscount());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Law of Demeter (Principle of Least Knowledge)
|
||||
|
||||
**Only talk to your immediate friends.**
|
||||
|
||||
A method should only call:
|
||||
1. Methods on `this`
|
||||
2. Methods on parameters
|
||||
3. Methods on objects it creates
|
||||
4. Methods on its direct components
|
||||
|
||||
```typescript
|
||||
// BAD: Reaching through objects
|
||||
order.getCustomer().getAddress().getCity();
|
||||
|
||||
// GOOD: Ask the immediate friend
|
||||
order.getShippingCity();
|
||||
```
|
||||
|
||||
This reduces coupling - changes to `Address` don't ripple through all callers.
|
||||
|
||||
---
|
||||
|
||||
## Encapsulation
|
||||
|
||||
**Hide internal details, expose behavior.**
|
||||
|
||||
### Levels of Encapsulation:
|
||||
1. **Data** - private fields, no direct access
|
||||
2. **Implementation** - how things work internally
|
||||
3. **Type** - concrete class hidden behind interface
|
||||
4. **Design** - architectural decisions hidden from clients
|
||||
|
||||
```typescript
|
||||
// BAD: Exposed internals
|
||||
class Order {
|
||||
public items: Item[] = [];
|
||||
public total: number = 0;
|
||||
}
|
||||
|
||||
// Client can corrupt state
|
||||
order.items.push(item);
|
||||
order.total = -999; // Oops!
|
||||
|
||||
// GOOD: Encapsulated
|
||||
class Order {
|
||||
private items: OrderItems;
|
||||
private total: Money;
|
||||
|
||||
addItem(item: Item): void {
|
||||
this.items.add(item);
|
||||
this.recalculateTotal();
|
||||
}
|
||||
|
||||
getTotal(): Money {
|
||||
return this.total; // Returns copy or immutable
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Polymorphism
|
||||
|
||||
**Replace conditionals with types.**
|
||||
|
||||
```typescript
|
||||
// BAD: Type checking
|
||||
function calculateShipping(method: string, value: number): number {
|
||||
if (method === 'standard') return value < 50 ? 5 : 0;
|
||||
if (method === 'express') return 15;
|
||||
if (method === 'overnight') return 25;
|
||||
throw new Error('Unknown method');
|
||||
}
|
||||
|
||||
// GOOD: Polymorphism
|
||||
interface ShippingMethod {
|
||||
calculateCost(orderValue: number): number;
|
||||
}
|
||||
|
||||
class StandardShipping implements ShippingMethod {
|
||||
calculateCost(orderValue: number): number {
|
||||
return orderValue < 50 ? 5 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
class ExpressShipping implements ShippingMethod {
|
||||
calculateCost(orderValue: number): number {
|
||||
return 15;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage - no conditionals
|
||||
function calculateShipping(method: ShippingMethod, value: number): number {
|
||||
return method.calculateCost(value);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Value Objects vs Entities
|
||||
|
||||
### Value Objects
|
||||
- Defined by their attributes (no identity)
|
||||
- Immutable
|
||||
- Comparable by value
|
||||
- Examples: `Money`, `Email`, `Address`, `DateRange`
|
||||
|
||||
```typescript
|
||||
class Money {
|
||||
constructor(
|
||||
private readonly amount: number,
|
||||
private readonly currency: string
|
||||
) {}
|
||||
|
||||
equals(other: Money): boolean {
|
||||
return this.amount === other.amount &&
|
||||
this.currency === other.currency;
|
||||
}
|
||||
|
||||
add(other: Money): Money {
|
||||
if (this.currency !== other.currency) {
|
||||
throw new CurrencyMismatch();
|
||||
}
|
||||
return new Money(this.amount + other.amount, this.currency);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Entities
|
||||
- Have identity (survives attribute changes)
|
||||
- Usually mutable (via methods)
|
||||
- Comparable by identity
|
||||
- Examples: `User`, `Order`, `Product`
|
||||
|
||||
```typescript
|
||||
class User {
|
||||
constructor(
|
||||
private readonly id: UserId,
|
||||
private email: Email,
|
||||
private name: Name
|
||||
) {}
|
||||
|
||||
equals(other: User): boolean {
|
||||
return this.id.equals(other.id); // Identity comparison
|
||||
}
|
||||
|
||||
changeEmail(newEmail: Email): void {
|
||||
this.email = newEmail; // Still same user
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Aggregates
|
||||
|
||||
A cluster of objects treated as a single unit for data changes.
|
||||
|
||||
- One object is the **aggregate root** (entry point)
|
||||
- External code only references the root
|
||||
- Root enforces invariants for the entire cluster
|
||||
|
||||
```typescript
|
||||
// Order is the aggregate root
|
||||
class Order {
|
||||
private items: OrderItem[] = [];
|
||||
|
||||
// All access through the root
|
||||
addItem(product: Product, quantity: number): void {
|
||||
const item = new OrderItem(product, quantity);
|
||||
this.items.push(item);
|
||||
this.validateTotal();
|
||||
}
|
||||
|
||||
removeItem(itemId: ItemId): void {
|
||||
this.items = this.items.filter(i => !i.id.equals(itemId));
|
||||
}
|
||||
|
||||
// Root enforces invariants
|
||||
private validateTotal(): void {
|
||||
if (this.calculateTotal().exceeds(MAX_ORDER_VALUE)) {
|
||||
throw new OrderTotalExceeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BAD: Accessing items directly
|
||||
order.items.push(new OrderItem(...)); // Bypasses validation!
|
||||
|
||||
// GOOD: Through the root
|
||||
order.addItem(product, 2); // Validation happens
|
||||
```
|
||||
Reference in New Issue
Block a user