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

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 |