docs: Add comprehensive system architecture diagrams
Created detailed ASCII diagrams documenting the entire system architecture: 1. System Architecture (_go-learning/diagrams/01-system-architecture.md) - Overall system architecture with client/server/storage layers - Layered architecture (Presentation → Application → Business → Data) - Component interaction and HTTP request flow - Data flow from app start through per-request lifecycle - Package dependencies and file organization
This commit is contained in:
@@ -0,0 +1,425 @@
|
||||
# Middleware Pattern in Go
|
||||
|
||||
## Pattern Overview
|
||||
|
||||
The Middleware Pattern wraps HTTP handlers to add cross-cutting concerns like logging, authentication, error recovery, and request preprocessing. It follows the decorator pattern, allowing you to compose multiple middleware into a chain.
|
||||
|
||||
## Pattern Structure
|
||||
|
||||
```go
|
||||
// Middleware function signature
|
||||
type Middleware func(http.Handler) http.Handler
|
||||
|
||||
// Middleware wraps a handler
|
||||
func MyMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Pre-processing (before handler)
|
||||
// ... do something before
|
||||
|
||||
// Call next handler
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
// Post-processing (after handler)
|
||||
// ... do something after
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Real Implementation from Project
|
||||
|
||||
### Preferences Middleware
|
||||
|
||||
```go
|
||||
// internal/middleware/preferences.go
|
||||
|
||||
// PreferencesMiddleware reads user preference cookies and stores them in request context
|
||||
func PreferencesMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Pre-processing: Read cookies
|
||||
prefs := &Preferences{
|
||||
CVLength: getCookieWithDefault(r, "cv-length", "short"),
|
||||
CVIcons: getCookieWithDefault(r, "cv-icons", "show"),
|
||||
CVLanguage: getCookieWithDefault(r, "cv-language", "en"),
|
||||
CVTheme: getCookieWithDefault(r, "cv-theme", "default"),
|
||||
ColorTheme: getCookieWithDefault(r, "color-theme", "light"),
|
||||
}
|
||||
|
||||
// Migrate old values
|
||||
if prefs.CVLength == "extended" {
|
||||
prefs.CVLength = "long"
|
||||
}
|
||||
|
||||
// Store in context
|
||||
ctx := context.WithValue(r.Context(), PreferencesKey, prefs)
|
||||
|
||||
// Call next handler with modified context
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
|
||||
// No post-processing needed for this middleware
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Recovery Middleware
|
||||
|
||||
```go
|
||||
// internal/middleware/recovery.go
|
||||
|
||||
// Recovery catches panics and returns 500 error
|
||||
func Recovery(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Setup panic recovery
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// Log panic with stack trace
|
||||
log.Printf("PANIC: %v\n%s", err, debug.Stack())
|
||||
|
||||
// Return error response
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
}()
|
||||
|
||||
// Call next handler (protected by defer/recover)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Logger Middleware
|
||||
|
||||
```go
|
||||
// internal/middleware/logger.go
|
||||
|
||||
// Logger logs HTTP requests and their duration
|
||||
func Logger(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Pre-processing: Start timer and log request
|
||||
start := time.Now()
|
||||
log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
|
||||
|
||||
// Wrap ResponseWriter to capture status code
|
||||
wrapped := &responseWriter{
|
||||
ResponseWriter: w,
|
||||
statusCode: http.StatusOK,
|
||||
}
|
||||
|
||||
// Call next handler
|
||||
next.ServeHTTP(wrapped, r)
|
||||
|
||||
// Post-processing: Log duration and status
|
||||
duration := time.Since(start)
|
||||
log.Printf("Completed in %v (status: %d)", duration, wrapped.statusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// Helper to capture response status
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (rw *responseWriter) WriteHeader(code int) {
|
||||
rw.statusCode = code
|
||||
rw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
```
|
||||
|
||||
## Middleware Composition
|
||||
|
||||
### Chaining Middleware
|
||||
|
||||
```go
|
||||
// internal/routes/routes.go
|
||||
|
||||
func Setup(cvHandler *handlers.CVHandler, healthHandler *handlers.HealthHandler) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Register routes
|
||||
mux.HandleFunc("/", cvHandler.Home)
|
||||
mux.HandleFunc("/cv", cvHandler.CVContent)
|
||||
mux.HandleFunc("/health", healthHandler.Health)
|
||||
|
||||
// Compose middleware chain
|
||||
// Execution order: Recovery → Logger → SecurityHeaders → Preferences → mux
|
||||
handler := middleware.Recovery(
|
||||
middleware.Logger(
|
||||
middleware.SecurityHeaders(
|
||||
middleware.PreferencesMiddleware(mux),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
return handler
|
||||
}
|
||||
```
|
||||
|
||||
### Route-Specific Middleware
|
||||
|
||||
```go
|
||||
// Apply middleware only to specific routes
|
||||
func Setup(cvHandler *handlers.CVHandler) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Public routes (minimal middleware)
|
||||
mux.HandleFunc("/", cvHandler.Home)
|
||||
mux.HandleFunc("/health", healthHandler.Health)
|
||||
|
||||
// Protected PDF route (additional middleware)
|
||||
pdfHandler := middleware.OriginChecker(
|
||||
middleware.RateLimiter(
|
||||
http.HandlerFunc(cvHandler.ExportPDF),
|
||||
3, // 3 requests per minute
|
||||
),
|
||||
)
|
||||
mux.Handle("/export/pdf", pdfHandler)
|
||||
|
||||
// Global middleware for all routes
|
||||
handler := middleware.Recovery(
|
||||
middleware.Logger(
|
||||
middleware.PreferencesMiddleware(mux),
|
||||
),
|
||||
)
|
||||
|
||||
return handler
|
||||
}
|
||||
```
|
||||
|
||||
## Common Middleware Use Cases
|
||||
|
||||
### 1. Authentication
|
||||
|
||||
```go
|
||||
func AuthMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Get token from header
|
||||
token := r.Header.Get("Authorization")
|
||||
|
||||
// Validate token
|
||||
userID, err := validateToken(token)
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Store user ID in context
|
||||
ctx := context.WithValue(r.Context(), UserIDKey, userID)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 2. CORS
|
||||
|
||||
```go
|
||||
func CORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Set CORS headers
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
// Handle preflight
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Request Timeout
|
||||
|
||||
```go
|
||||
func Timeout(duration time.Duration) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(r.Context(), duration)
|
||||
defer cancel()
|
||||
|
||||
// Create channel for handler completion
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Wait for completion or timeout
|
||||
select {
|
||||
case <-done:
|
||||
// Handler completed
|
||||
case <-ctx.Done():
|
||||
// Timeout occurred
|
||||
http.Error(w, "Request Timeout", http.StatusGatewayTimeout)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Request ID
|
||||
|
||||
```go
|
||||
func RequestID(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Generate unique ID
|
||||
requestID := uuid.New().String()
|
||||
|
||||
// Add to response header
|
||||
w.Header().Set("X-Request-ID", requestID)
|
||||
|
||||
// Store in context
|
||||
ctx := context.WithValue(r.Context(), RequestIDKey, requestID)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Middleware Execution Flow
|
||||
|
||||
```
|
||||
Request
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ Recovery Middleware │ ← Outermost (catches all panics)
|
||||
│ defer/recover │
|
||||
└─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ Logger Middleware │ ← Logs request + duration
|
||||
│ Pre: Log request │
|
||||
│ Post: Log duration │
|
||||
└─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ Security Middleware │ ← Add security headers
|
||||
│ Set headers │
|
||||
└─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ Preferences Middleware │ ← Innermost (closest to handler)
|
||||
│ Read cookies → context │
|
||||
└─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ Handler │ ← Business logic
|
||||
│ Process request │
|
||||
└─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
Response (unwraps in reverse order)
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Separation of Concerns**: Cross-cutting logic separate from handlers
|
||||
2. **Composability**: Chain multiple middleware together
|
||||
3. **Reusability**: Same middleware for multiple routes
|
||||
4. **Testability**: Easy to test in isolation
|
||||
5. **Maintainability**: Change behavior without touching handlers
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO
|
||||
|
||||
```go
|
||||
// Keep middleware focused on one concern
|
||||
func LoggerMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Only logging logic here
|
||||
log.Printf("[%s] %s", r.Method, r.URL.Path)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// Use context for request-scoped values
|
||||
func PreferencesMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
prefs := readPreferences(r)
|
||||
ctx := context.WithValue(r.Context(), PrefsKey, prefs)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// Order middleware correctly (outer to inner)
|
||||
handler := Recovery(Logger(Auth(mux)))
|
||||
```
|
||||
|
||||
### ❌ DON'T
|
||||
|
||||
```go
|
||||
// DON'T mix multiple concerns in one middleware
|
||||
func BadMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Too much! Logging, auth, CORS, caching...
|
||||
log.Print(r.URL)
|
||||
if !checkAuth(r) { return }
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
cached := getCache(r.URL.Path)
|
||||
// ...
|
||||
})
|
||||
}
|
||||
|
||||
// DON'T store context in struct
|
||||
type BadMiddleware struct {
|
||||
ctx context.Context // Wrong!
|
||||
}
|
||||
|
||||
// DON'T modify original request (use r.WithContext)
|
||||
func BadMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Header.Set("X-Foo", "bar") // Modifies original!
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Middleware
|
||||
|
||||
```go
|
||||
func TestPreferencesMiddleware(t *testing.T) {
|
||||
// Create test handler that reads preferences
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
prefs := GetPreferences(r)
|
||||
if prefs.CVLength != "long" {
|
||||
t.Errorf("expected long, got %s", prefs.CVLength)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
// Wrap with middleware
|
||||
wrapped := PreferencesMiddleware(handler)
|
||||
|
||||
// Create test request with cookie
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.AddCookie(&http.Cookie{Name: "cv-length", Value: "long"})
|
||||
|
||||
// Execute
|
||||
w := httptest.NewRecorder()
|
||||
wrapped.ServeHTTP(w, req)
|
||||
|
||||
// Verify
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Related Patterns
|
||||
|
||||
- **Chain of Responsibility**: Middleware is a specific implementation
|
||||
- **Decorator Pattern**: Wrapping handlers adds behavior
|
||||
- **Context Pattern**: Often used together for request-scoped data
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Writing Middleware in Go](https://www.alexedwards.net/blog/making-and-using-middleware)
|
||||
- [Middleware Pattern in Go](https://gowebexamples.com/advanced-middleware/)
|
||||
- [Context Pattern](./03-context-pattern.md) - Used with middleware
|
||||
@@ -0,0 +1,219 @@
|
||||
# Go Patterns Used in This Project
|
||||
|
||||
This directory contains documentation on the Go design patterns and idioms used throughout the CV website project.
|
||||
|
||||
## Pattern Catalog
|
||||
|
||||
1. **[Middleware Pattern](./01-middleware-pattern.md)** - HTTP middleware chain for cross-cutting concerns
|
||||
2. **[Handler Pattern](./02-handler-pattern.md)** - Organized HTTP handler structure
|
||||
3. **[Context Pattern](./03-context-pattern.md)** - Request-scoped values using context
|
||||
4. **[Error Wrapping](./04-error-wrapping.md)** - Structured error handling with wrapping
|
||||
5. **[Dependency Injection](./05-dependency-injection.md)** - Constructor-based dependency injection
|
||||
6. **[Template Pattern](./06-template-pattern.md)** - Cached template management
|
||||
7. **[Singleton Pattern](./07-singleton-pattern.md)** - Single instance managers (template, config)
|
||||
8. **[Factory Pattern](./08-factory-pattern.md)** - Error and response constructors
|
||||
|
||||
## Pattern Categories
|
||||
|
||||
### Structural Patterns
|
||||
- **Middleware Pattern** - Composable request processing
|
||||
- **Singleton Pattern** - Single instance coordination
|
||||
- **Dependency Injection** - Decoupled component initialization
|
||||
|
||||
### Behavioral Patterns
|
||||
- **Handler Pattern** - Request routing and handling
|
||||
- **Context Pattern** - Request-scoped data propagation
|
||||
- **Template Pattern** - Flexible rendering engine
|
||||
|
||||
### Error Handling Patterns
|
||||
- **Error Wrapping** - Context-rich error chains
|
||||
- **Typed Errors** - Domain-specific error types
|
||||
- **Factory Pattern** - Consistent error creation
|
||||
|
||||
## Pattern Usage Map
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Pattern Usage Map │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
|
||||
main.go
|
||||
├─→ Singleton Pattern (config, template manager)
|
||||
├─→ Dependency Injection (handler construction)
|
||||
└─→ Middleware Pattern (chain setup)
|
||||
|
||||
internal/handlers/
|
||||
├─→ Handler Pattern (method organization)
|
||||
├─→ Error Wrapping (error handling)
|
||||
├─→ Factory Pattern (error/response creation)
|
||||
└─→ Context Pattern (preference access)
|
||||
|
||||
internal/middleware/
|
||||
├─→ Middleware Pattern (http.Handler wrapping)
|
||||
├─→ Context Pattern (value storage)
|
||||
└─→ Error Wrapping (panic recovery)
|
||||
|
||||
internal/templates/
|
||||
├─→ Singleton Pattern (manager instance)
|
||||
├─→ Template Pattern (rendering strategy)
|
||||
└─→ Dependency Injection (config injection)
|
||||
|
||||
internal/models/
|
||||
├─→ Factory Pattern (model loading)
|
||||
└─→ Error Wrapping (validation errors)
|
||||
```
|
||||
|
||||
## When to Use Each Pattern
|
||||
|
||||
### Middleware Pattern
|
||||
✓ Cross-cutting concerns (logging, auth, CORS)
|
||||
✓ Request/response modification
|
||||
✓ Chain-of-responsibility needs
|
||||
✗ Business logic (use handlers instead)
|
||||
|
||||
### Handler Pattern
|
||||
✓ HTTP request handling
|
||||
✓ Route-specific logic
|
||||
✓ Organizing endpoints by resource
|
||||
✗ Generic utilities (use packages instead)
|
||||
|
||||
### Context Pattern
|
||||
✓ Request-scoped values (user, preferences)
|
||||
✓ Cancellation signals
|
||||
✓ Deadlines and timeouts
|
||||
✗ Function parameters (use explicit params)
|
||||
|
||||
### Error Wrapping
|
||||
✓ Adding context to errors
|
||||
✓ Preserving error chains
|
||||
✓ Debug information
|
||||
✗ Simple errors (use errors.New)
|
||||
|
||||
### Dependency Injection
|
||||
✓ Decoupling components
|
||||
✓ Testing with mocks
|
||||
✓ Configuration flexibility
|
||||
✗ Simple functions (use direct calls)
|
||||
|
||||
### Template Pattern
|
||||
✓ Flexible rendering
|
||||
✓ HTML generation
|
||||
✓ Hot reload in development
|
||||
✗ JSON APIs (use direct encoding)
|
||||
|
||||
### Singleton Pattern
|
||||
✓ Shared resources (DB, cache)
|
||||
✓ Configuration managers
|
||||
✓ Template engines
|
||||
✗ Stateless utilities (use packages)
|
||||
|
||||
### Factory Pattern
|
||||
✓ Complex object creation
|
||||
✓ Consistent initialization
|
||||
✓ Error construction
|
||||
✗ Simple structs (use literals)
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### ❌ Global State
|
||||
```go
|
||||
// BAD: Mutable global variable
|
||||
var globalConfig Config
|
||||
|
||||
// GOOD: Pass as dependency
|
||||
func NewHandler(config *Config) *Handler
|
||||
```
|
||||
|
||||
### ❌ Panic for Flow Control
|
||||
```go
|
||||
// BAD: Using panic for expected errors
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// GOOD: Return errors
|
||||
if err != nil {
|
||||
return fmt.Errorf("operation failed: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Ignoring Errors
|
||||
```go
|
||||
// BAD: Ignoring error
|
||||
_ = json.Unmarshal(data, &result)
|
||||
|
||||
// GOOD: Handle error
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return fmt.Errorf("unmarshal: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Context in Structs
|
||||
```go
|
||||
// BAD: Storing context in struct
|
||||
type Handler struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// GOOD: Pass context as first parameter
|
||||
func (h *Handler) Handle(ctx context.Context, w, r)
|
||||
```
|
||||
|
||||
### ❌ Naked Returns
|
||||
```go
|
||||
// BAD: Naked return with named results
|
||||
func process() (result string, err error) {
|
||||
result = "foo"
|
||||
return // Confusing!
|
||||
}
|
||||
|
||||
// GOOD: Explicit return
|
||||
func process() (string, error) {
|
||||
result := "foo"
|
||||
return result, nil
|
||||
}
|
||||
```
|
||||
|
||||
## Learning Path
|
||||
|
||||
For developers new to these patterns:
|
||||
|
||||
1. **Start with**: Handler Pattern, Error Wrapping
|
||||
2. **Then learn**: Middleware Pattern, Context Pattern
|
||||
3. **Advanced**: Dependency Injection, Template Pattern
|
||||
4. **Master**: Singleton Pattern, Factory Pattern
|
||||
|
||||
## Resources
|
||||
|
||||
- [Effective Go](https://golang.org/doc/effective_go) - Official Go style guide
|
||||
- [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) - Common mistakes
|
||||
- [Practical Go](https://dave.cheney.net/practical-go) - Best practices
|
||||
|
||||
## Pattern Evolution
|
||||
|
||||
This project evolved through these pattern adoptions:
|
||||
|
||||
### Phase 1: Basic Structure
|
||||
- Simple handlers
|
||||
- No middleware
|
||||
- Manual cookie reading
|
||||
|
||||
### Phase 2: Middleware Introduction
|
||||
- PreferencesMiddleware added
|
||||
- Cookie handling centralized
|
||||
- Context pattern adopted
|
||||
|
||||
### Phase 3: Type Safety
|
||||
- Request/response types
|
||||
- Validation tags
|
||||
- Typed errors
|
||||
|
||||
### Phase 4: Error Handling
|
||||
- Error wrapping throughout
|
||||
- Domain error types
|
||||
- Centralized error handler
|
||||
|
||||
### Phase 5: Testing
|
||||
- Dependency injection for testability
|
||||
- Mock-friendly interfaces
|
||||
- Benchmark tests
|
||||
Reference in New Issue
Block a user