Files
skills/solid/SKILL.md
Mathias d6a71e370e
Some checks failed
release / tag (push) Has been cancelled
chore: bootstrap skills library — 19 skills + installer + CI auto-tag
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]
2026-05-24 14:59:54 +02:00

260 lines
9.3 KiB
Markdown

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