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]
10 KiB
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
- You recognize the problem - You've seen it before
- The pattern fits - Not forcing it
- It simplifies - Doesn't add unnecessary complexity
- 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.
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.
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.
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.
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.
// 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.
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.
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).
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.
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.
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.
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.
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:
- What problem does it solve? (Creational, Structural, Behavioral)
- What scope? (Object-level, Class-level, System-level)
- When is it applied? (Compile-time, Runtime)
- 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 |