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:
318
solid/references/architecture.md
Normal file
318
solid/references/architecture.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# Software Architecture
|
||||
|
||||
## The Goal of Architecture
|
||||
|
||||
Enable the development team to:
|
||||
1. **Add** features with minimal friction
|
||||
2. **Change** existing features safely
|
||||
3. **Remove** features cleanly
|
||||
4. **Test** features in isolation
|
||||
5. **Deploy** independently when possible
|
||||
|
||||
## Architectural Principles
|
||||
|
||||
### 1. Vertical Boundaries (Features/Slices)
|
||||
|
||||
Organize by **feature**, not by technical layer.
|
||||
|
||||
```
|
||||
BAD: Layer-first
|
||||
src/
|
||||
controllers/
|
||||
UserController.ts
|
||||
OrderController.ts
|
||||
services/
|
||||
UserService.ts
|
||||
OrderService.ts
|
||||
repositories/
|
||||
UserRepository.ts
|
||||
OrderRepository.ts
|
||||
|
||||
GOOD: Feature-first
|
||||
src/
|
||||
users/
|
||||
UserController.ts
|
||||
UserService.ts
|
||||
UserRepository.ts
|
||||
orders/
|
||||
OrderController.ts
|
||||
OrderService.ts
|
||||
OrderRepository.ts
|
||||
```
|
||||
|
||||
**Why:** Changes to "users" feature stay in `users/`. High cohesion within features.
|
||||
|
||||
### 2. Horizontal Boundaries (Layers)
|
||||
|
||||
Separate concerns into layers with clear dependencies.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ Presentation │ UI, Controllers, CLI
|
||||
├──────────────────────────────────────┤
|
||||
│ Application │ Use Cases, Orchestration
|
||||
├──────────────────────────────────────┤
|
||||
│ Domain │ Business Logic, Entities
|
||||
├──────────────────────────────────────┤
|
||||
│ Infrastructure │ Database, APIs, External
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3. The Dependency Rule
|
||||
|
||||
**Dependencies point INWARD.**
|
||||
|
||||
```
|
||||
Infrastructure → Application → Domain
|
||||
↓ ↓ ↓
|
||||
(outer) (middle) (inner)
|
||||
```
|
||||
|
||||
- Inner layers know NOTHING about outer layers
|
||||
- Domain has zero dependencies on infrastructure
|
||||
- Use interfaces to invert dependencies
|
||||
|
||||
```typescript
|
||||
// Domain defines the interface (inner)
|
||||
interface UserRepository {
|
||||
save(user: User): Promise<void>;
|
||||
findById(id: UserId): Promise<User | null>;
|
||||
}
|
||||
|
||||
// Infrastructure implements it (outer)
|
||||
class PostgresUserRepository implements UserRepository {
|
||||
save(user: User): Promise<void> {
|
||||
// SQL here
|
||||
}
|
||||
}
|
||||
|
||||
// Domain service uses the interface
|
||||
class UserService {
|
||||
constructor(private repo: UserRepository) {} // Depends on abstraction
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Contracts
|
||||
|
||||
Interfaces define boundaries between components.
|
||||
|
||||
```typescript
|
||||
// The contract
|
||||
interface PaymentGateway {
|
||||
charge(amount: Money, card: CardDetails): Promise<ChargeResult>;
|
||||
refund(chargeId: string): Promise<RefundResult>;
|
||||
}
|
||||
|
||||
// Multiple implementations possible
|
||||
class StripeGateway implements PaymentGateway { }
|
||||
class PayPalGateway implements PaymentGateway { }
|
||||
class MockGateway implements PaymentGateway { } // For tests
|
||||
```
|
||||
|
||||
### 5. Cross-Cutting Concerns
|
||||
|
||||
Concerns that span multiple features: logging, auth, validation, error handling.
|
||||
|
||||
**Options:**
|
||||
- Middleware/interceptors
|
||||
- Decorators
|
||||
- Aspect-oriented approaches
|
||||
- Base classes (use sparingly)
|
||||
|
||||
```typescript
|
||||
// Middleware approach
|
||||
class LoggingMiddleware {
|
||||
handle(request: Request, next: Handler): Response {
|
||||
console.log(`Request: ${request.path}`);
|
||||
const response = next(request);
|
||||
console.log(`Response: ${response.status}`);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Conway's Law
|
||||
|
||||
> "Organizations design systems that mirror their communication structure."
|
||||
|
||||
**Implication:** Team structure affects architecture. Align both intentionally.
|
||||
|
||||
---
|
||||
|
||||
## Common Architectural Styles
|
||||
|
||||
### Layered Architecture
|
||||
|
||||
Traditional layers: Presentation → Business → Persistence
|
||||
|
||||
**Pros:** Simple, well-understood
|
||||
**Cons:** Can become a "big ball of mud" without discipline
|
||||
|
||||
### Hexagonal Architecture (Ports & Adapters)
|
||||
|
||||
Domain at center, adapters around the edges.
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ HTTP Adapter │
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
┌─────────────────▼─────────────────┐
|
||||
│ DOMAIN │
|
||||
│ ┌─────────────────────────┐ │
|
||||
│ │ Business Logic │ │
|
||||
│ │ Use Cases │ │
|
||||
│ └─────────────────────────┘ │
|
||||
└─────────────────┬─────────────────┘
|
||||
│
|
||||
┌─────────▼───────────┐
|
||||
│ Database Adapter │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
**Ports:** Interfaces defined by the domain
|
||||
**Adapters:** Implementations that connect to the outside world
|
||||
|
||||
### Clean Architecture
|
||||
|
||||
Similar to Hexagonal, with explicit layers:
|
||||
|
||||
1. **Entities** - Enterprise business rules
|
||||
2. **Use Cases** - Application business rules
|
||||
3. **Interface Adapters** - Controllers, Presenters, Gateways
|
||||
4. **Frameworks & Drivers** - Web, DB, External interfaces
|
||||
|
||||
---
|
||||
|
||||
## Feature-Driven Structure (Frontend)
|
||||
|
||||
```
|
||||
src/
|
||||
features/
|
||||
auth/
|
||||
components/
|
||||
LoginForm.tsx
|
||||
SignupForm.tsx
|
||||
hooks/
|
||||
useAuth.ts
|
||||
services/
|
||||
authService.ts
|
||||
types/
|
||||
auth.types.ts
|
||||
index.ts # Public API
|
||||
checkout/
|
||||
components/
|
||||
hooks/
|
||||
services/
|
||||
types/
|
||||
index.ts
|
||||
shared/
|
||||
components/ # Truly shared UI
|
||||
hooks/ # Truly shared hooks
|
||||
utils/ # Truly shared utilities
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature-Driven Structure (Backend)
|
||||
|
||||
```
|
||||
src/
|
||||
modules/
|
||||
users/
|
||||
domain/
|
||||
User.ts
|
||||
UserRepository.ts # Interface
|
||||
application/
|
||||
CreateUser.ts # Use case
|
||||
GetUser.ts # Use case
|
||||
infrastructure/
|
||||
PostgresUserRepo.ts
|
||||
presentation/
|
||||
UserController.ts
|
||||
UserDTO.ts
|
||||
orders/
|
||||
domain/
|
||||
application/
|
||||
infrastructure/
|
||||
presentation/
|
||||
shared/
|
||||
domain/ # Shared value objects
|
||||
infrastructure/ # Shared infra utilities
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Walking Skeleton
|
||||
|
||||
Start with a minimal end-to-end slice:
|
||||
|
||||
1. **Thinnest possible feature** that touches all layers
|
||||
2. **Deployable** from day one
|
||||
3. **Proves the architecture** works
|
||||
|
||||
Example walking skeleton for e-commerce:
|
||||
- User can view ONE product (hardcoded)
|
||||
- User can add it to cart
|
||||
- User can "checkout" (just logs)
|
||||
|
||||
From there, flesh out each feature fully.
|
||||
|
||||
---
|
||||
|
||||
## Testing Architecture
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────┐
|
||||
│ E2E / Acceptance Tests │ Few, slow, high confidence
|
||||
├────────────────────────────────────────────┤
|
||||
│ Integration Tests │ Some, medium speed
|
||||
├────────────────────────────────────────────┤
|
||||
│ Unit Tests │ Many, fast, isolated
|
||||
└────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Test by layer:**
|
||||
- **Domain:** Unit tests (most tests here)
|
||||
- **Application:** Integration tests with mocked infra
|
||||
- **Infrastructure:** Integration tests with real dependencies
|
||||
- **E2E:** Critical paths only
|
||||
|
||||
---
|
||||
|
||||
## Architecture Decision Records (ADRs)
|
||||
|
||||
Document significant decisions:
|
||||
|
||||
```markdown
|
||||
# ADR 001: Use PostgreSQL for persistence
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
We need a database. Options: PostgreSQL, MongoDB, MySQL
|
||||
|
||||
## Decision
|
||||
PostgreSQL for:
|
||||
- ACID compliance
|
||||
- Team familiarity
|
||||
- JSON support for flexibility
|
||||
|
||||
## Consequences
|
||||
- Need PostgreSQL expertise
|
||||
- Schema migrations required
|
||||
- Excellent query capabilities
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Red Flags in Architecture
|
||||
|
||||
- **Circular dependencies** between modules
|
||||
- **Domain depending on infrastructure**
|
||||
- **Framework code in business logic**
|
||||
- **No clear boundaries** between features
|
||||
- **Shared mutable state** across modules
|
||||
- **"Util" or "Common" packages** that grow forever
|
||||
- **Database schema driving domain model**
|
||||
286
solid/references/complexity.md
Normal file
286
solid/references/complexity.md
Normal file
@@ -0,0 +1,286 @@
|
||||
# Managing Complexity
|
||||
|
||||
## The Two Types of Complexity
|
||||
|
||||
### Essential Complexity
|
||||
Inherent to the problem domain. Cannot be removed, only managed.
|
||||
- Business rules
|
||||
- Domain logic
|
||||
- User requirements
|
||||
|
||||
### Accidental Complexity
|
||||
Introduced by our solutions. CAN and SHOULD be minimized.
|
||||
- Poor abstractions
|
||||
- Unnecessary indirection
|
||||
- Framework ceremony
|
||||
- Technical debt
|
||||
|
||||
**Goal: Minimize accidental complexity while clearly expressing essential complexity.**
|
||||
|
||||
---
|
||||
|
||||
## Detecting Complexity
|
||||
|
||||
### 1. Change Amplification
|
||||
Small changes require touching many files.
|
||||
|
||||
**Symptom:** "To add this field, I need to update 15 files."
|
||||
|
||||
**Cause:** Scattered responsibilities, poor abstraction boundaries.
|
||||
|
||||
### 2. Cognitive Load
|
||||
Code is hard to understand, requires holding too much in memory.
|
||||
|
||||
**Symptom:** "I need to understand 10 other classes to understand this one."
|
||||
|
||||
**Cause:** Tight coupling, hidden dependencies, unclear naming.
|
||||
|
||||
### 3. Unknown Unknowns
|
||||
Behavior is surprising, side effects are hidden.
|
||||
|
||||
**Symptom:** "I changed this, and something completely unrelated broke."
|
||||
|
||||
**Cause:** Global state, hidden dependencies, implicit contracts.
|
||||
|
||||
---
|
||||
|
||||
## The XP Values for Fighting Complexity
|
||||
|
||||
From Extreme Programming:
|
||||
|
||||
### 1. Communication
|
||||
Code should communicate clearly. Names, structure, tests all contribute.
|
||||
|
||||
### 2. Simplicity
|
||||
Do the simplest thing that could possibly work.
|
||||
|
||||
### 3. Feedback
|
||||
Fast feedback loops catch complexity early. TDD, CI, code review.
|
||||
|
||||
### 4. Courage
|
||||
Refactor aggressively. Don't let complexity accumulate.
|
||||
|
||||
### 5. Respect
|
||||
Respect future readers (including yourself). Write for humans first.
|
||||
|
||||
---
|
||||
|
||||
## KISS - Keep It Simple, Silly
|
||||
|
||||
> "The simplest solution that works is usually the best."
|
||||
|
||||
### How to Apply:
|
||||
1. Start with the obvious solution
|
||||
2. Only add complexity when REQUIRED
|
||||
3. Prefer boring, well-understood approaches
|
||||
4. Question every abstraction
|
||||
|
||||
```typescript
|
||||
// Over-engineered
|
||||
class UserServiceFactoryProvider {
|
||||
private static instance: UserServiceFactoryProvider;
|
||||
|
||||
static getInstance(): UserServiceFactoryProvider { ... }
|
||||
createFactory(): UserServiceFactory { ... }
|
||||
}
|
||||
|
||||
// KISS
|
||||
class UserService {
|
||||
getUser(id: string): User { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## YAGNI - You Aren't Gonna Need It
|
||||
|
||||
> "Don't build features until they're actually needed."
|
||||
|
||||
### Warning Signs:
|
||||
- "We might need this later"
|
||||
- "It would be nice to have"
|
||||
- "Just in case"
|
||||
- "For future extensibility"
|
||||
|
||||
### The Cost of YAGNI Violations:
|
||||
1. **Development time** - Building unused features
|
||||
2. **Maintenance burden** - Code that must be maintained
|
||||
3. **Cognitive load** - More to understand
|
||||
4. **Wrong abstraction** - Guessing future needs incorrectly
|
||||
|
||||
```typescript
|
||||
// YAGNI violation: Building for hypothetical needs
|
||||
class User {
|
||||
// "We might need these someday"
|
||||
middleName?: string;
|
||||
secondaryEmail?: string;
|
||||
faxNumber?: string;
|
||||
linkedinProfile?: string;
|
||||
twitterHandle?: string;
|
||||
}
|
||||
|
||||
// YAGNI: Only what's needed NOW
|
||||
class User {
|
||||
name: string;
|
||||
email: Email;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DRY - Don't Repeat Yourself (with The Rule of Three)
|
||||
|
||||
> "Every piece of knowledge should have a single, unambiguous representation."
|
||||
|
||||
### BUT: The Rule of Three
|
||||
|
||||
**Don't extract duplication until you see it THREE times.**
|
||||
|
||||
Why? The wrong abstraction is worse than duplication.
|
||||
|
||||
```
|
||||
Duplication #1 → Leave it
|
||||
Duplication #2 → Note it, leave it
|
||||
Duplication #3 → NOW extract it
|
||||
```
|
||||
|
||||
### Example:
|
||||
```typescript
|
||||
// First time - leave it
|
||||
function processUserOrder(order) {
|
||||
validate(order);
|
||||
calculateTax(order);
|
||||
save(order);
|
||||
}
|
||||
|
||||
// Second time - note the similarity, but leave it
|
||||
function processGuestOrder(order) {
|
||||
validate(order);
|
||||
calculateTax(order);
|
||||
save(order);
|
||||
sendGuestEmail(order);
|
||||
}
|
||||
|
||||
// Third time - NOW extract
|
||||
function processCorporateOrder(order) {
|
||||
validate(order);
|
||||
calculateTax(order);
|
||||
save(order);
|
||||
applyCorporateDiscount(order);
|
||||
}
|
||||
|
||||
// After three, extract the common parts
|
||||
function processOrder(order: Order, postProcessing: (o: Order) => void) {
|
||||
validate(order);
|
||||
calculateTax(order);
|
||||
save(order);
|
||||
postProcessing(order);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Separation of Concerns
|
||||
|
||||
> "Each module should address a single concern."
|
||||
|
||||
### Concerns to Separate:
|
||||
- **Business logic** vs **Infrastructure**
|
||||
- **What** (policy) vs **How** (mechanism)
|
||||
- **Input** vs **Processing** vs **Output**
|
||||
- **Data** vs **Behavior**
|
||||
|
||||
### Example:
|
||||
```typescript
|
||||
// BAD: Mixed concerns
|
||||
class OrderProcessor {
|
||||
process(order: Order) {
|
||||
// Validation
|
||||
if (!order.items.length) throw new Error('Empty');
|
||||
|
||||
// Business logic
|
||||
let total = 0;
|
||||
for (const item of order.items) {
|
||||
total += item.price * item.quantity;
|
||||
}
|
||||
|
||||
// Persistence
|
||||
const db = new Database();
|
||||
db.query(`INSERT INTO orders...`);
|
||||
|
||||
// Notification
|
||||
const email = new EmailClient();
|
||||
email.send(order.customer.email, 'Order confirmed');
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Separated concerns
|
||||
class OrderProcessor {
|
||||
constructor(
|
||||
private validator: OrderValidator,
|
||||
private calculator: OrderCalculator,
|
||||
private repository: OrderRepository,
|
||||
private notifier: OrderNotifier
|
||||
) {}
|
||||
|
||||
process(order: Order): ProcessResult {
|
||||
this.validator.validate(order);
|
||||
const total = this.calculator.calculateTotal(order);
|
||||
const savedOrder = this.repository.save(order);
|
||||
this.notifier.notifyConfirmation(savedOrder);
|
||||
return ProcessResult.success(savedOrder);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Managing Technical Debt
|
||||
|
||||
### Types of Technical Debt:
|
||||
1. **Deliberate** - Conscious trade-off for speed
|
||||
2. **Accidental** - Mistakes, lack of knowledge
|
||||
3. **Bit rot** - Code degrades over time
|
||||
|
||||
### The Boy Scout Rule:
|
||||
> "Leave the code better than you found it."
|
||||
|
||||
Every time you touch code:
|
||||
- Improve one small thing
|
||||
- Fix one naming issue
|
||||
- Extract one method
|
||||
- Add one missing test
|
||||
|
||||
### When to Pay Down Debt:
|
||||
- When it's in your path (you're already there)
|
||||
- When it's blocking new features
|
||||
- When it's causing bugs
|
||||
- During dedicated refactoring time
|
||||
|
||||
### When NOT to Refactor:
|
||||
- Code that works and won't change
|
||||
- Code being replaced soon
|
||||
- When you don't have tests
|
||||
|
||||
---
|
||||
|
||||
## The Four Elements of Simple Design
|
||||
|
||||
In priority order (from XP):
|
||||
|
||||
1. **Runs all the tests**
|
||||
- If it doesn't work, nothing else matters
|
||||
|
||||
2. **Expresses intent**
|
||||
- Clear names, obvious structure
|
||||
- Code tells the story
|
||||
|
||||
3. **No duplication**
|
||||
- DRY (but Rule of Three)
|
||||
- Single source of truth
|
||||
|
||||
4. **Minimal**
|
||||
- Fewest classes and methods possible
|
||||
- Remove anything unnecessary
|
||||
|
||||
If these four are true, the design is simple enough.
|
||||
504
solid/references/design-patterns.md
Normal file
504
solid/references/design-patterns.md
Normal file
@@ -0,0 +1,504 @@
|
||||
# Design Patterns
|
||||
|
||||
## What Are Design Patterns?
|
||||
|
||||
Reusable solutions to common design problems. A shared vocabulary for discussing design.
|
||||
|
||||
## WARNING: Don't Force Patterns
|
||||
|
||||
> "Let patterns emerge from refactoring, don't force them upfront."
|
||||
|
||||
Patterns should solve problems you HAVE, not problems you MIGHT have.
|
||||
|
||||
## When to Use Patterns
|
||||
|
||||
1. **You recognize the problem** - You've seen it before
|
||||
2. **The pattern fits** - Not forcing it
|
||||
3. **It simplifies** - Doesn't add unnecessary complexity
|
||||
4. **Team understands it** - Shared knowledge
|
||||
|
||||
---
|
||||
|
||||
## Creational Patterns
|
||||
|
||||
### Singleton
|
||||
|
||||
**Purpose:** Ensure only one instance exists.
|
||||
|
||||
**When to use:** Global configuration, connection pools, logging.
|
||||
|
||||
**Warning:** Often overused. Consider dependency injection instead.
|
||||
|
||||
```typescript
|
||||
class Logger {
|
||||
private static instance: Logger;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): Logger {
|
||||
if (!Logger.instance) {
|
||||
Logger.instance = new Logger();
|
||||
}
|
||||
return Logger.instance;
|
||||
}
|
||||
|
||||
log(message: string): void { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Factory
|
||||
|
||||
**Purpose:** Create objects without specifying exact class.
|
||||
|
||||
**When to use:** Object creation logic is complex, or varies by type.
|
||||
|
||||
```typescript
|
||||
interface Notification {
|
||||
send(message: string): void;
|
||||
}
|
||||
|
||||
class EmailNotification implements Notification { ... }
|
||||
class SMSNotification implements Notification { ... }
|
||||
class PushNotification implements Notification { ... }
|
||||
|
||||
class NotificationFactory {
|
||||
create(type: 'email' | 'sms' | 'push'): Notification {
|
||||
switch (type) {
|
||||
case 'email': return new EmailNotification();
|
||||
case 'sms': return new SMSNotification();
|
||||
case 'push': return new PushNotification();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Builder
|
||||
|
||||
**Purpose:** Construct complex objects step by step.
|
||||
|
||||
**When to use:** Objects with many optional parameters, test data creation.
|
||||
|
||||
```typescript
|
||||
class UserBuilder {
|
||||
private user: Partial<User> = {};
|
||||
|
||||
withName(name: string): UserBuilder {
|
||||
this.user.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
withEmail(email: string): UserBuilder {
|
||||
this.user.email = email;
|
||||
return this;
|
||||
}
|
||||
|
||||
withAge(age: number): UserBuilder {
|
||||
this.user.age = age;
|
||||
return this;
|
||||
}
|
||||
|
||||
build(): User {
|
||||
return new User(
|
||||
this.user.name!,
|
||||
this.user.email!,
|
||||
this.user.age
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const user = new UserBuilder()
|
||||
.withName('Alice')
|
||||
.withEmail('alice@example.com')
|
||||
.build();
|
||||
```
|
||||
|
||||
### Prototype
|
||||
|
||||
**Purpose:** Create new objects by cloning existing ones.
|
||||
|
||||
**When to use:** Object creation is expensive, or you need copies with slight variations.
|
||||
|
||||
```typescript
|
||||
interface Prototype {
|
||||
clone(): Prototype;
|
||||
}
|
||||
|
||||
class Document implements Prototype {
|
||||
constructor(
|
||||
public title: string,
|
||||
public content: string,
|
||||
public metadata: Metadata
|
||||
) {}
|
||||
|
||||
clone(): Document {
|
||||
return new Document(
|
||||
this.title,
|
||||
this.content,
|
||||
{ ...this.metadata }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Structural Patterns
|
||||
|
||||
### Adapter
|
||||
|
||||
**Purpose:** Make incompatible interfaces work together.
|
||||
|
||||
**When to use:** Integrating third-party libraries, legacy code.
|
||||
|
||||
```typescript
|
||||
// Third-party library with different interface
|
||||
class OldPaymentAPI {
|
||||
makePayment(cents: number): boolean { ... }
|
||||
}
|
||||
|
||||
// Our interface
|
||||
interface PaymentGateway {
|
||||
charge(amount: Money): ChargeResult;
|
||||
}
|
||||
|
||||
// Adapter
|
||||
class OldPaymentAdapter implements PaymentGateway {
|
||||
constructor(private oldAPI: OldPaymentAPI) {}
|
||||
|
||||
charge(amount: Money): ChargeResult {
|
||||
const cents = amount.toCents();
|
||||
const success = this.oldAPI.makePayment(cents);
|
||||
return success ? ChargeResult.success() : ChargeResult.failed();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Decorator
|
||||
|
||||
**Purpose:** Add behavior to objects dynamically.
|
||||
|
||||
**When to use:** Adding features without modifying existing code.
|
||||
|
||||
```typescript
|
||||
interface Notifier {
|
||||
send(message: string): void;
|
||||
}
|
||||
|
||||
class EmailNotifier implements Notifier {
|
||||
send(message: string): void {
|
||||
console.log(`Email: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Decorators
|
||||
class SMSDecorator implements Notifier {
|
||||
constructor(private wrapped: Notifier) {}
|
||||
|
||||
send(message: string): void {
|
||||
this.wrapped.send(message);
|
||||
console.log(`SMS: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
class SlackDecorator implements Notifier {
|
||||
constructor(private wrapped: Notifier) {}
|
||||
|
||||
send(message: string): void {
|
||||
this.wrapped.send(message);
|
||||
console.log(`Slack: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage - compose behaviors
|
||||
const notifier = new SlackDecorator(
|
||||
new SMSDecorator(
|
||||
new EmailNotifier()
|
||||
)
|
||||
);
|
||||
notifier.send('Alert!'); // Sends to all three
|
||||
```
|
||||
|
||||
### Proxy
|
||||
|
||||
**Purpose:** Control access to an object.
|
||||
|
||||
**When to use:** Lazy loading, access control, logging, caching.
|
||||
|
||||
```typescript
|
||||
interface Image {
|
||||
display(): void;
|
||||
}
|
||||
|
||||
class RealImage implements Image {
|
||||
constructor(private filename: string) {
|
||||
this.loadFromDisk(); // Expensive
|
||||
}
|
||||
|
||||
private loadFromDisk(): void { ... }
|
||||
|
||||
display(): void { ... }
|
||||
}
|
||||
|
||||
// Lazy loading proxy
|
||||
class ImageProxy implements Image {
|
||||
private realImage: RealImage | null = null;
|
||||
|
||||
constructor(private filename: string) {}
|
||||
|
||||
display(): void {
|
||||
if (!this.realImage) {
|
||||
this.realImage = new RealImage(this.filename);
|
||||
}
|
||||
this.realImage.display();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Composite
|
||||
|
||||
**Purpose:** Treat individual objects and compositions uniformly.
|
||||
|
||||
**When to use:** Tree structures, hierarchies (files/folders, UI components).
|
||||
|
||||
```typescript
|
||||
interface Component {
|
||||
getPrice(): number;
|
||||
}
|
||||
|
||||
class Product implements Component {
|
||||
constructor(private price: number) {}
|
||||
|
||||
getPrice(): number {
|
||||
return this.price;
|
||||
}
|
||||
}
|
||||
|
||||
class Box implements Component {
|
||||
private children: Component[] = [];
|
||||
|
||||
add(component: Component): void {
|
||||
this.children.push(component);
|
||||
}
|
||||
|
||||
getPrice(): number {
|
||||
return this.children.reduce(
|
||||
(sum, child) => sum + child.getPrice(),
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const smallBox = new Box();
|
||||
smallBox.add(new Product(10));
|
||||
smallBox.add(new Product(20));
|
||||
|
||||
const bigBox = new Box();
|
||||
bigBox.add(smallBox);
|
||||
bigBox.add(new Product(50));
|
||||
|
||||
console.log(bigBox.getPrice()); // 80
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Behavioral Patterns
|
||||
|
||||
### Strategy
|
||||
|
||||
**Purpose:** Define a family of algorithms, make them interchangeable.
|
||||
|
||||
**When to use:** Multiple ways to do something, switchable at runtime.
|
||||
|
||||
```typescript
|
||||
interface PricingStrategy {
|
||||
calculate(basePrice: number): number;
|
||||
}
|
||||
|
||||
class RegularPricing implements PricingStrategy {
|
||||
calculate(basePrice: number): number {
|
||||
return basePrice;
|
||||
}
|
||||
}
|
||||
|
||||
class PremiumDiscount implements PricingStrategy {
|
||||
calculate(basePrice: number): number {
|
||||
return basePrice * 0.8; // 20% off
|
||||
}
|
||||
}
|
||||
|
||||
class BlackFriday implements PricingStrategy {
|
||||
calculate(basePrice: number): number {
|
||||
return basePrice * 0.5; // 50% off
|
||||
}
|
||||
}
|
||||
|
||||
class ShoppingCart {
|
||||
constructor(private pricing: PricingStrategy) {}
|
||||
|
||||
calculateTotal(items: Item[]): number {
|
||||
const base = items.reduce((sum, i) => sum + i.price, 0);
|
||||
return this.pricing.calculate(base);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Observer
|
||||
|
||||
**Purpose:** Notify multiple objects about state changes.
|
||||
|
||||
**When to use:** Event systems, pub/sub, reactive updates.
|
||||
|
||||
```typescript
|
||||
interface Observer {
|
||||
update(event: Event): void;
|
||||
}
|
||||
|
||||
class EventEmitter {
|
||||
private observers: Observer[] = [];
|
||||
|
||||
subscribe(observer: Observer): void {
|
||||
this.observers.push(observer);
|
||||
}
|
||||
|
||||
unsubscribe(observer: Observer): void {
|
||||
this.observers = this.observers.filter(o => o !== observer);
|
||||
}
|
||||
|
||||
notify(event: Event): void {
|
||||
this.observers.forEach(o => o.update(event));
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
class OrderService extends EventEmitter {
|
||||
placeOrder(order: Order): void {
|
||||
// Process order...
|
||||
this.notify({ type: 'ORDER_PLACED', order });
|
||||
}
|
||||
}
|
||||
|
||||
class EmailService implements Observer {
|
||||
update(event: Event): void {
|
||||
if (event.type === 'ORDER_PLACED') {
|
||||
this.sendConfirmation(event.order);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Template Method
|
||||
|
||||
**Purpose:** Define algorithm skeleton, let subclasses override steps.
|
||||
|
||||
**When to use:** Common algorithm with varying steps.
|
||||
|
||||
```typescript
|
||||
abstract class DataExporter {
|
||||
// Template method - defines the algorithm
|
||||
export(data: Data[]): void {
|
||||
this.validate(data);
|
||||
const formatted = this.format(data);
|
||||
this.write(formatted);
|
||||
this.notify();
|
||||
}
|
||||
|
||||
// Common steps
|
||||
private validate(data: Data[]): void { ... }
|
||||
private notify(): void { ... }
|
||||
|
||||
// Steps to override
|
||||
protected abstract format(data: Data[]): string;
|
||||
protected abstract write(content: string): void;
|
||||
}
|
||||
|
||||
class CSVExporter extends DataExporter {
|
||||
protected format(data: Data[]): string {
|
||||
return data.map(d => d.toCSV()).join('\n');
|
||||
}
|
||||
|
||||
protected write(content: string): void {
|
||||
fs.writeFileSync('export.csv', content);
|
||||
}
|
||||
}
|
||||
|
||||
class JSONExporter extends DataExporter {
|
||||
protected format(data: Data[]): string {
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
|
||||
protected write(content: string): void {
|
||||
fs.writeFileSync('export.json', content);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Command
|
||||
|
||||
**Purpose:** Encapsulate a request as an object.
|
||||
|
||||
**When to use:** Undo/redo, queuing, logging actions.
|
||||
|
||||
```typescript
|
||||
interface Command {
|
||||
execute(): void;
|
||||
undo(): void;
|
||||
}
|
||||
|
||||
class AddItemCommand implements Command {
|
||||
constructor(
|
||||
private cart: Cart,
|
||||
private item: Item
|
||||
) {}
|
||||
|
||||
execute(): void {
|
||||
this.cart.add(this.item);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
this.cart.remove(this.item);
|
||||
}
|
||||
}
|
||||
|
||||
class CommandHistory {
|
||||
private history: Command[] = [];
|
||||
|
||||
execute(command: Command): void {
|
||||
command.execute();
|
||||
this.history.push(command);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const command = this.history.pop();
|
||||
command?.undo();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern Awareness
|
||||
|
||||
### The Four-Dimensional Lens
|
||||
|
||||
When analyzing new code/libraries, ask:
|
||||
|
||||
1. **What problem does it solve?** (Creational, Structural, Behavioral)
|
||||
2. **What scope?** (Object-level, Class-level, System-level)
|
||||
3. **When is it applied?** (Compile-time, Runtime)
|
||||
4. **How coupled?** (Tight, Loose)
|
||||
|
||||
This helps recognize patterns even in unfamiliar code.
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
| Anti-Pattern | Problem | Solution |
|
||||
|--------------|---------|----------|
|
||||
| **God Object** | Class does everything | Split by responsibility |
|
||||
| **Spaghetti Code** | Tangled, no structure | Refactor to layers |
|
||||
| **Golden Hammer** | Using one pattern for everything | Match pattern to problem |
|
||||
| **Premature Optimization** | Optimizing before needed | YAGNI, profile first |
|
||||
| **Copy-Paste Programming** | Duplication | Extract, Rule of Three |
|
||||
317
solid/references/go-adaptation.md
Normal file
317
solid/references/go-adaptation.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# SOLID Principles: Go Adaptation
|
||||
|
||||
This document rewrites each SOLID principle with idiomatic Go examples. Where Go idioms conflict with OOP-centric formulations, Go wins. Tension is noted.
|
||||
|
||||
## S — Single Responsibility Principle in Go
|
||||
|
||||
**OOP formulation:** A class should have one reason to change.
|
||||
|
||||
**Go formulation:** A type or package should have one reason to change. Responsibility maps to the unit of deployment (package), not just types.
|
||||
|
||||
### Go example
|
||||
|
||||
```go
|
||||
// Bad: Order type mixes domain, persistence, and notification
|
||||
package order
|
||||
|
||||
type Order struct {
|
||||
ID string
|
||||
Items []Item
|
||||
Customer Customer
|
||||
}
|
||||
|
||||
func (o *Order) Save(db *sql.DB) error { ... } // persistence concern
|
||||
func (o *Order) SendReceipt(smtp *mail.Client) error { ... } // notification concern
|
||||
func (o *Order) CalculateTotal() Money { ... } // domain concern (correct)
|
||||
|
||||
// Good: each type/package has one reason to change
|
||||
package order
|
||||
|
||||
// Domain type: only reason to change = business rules change
|
||||
type Order struct {
|
||||
ID OrderID
|
||||
Items []Item
|
||||
Customer Customer
|
||||
}
|
||||
|
||||
func (o Order) Total() Money {
|
||||
total := Money{}
|
||||
for _, item := range o.Items {
|
||||
total = total.Add(item.Price.Multiply(item.Quantity))
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
package orderstore
|
||||
|
||||
// Persistence: only reason to change = storage mechanism changes
|
||||
type Store struct { db *sql.DB }
|
||||
|
||||
func (s *Store) Save(ctx context.Context, o order.Order) error { ... }
|
||||
|
||||
package ordernotify
|
||||
|
||||
// Notification: only reason to change = notification channel changes
|
||||
type Notifier struct { mailer Mailer }
|
||||
|
||||
func (n *Notifier) SendReceipt(ctx context.Context, o order.Order) error { ... }
|
||||
```
|
||||
|
||||
### Package-level SRP
|
||||
|
||||
Package names should be nouns that describe one concept:
|
||||
- `store`, `cache`, `handler`, `validator` — single responsibility
|
||||
- `util`, `common`, `misc`, `helpers` — SRP violation waiting to happen
|
||||
|
||||
---
|
||||
|
||||
## O — Open/Closed Principle in Go
|
||||
|
||||
**OOP formulation:** Open for extension via subclassing, closed for modification.
|
||||
|
||||
**Go formulation:** Open for extension by implementing an interface or adding new types; closed for modification of existing types. No subclassing needed.
|
||||
|
||||
### Go example
|
||||
|
||||
```go
|
||||
// Bad: adding a new discount type requires modifying existing code
|
||||
func ApplyDiscount(order *Order, discountType string) Money {
|
||||
switch discountType {
|
||||
case "percentage":
|
||||
return order.Total().Multiply(0.9)
|
||||
case "fixed":
|
||||
return order.Total().Subtract(Money{Amount: 10})
|
||||
// Must add case here every time
|
||||
}
|
||||
return order.Total()
|
||||
}
|
||||
|
||||
// Good: add new discount types by implementing the interface
|
||||
type Discounter interface {
|
||||
Apply(total Money) Money
|
||||
}
|
||||
|
||||
type PercentageDiscount struct{ Percent float64 }
|
||||
func (d PercentageDiscount) Apply(total Money) Money {
|
||||
return total.Multiply(1 - d.Percent/100)
|
||||
}
|
||||
|
||||
type FixedDiscount struct{ Amount Money }
|
||||
func (d FixedDiscount) Apply(total Money) Money {
|
||||
return total.Subtract(d.Amount)
|
||||
}
|
||||
|
||||
// Adding SeniorDiscount requires zero changes to existing code
|
||||
type SeniorDiscount struct{}
|
||||
func (d SeniorDiscount) Apply(total Money) Money {
|
||||
return total.Multiply(0.85)
|
||||
}
|
||||
|
||||
func ApplyDiscount(order *Order, d Discounter) Money {
|
||||
return d.Apply(order.Total())
|
||||
}
|
||||
```
|
||||
|
||||
### Go tension
|
||||
|
||||
Go has no inheritance, so "closed for modification" is natural — you can't subclass a concrete type to override behavior. The extension point is always an interface. If you're adding switch cases to handle new types, that's the signal to introduce an interface.
|
||||
|
||||
---
|
||||
|
||||
## L — Liskov Substitution Principle in Go
|
||||
|
||||
**OOP formulation:** Subtypes must be substitutable for their base types.
|
||||
|
||||
**Go formulation:** Any implementation of an interface must honor the interface's documented contract, not just its method signatures. Go's structural typing means LSP is enforced by convention and documentation, not the compiler.
|
||||
|
||||
### Contract documentation
|
||||
|
||||
```go
|
||||
// UserStore: all implementations must honor this contract:
|
||||
// - Save: persists user, returns ErrDuplicateEmail if email already exists
|
||||
// - GetByEmail: returns ErrNotFound if user does not exist
|
||||
// - Both methods must be safe for concurrent use
|
||||
type UserStore interface {
|
||||
Save(ctx context.Context, u User) error
|
||||
GetByEmail(ctx context.Context, email string) (User, error)
|
||||
}
|
||||
|
||||
var (
|
||||
ErrDuplicateEmail = errors.New("duplicate email")
|
||||
ErrNotFound = errors.New("not found")
|
||||
)
|
||||
|
||||
// PostgresUserStore: honors contract
|
||||
type PostgresUserStore struct { db *sql.DB }
|
||||
func (s *PostgresUserStore) Save(ctx context.Context, u User) error {
|
||||
_, err := s.db.ExecContext(ctx, "INSERT INTO users ...", u.Email)
|
||||
if isUniqueViolation(err) {
|
||||
return ErrDuplicateEmail // Returns documented error
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// InMemoryUserStore: honors contract (for tests)
|
||||
type InMemoryUserStore struct {
|
||||
mu sync.Mutex
|
||||
users map[string]User
|
||||
}
|
||||
func (s *InMemoryUserStore) Save(ctx context.Context, u User) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if _, exists := s.users[u.Email]; exists {
|
||||
return ErrDuplicateEmail // Same documented error
|
||||
}
|
||||
s.users[u.Email] = u
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### LSP violations to watch for
|
||||
|
||||
```go
|
||||
// Bad: violates contract — does not return ErrDuplicateEmail
|
||||
type CachedUserStore struct { ... }
|
||||
func (s *CachedUserStore) Save(ctx context.Context, u User) error {
|
||||
return errors.New("cache: duplicate key") // Different error type breaks callers
|
||||
}
|
||||
|
||||
// Bad: panics on some inputs — violates contract
|
||||
type LazyUserStore struct { ... }
|
||||
func (s *LazyUserStore) GetByEmail(ctx context.Context, email string) (User, error) {
|
||||
panic("not implemented yet") // Violates LSP
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## I — Interface Segregation Principle in Go
|
||||
|
||||
**OOP formulation:** Clients should not depend on methods they do not use.
|
||||
|
||||
**Go formulation:** Define the narrowest interface possible at the point of use. Go's structural typing makes this natural — you don't need the implementation to declare what it implements.
|
||||
|
||||
### io.Reader as the canonical example
|
||||
|
||||
```go
|
||||
// io.Reader is a single-method interface
|
||||
type Reader interface {
|
||||
Read(p []byte) (n int, err error)
|
||||
}
|
||||
|
||||
// Any type with Read() satisfies this — os.File, bytes.Buffer, net.Conn, etc.
|
||||
// Callers that only need to read accept Reader, not a fat interface
|
||||
func parseConfig(r io.Reader) (Config, error) { ... }
|
||||
```
|
||||
|
||||
### Define interfaces where you consume them
|
||||
|
||||
```go
|
||||
// package report — only needs to read invoices
|
||||
package report
|
||||
|
||||
// Define the interface here, at the point of use — not in the invoice package
|
||||
type InvoiceReader interface {
|
||||
GetByID(ctx context.Context, id InvoiceID) (Invoice, error)
|
||||
ListByCustomer(ctx context.Context, customerID CustomerID) ([]Invoice, error)
|
||||
}
|
||||
|
||||
func NewReporter(invoices InvoiceReader) *Reporter { ... }
|
||||
|
||||
// The invoice package's PostgresStore has 10+ methods
|
||||
// This interface only exposes what the reporter needs
|
||||
// Adding new methods to PostgresStore never forces changes to the reporter
|
||||
```
|
||||
|
||||
### Composing interfaces
|
||||
|
||||
```go
|
||||
type Reader interface {
|
||||
Read(p []byte) (n int, err error)
|
||||
}
|
||||
|
||||
type Writer interface {
|
||||
Write(p []byte) (n int, err error)
|
||||
}
|
||||
|
||||
// Compose at the call site, not in the definition
|
||||
type ReadWriter interface {
|
||||
Reader
|
||||
Writer
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## D — Dependency Inversion Principle in Go
|
||||
|
||||
**OOP formulation:** High-level modules should not depend on low-level modules. Both depend on abstractions.
|
||||
|
||||
**Go formulation:** Accept interfaces, not concrete types. Domain packages import nothing from infrastructure packages. Infrastructure packages import from domain packages.
|
||||
|
||||
### The dependency direction rule
|
||||
|
||||
```
|
||||
cmd/ → internal/handler → internal/service → internal/domain
|
||||
internal/store (postgres) → internal/domain
|
||||
internal/mailer (smtp) → internal/domain
|
||||
|
||||
domain imports nothing from store, mailer, handler
|
||||
```
|
||||
|
||||
### Constructor injection pattern
|
||||
|
||||
```go
|
||||
// Good: domain service accepts interfaces
|
||||
package service
|
||||
|
||||
type UserService struct {
|
||||
store UserStore // interface
|
||||
mailer Mailer // interface
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// NewUserService constructs with injected dependencies.
|
||||
// All parameters are interfaces — callers provide implementations.
|
||||
func NewUserService(store UserStore, mailer Mailer, logger *slog.Logger) *UserService {
|
||||
return &UserService{store: store, mailer: mailer, logger: logger}
|
||||
}
|
||||
|
||||
// Bad: domain service imports infrastructure
|
||||
package service
|
||||
|
||||
import "github.com/example/myapp/internal/store/postgres"
|
||||
|
||||
type UserService struct {
|
||||
store *postgres.Store // Locked to PostgreSQL — breaks DIP
|
||||
}
|
||||
```
|
||||
|
||||
### Wire it up at the boundary
|
||||
|
||||
```go
|
||||
// cmd/server/main.go — the composition root
|
||||
func main() {
|
||||
db := mustOpenDB(cfg.DatabaseURL)
|
||||
store := postgres.NewUserStore(db)
|
||||
mailer := smtp.NewMailer(cfg.SMTP)
|
||||
logger := slog.Default()
|
||||
svc := service.NewUserService(store, mailer, logger)
|
||||
handler := handler.NewUserHandler(svc)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The `main` function (or a dependency injection framework) is the only place that names concrete implementations. All other code depends on interfaces.
|
||||
|
||||
### When NOT to extract an interface
|
||||
|
||||
Don't extract an interface prematurely. These are fine as concrete dependencies:
|
||||
|
||||
- `*slog.Logger` — no interface needed; it already accepts a Handler interface internally
|
||||
- `*sql.DB` — acceptable in `store` packages; extract an interface at the service boundary
|
||||
- Standard library types that are stable and have no test double need
|
||||
|
||||
Only extract an interface when you have:
|
||||
1. Multiple implementations (real + test double), or
|
||||
2. A package boundary you want to keep clean (domain knows nothing of postgres)
|
||||
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
|
||||
```
|
||||
262
solid/references/solid-principles.md
Normal file
262
solid/references/solid-principles.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# SOLID Principles
|
||||
|
||||
## Overview
|
||||
|
||||
SOLID helps structure software to be flexible, maintainable, and testable. These principles reduce coupling and increase cohesion.
|
||||
|
||||
## S - Single Responsibility Principle (SRP)
|
||||
|
||||
> "A class should have one, and only one, reason to change."
|
||||
|
||||
### Problem It Solves
|
||||
God objects that do everything - hard to test, hard to change, hard to understand.
|
||||
|
||||
### How to Apply
|
||||
Each class handles ONE responsibility. If you find yourself saying "and" when describing what a class does, split it.
|
||||
|
||||
```typescript
|
||||
// BAD: Multiple responsibilities
|
||||
class Order {
|
||||
calculateTotal(): number { ... }
|
||||
saveToDatabase(): void { ... } // Persistence
|
||||
generateInvoice(): string { ... } // Presentation
|
||||
}
|
||||
|
||||
// GOOD: Single responsibility each
|
||||
class Order {
|
||||
private items: OrderItem[] = [];
|
||||
|
||||
addItem(item: OrderItem): void { ... }
|
||||
calculateTotal(): number { ... }
|
||||
}
|
||||
|
||||
class OrderRepository {
|
||||
save(order: Order): Promise<void> { ... }
|
||||
}
|
||||
|
||||
class InvoiceGenerator {
|
||||
generate(order: Order): Invoice { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Detection Questions
|
||||
- Does this class have multiple reasons to change?
|
||||
- Can I describe it without using "and"?
|
||||
- Would different stakeholders request changes to different parts?
|
||||
|
||||
---
|
||||
|
||||
## O - Open/Closed Principle (OCP)
|
||||
|
||||
> "Software entities should be open for extension but closed for modification."
|
||||
|
||||
### Problem It Solves
|
||||
Having to modify existing, tested code every time requirements change. Risk of breaking working features.
|
||||
|
||||
### How to Apply
|
||||
Design abstractions that allow new behavior through new classes, not edits to existing ones.
|
||||
|
||||
```typescript
|
||||
// BAD: Must modify to add new shipping
|
||||
class ShippingCalculator {
|
||||
calculate(type: string, value: number): number {
|
||||
if (type === 'standard') return value < 50 ? 5 : 0;
|
||||
if (type === 'express') return 15;
|
||||
// Must add more ifs for new types!
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Open for extension
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Add new shipping by creating new class, not modifying existing
|
||||
class SameDayShipping implements ShippingMethod {
|
||||
calculateCost(orderValue: number): number {
|
||||
return 25;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Architectural Insight
|
||||
OCP at architecture level means: **design your codebase so new features are added by adding code, not changing existing code.**
|
||||
|
||||
---
|
||||
|
||||
## L - Liskov Substitution Principle (LSP)
|
||||
|
||||
> "Subtypes must be substitutable for their base types without altering program correctness."
|
||||
|
||||
### Problem It Solves
|
||||
Subclasses that break expectations, requiring type-checking and special cases.
|
||||
|
||||
### How to Apply
|
||||
Subclasses must honor the contract of the parent. If the parent returns positive numbers, subclasses cannot return negatives.
|
||||
|
||||
```typescript
|
||||
// BAD: Violates parent's contract
|
||||
class DiscountPolicy {
|
||||
getDiscount(value: number): number {
|
||||
return 0; // Non-negative expected
|
||||
}
|
||||
}
|
||||
|
||||
class WeirdDiscount extends DiscountPolicy {
|
||||
getDiscount(value: number): number {
|
||||
return -5; // Increases cost! Breaks expectations
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Enforces contract
|
||||
class DiscountPolicy {
|
||||
constructor(private discount: number) {
|
||||
if (discount < 0) throw new Error("Discount must be non-negative");
|
||||
}
|
||||
|
||||
getDiscount(): number {
|
||||
return this.discount;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Key Insight
|
||||
This is why you can swap `InMemoryUserRepo` for `PostgresUserRepo` - they both honor the `UserRepo` interface contract.
|
||||
|
||||
---
|
||||
|
||||
## I - Interface Segregation Principle (ISP)
|
||||
|
||||
> "Clients should not be forced to depend on methods they do not use."
|
||||
|
||||
### Problem It Solves
|
||||
Fat interfaces that force partial implementations, empty methods, or throws.
|
||||
|
||||
### How to Apply
|
||||
Split large interfaces into smaller, cohesive ones. Clients depend only on what they need.
|
||||
|
||||
```typescript
|
||||
// BAD: Fat interface
|
||||
interface WarehouseDevice {
|
||||
printLabel(orderId: string): void;
|
||||
scanBarcode(): string;
|
||||
packageItem(orderId: string): void;
|
||||
}
|
||||
|
||||
class BasicPrinter implements WarehouseDevice {
|
||||
printLabel(orderId: string): void { /* works */ }
|
||||
scanBarcode(): string { throw new Error("Not supported"); } // Forced!
|
||||
packageItem(orderId: string): void { throw new Error("Not supported"); }
|
||||
}
|
||||
|
||||
// GOOD: Segregated interfaces
|
||||
interface LabelPrinter {
|
||||
printLabel(orderId: string): void;
|
||||
}
|
||||
|
||||
interface BarcodeScanner {
|
||||
scanBarcode(): string;
|
||||
}
|
||||
|
||||
interface ItemPackager {
|
||||
packageItem(orderId: string): void;
|
||||
}
|
||||
|
||||
class BasicPrinter implements LabelPrinter {
|
||||
printLabel(orderId: string): void { /* only what it does */ }
|
||||
}
|
||||
```
|
||||
|
||||
### Detection
|
||||
If you see `throw new Error("Not implemented")` or empty method bodies, the interface is too fat.
|
||||
|
||||
---
|
||||
|
||||
## D - Dependency Inversion Principle (DIP)
|
||||
|
||||
> "High-level modules should not depend on low-level modules. Both should depend on abstractions."
|
||||
|
||||
### Problem It Solves
|
||||
Tight coupling to specific implementations (databases, APIs, frameworks). Hard to test, hard to swap.
|
||||
|
||||
### How to Apply
|
||||
Depend on interfaces, inject implementations.
|
||||
|
||||
```typescript
|
||||
// BAD: Direct dependency on concrete class
|
||||
class OrderService {
|
||||
private emailService = new SendGridEmailService(); // Locked in!
|
||||
|
||||
confirmOrder(email: string): void {
|
||||
this.emailService.send(email, "Order confirmed");
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Depend on abstraction
|
||||
interface EmailService {
|
||||
send(to: string, message: string): void;
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
constructor(private emailService: EmailService) {}
|
||||
|
||||
confirmOrder(email: string): void {
|
||||
this.emailService.send(email, "Order confirmed");
|
||||
}
|
||||
}
|
||||
|
||||
// Now can inject any implementation
|
||||
new OrderService(new SendGridEmailService());
|
||||
new OrderService(new SESEmailService());
|
||||
new OrderService(new MockEmailService()); // For tests!
|
||||
```
|
||||
|
||||
### The Dependency Rule
|
||||
Source code dependencies should point **inward** toward high-level policies (domain logic), never toward low-level details (infrastructure).
|
||||
|
||||
```
|
||||
Infrastructure → Application → Domain
|
||||
↑ ↑ ↑
|
||||
(outer) (middle) (inner)
|
||||
|
||||
Dependencies flow: outer → inner
|
||||
Never: inner → outer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Applying SOLID at Architecture Level
|
||||
|
||||
These principles scale beyond classes:
|
||||
|
||||
| Principle | Architecture Application |
|
||||
|-----------|--------------------------|
|
||||
| SRP | Each bounded context has one responsibility |
|
||||
| OCP | New features = new modules, not edits to existing |
|
||||
| LSP | Microservices with same contract are substitutable |
|
||||
| ISP | Thin interfaces between services |
|
||||
| DIP | High-level business logic doesn't know about databases/frameworks |
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Principle | One-Liner | Red Flag |
|
||||
|-----------|-----------|----------|
|
||||
| SRP | One reason to change | "This class handles X and Y and Z" |
|
||||
| OCP | Add, don't modify | `if/else` chains for types |
|
||||
| LSP | Subtypes are substitutable | Type-checking in calling code |
|
||||
| ISP | Small, focused interfaces | Empty method implementations |
|
||||
| DIP | Depend on abstractions | `new ConcreteClass()` in business logic |
|
||||
Reference in New Issue
Block a user