chore: bootstrap skills library — 19 skills + installer + CI auto-tag
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:
Mathias
2026-05-24 14:59:54 +02:00
commit d6a71e370e
33 changed files with 8688 additions and 0 deletions

259
solid/SKILL.md Normal file
View File

@@ -0,0 +1,259 @@
---
name: solid
description: Apply SOLID design principles in Go. Use during architecture decisions, design reviews, and when adding new abstractions.
---
# SOLID Principles
## Overview
SOLID helps structure software to be flexible, maintainable, and testable. These principles reduce coupling and increase cohesion.
In Go, several principles manifest differently than in classical OOP languages. Where Go idioms conflict with OOP-centric SOLID, **Go wins**. The tension is noted explicitly.
## Quick Reference
| Principle | One-Liner | Question to Ask |
|-----------|-----------|-----------------|
| **S**RP | One reason to change | "Does this type have ONE reason to change?" |
| **O**CP | Open for extension, closed for modification | "Can I extend without modifying?" |
| **L**SP | Subtypes are substitutable | "Can any implementation replace another safely?" |
| **I**SP | Small, focused interfaces | "Are clients forced to depend on unused methods?" |
| **D**IP | Depend on abstractions | "Do I accept interfaces, not concrete types?" |
## S — Single Responsibility Principle
> "A class should have one, and only one, reason to change."
In Go: a **type** or **package** should have one reason to change. A package that handles both domain logic and database queries has two reasons to change.
**Detection:**
- Can you describe the type's responsibility without using "and"?
- Would different stakeholders (product, ops, DBA) request changes to different parts?
**Go example:**
```go
// Bad: multiple responsibilities
type UserHandler struct {}
func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ... }
func (h *UserHandler) SaveToDatabase(u User) error { ... }
func (h *UserHandler) SendWelcomeEmail(u User) error { ... }
// Good: single responsibility each
type UserHandler struct {
store UserStore
mailer Mailer
}
type PostgresUserStore struct { db *sql.DB }
func (s *PostgresUserStore) Save(ctx context.Context, u User) error { ... }
type SMTPMailer struct { client *smtp.Client }
func (m *SMTPMailer) SendWelcome(ctx context.Context, email string) error { ... }
```
## O — Open/Closed Principle
> "Software entities should be open for extension but closed for modification."
In Go: add new behavior by implementing an interface or adding a new type, not by modifying existing code. The canonical Go pattern is a `switch` on a string type that needs a new case every time — replace it with an interface.
**Go example:**
```go
// Bad: must modify to add new payment method
func ProcessPayment(method string, amount Money) error {
switch method {
case "stripe":
return stripeCharge(amount)
case "paypal":
return paypalCharge(amount)
// Must add cases here for every new method!
}
return errors.New("unknown payment method")
}
// Good: open for extension via new types
type PaymentProcessor interface {
Charge(ctx context.Context, amount Money) error
}
type StripeProcessor struct { client *stripe.Client }
func (p *StripeProcessor) Charge(ctx context.Context, amount Money) error { ... }
type PayPalProcessor struct { client *paypal.Client }
func (p *PayPalProcessor) Charge(ctx context.Context, amount Money) error { ... }
// Add new payment method: implement PaymentProcessor. No existing code changes.
```
## L — Liskov Substitution Principle
> "Subtypes must be substitutable for their base types without altering program correctness."
In Go: **interfaces are structural**. Any type that implements the method set satisfies the interface. This means callers should be able to use any implementation interchangeably without knowing the difference.
The key question: does your implementation honor the **contract** (documented behavior, not just method signatures)?
**Go example:**
```go
// UserStore contract: Save persists a user and returns ErrDuplicate if email exists
type UserStore interface {
Save(ctx context.Context, u User) error
GetByEmail(ctx context.Context, email string) (User, error)
}
// Good: both implementations honor the contract
type PostgresUserStore struct { ... }
func (s *PostgresUserStore) Save(ctx context.Context, u User) error { ... }
type InMemoryUserStore struct { users map[string]User }
func (s *InMemoryUserStore) Save(ctx context.Context, u User) error {
if _, exists := s.users[u.Email]; exists {
return ErrDuplicate // Must return ErrDuplicate, not some other error
}
s.users[u.Email] = u
return nil
}
// Bad: violates contract — callers of UserStore cannot substitute this
type BrokenStore struct { ... }
func (s *BrokenStore) Save(ctx context.Context, u User) error {
panic("not implemented") // Violates contract
}
```
**Go tension:** Go has no inheritance, so "refused bequest" (subclass ignoring parent methods) doesn't apply. The LSP concern in Go is about interface implementations that partially satisfy the contract through panics or no-ops.
## I — Interface Segregation Principle
> "Clients should not be forced to depend on methods they do not use."
In Go: this is idiomatic. The `io.Reader`, `io.Writer`, and `io.Closer` pattern is the model — small, focused interfaces that can be composed when needed.
**Go example:**
```go
// Bad: fat interface — callers that only read must depend on write methods too
type FileStore interface {
Read(name string) ([]byte, error)
Write(name string, data []byte) error
Delete(name string) error
List(dir string) ([]string, error)
Stats(name string) (FileInfo, error)
}
// Good: segregated interfaces — callers depend only on what they need
type FileReader interface {
Read(name string) ([]byte, error)
}
type FileWriter interface {
Write(name string, data []byte) error
}
type FileDeleter interface {
Delete(name string) error
}
// Compose when needed
type FileReadWriter interface {
FileReader
FileWriter
}
// Handler that only reads: depends on FileReader only
func NewReportHandler(store FileReader) *ReportHandler { ... }
```
**Go idiom:** Define interfaces at the point of use, not at the point of implementation. The implementation package should not define the interface — the package that consumes it should.
```go
// In the consumer package (handler)
type UserStore interface {
GetByID(ctx context.Context, id UserID) (User, error)
}
// The postgres package doesn't need to know about this interface
// It just implements the method, and Go's structural typing handles the rest
```
## D — Dependency Inversion Principle
> "High-level modules should not depend on low-level modules. Both should depend on abstractions."
In Go: pass dependencies as interface parameters. Never instantiate concrete dependencies inside a function or type that contains business logic.
```go
// Bad: high-level order service depends on low-level email implementation
type OrderService struct {
emailClient *sendgrid.Client // Locked to SendGrid
db *sql.DB // Locked to PostgreSQL
}
// Good: depends on abstractions
type Mailer interface {
Send(ctx context.Context, to, subject, body string) error
}
type OrderRepository interface {
Save(ctx context.Context, o Order) error
GetByID(ctx context.Context, id OrderID) (Order, error)
}
type OrderService struct {
repo OrderRepository
mailer Mailer
}
func NewOrderService(repo OrderRepository, mailer Mailer) *OrderService {
return &OrderService{repo: repo, mailer: mailer}
}
```
**The Dependency Rule:** Source code dependencies point **inward** toward domain logic, never outward toward infrastructure.
```
HTTP handlers → Application services → Domain types
Database layer → Application services → Domain types
Domain types know nothing about HTTP, SQL, or external APIs.
```
## Applying SOLID at Architecture Level
| Principle | Package/Module Application |
|-----------|---------------------------|
| SRP | Each package has one clear purpose |
| OCP | New features = new packages/types, not edits to existing |
| LSP | All implementations of an interface are interchangeable |
| ISP | Interfaces defined at point of use, as narrow as possible |
| DIP | Domain packages import nothing from infrastructure packages |
## Go Tensions with OOP-centric SOLID
| OOP SOLID | Go reality |
|-----------|-----------|
| Inheritance hierarchies for OCP | Use interfaces + new types instead |
| Abstract base classes for LSP | No inheritance; use interface contracts |
| Explicit interface declarations | Interfaces are implicit; define where consumed |
| "Program to an interface" as ritual | Only extract interface when you have 2+ implementations or need testability |
**Don't over-abstract.** A function that takes a `*sql.DB` directly is fine if there's only one implementation and it's never tested in isolation. Extract an interface when you need it.
## Red Flags
| Flag | Likely Violation |
|------|-----------------|
| Type that "handles X and Y and Z" | SRP |
| Large `switch` on a type string | OCP |
| Implementation that panics on some methods | LSP |
| Interface with 10+ methods | ISP |
| `new(ConcreteType)` inside business logic | DIP |
| Package imports something from `infrastructure/` | DIP |
## References
- `references/solid-principles.md` — canonical SOLID reference with TypeScript examples
- `references/go-adaptation.md` — this workspace's Go-specific rewrite of each principle
- Load `clean-code` skill for naming and structure
- Load `code-review` skill for detecting violations during review

View 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**

View 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.

View 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 |

View 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)

View 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
```

View 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 |