Commit 02735b0

bryfry <bryon@fryer.io>
2025-07-24 09:46:27
Task 1.13: Implement dependency injection pattern
- Add interfaces for future Database and Mailer services - Refactor application struct as central dependency container - Create initializeApplication function to centralize dependency setup - Add validateDependencies method with comprehensive validation - Convert confirmWithToken to use dependency injection instead of hardcoded values - Remove all direct dependency access and global variables See: docs/todo/task_1.13.md
1 parent 0361002
Changed files (3)
cmd/web/handlers.go
@@ -1,12 +1,31 @@
 package main
 
 import (
+	"fmt"
 	"html/template"
 	"log"
 	"net/http"
+	"os"
 	"regexp"
 )
 
+// Interfaces for future service dependencies
+// These will be implemented when we add database and email functionality
+
+// Database represents the database service interface
+type Database interface {
+	// Future database methods will be defined here
+	// Example: GetReminder(id string) (*Reminder, error)
+	// Example: SaveReminder(reminder *Reminder) error
+}
+
+// Mailer represents the email service interface
+type Mailer interface {
+	// Future email methods will be defined here
+	// Example: SendReminder(email, productURL string) error
+	// Example: SendConfirmation(email, token string) error
+}
+
 // PageData holds common data passed to templates
 type PageData struct {
 	Title       string
@@ -23,14 +42,17 @@ type config struct {
 }
 
 // application holds the application-wide dependencies and configuration
-// This demonstrates a custom handler type that can hold state and dependencies
+// This serves as the central dependency injection container
 type application struct {
-	config config
-	logger struct {
+	config      config
+	logger      struct {
 		info  *log.Logger
 		error *log.Logger
 	}
 	serviceName string
+	// Future service dependencies (interfaces for clean injection)
+	// database Database
+	// mailer   Mailer
 }
 
 // TemplateHandler represents handlers that render templates
@@ -139,17 +161,19 @@ func (app *application) processSubmit(w http.ResponseWriter, r *http.Request) {
 }
 
 // confirmWithToken handles magic link confirmation with token validation
-// This demonstrates a stateless handler function that can be converted with http.HandlerFunc
-func confirmWithToken(w http.ResponseWriter, r *http.Request) {
+// This demonstrates dependency injection by making it a method on application
+func (app *application) confirmWithToken(w http.ResponseWriter, r *http.Request) {
 	token := r.PathValue("token")
 
 	if !isValidToken(token) {
-		w.Header().Add("Server", "buylater.email")
+		w.Header().Add("Server", app.serviceName)
+		app.logger.error.Printf("Invalid token attempted: %s", token)
 		http.NotFound(w, r)
 		return
 	}
 
-	w.Header().Add("Server", "buylater.email")
+	w.Header().Add("Server", app.serviceName)
+	app.logger.info.Printf("Token confirmed successfully: %s", token[:8]+"...")
 	w.WriteHeader(http.StatusOK)
 	w.Write([]byte("Email Confirmed! Your purchase reminder has been activated via magic link."))
 }
@@ -165,5 +189,77 @@ func isValidToken(token string) bool {
 	return matched
 }
 
+// initializeApplication creates and configures the application with all dependencies
+// This centralizes dependency injection and makes testing easier
+func initializeApplication(cfg config) *application {
+	// Create structured loggers
+	infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
+	errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
+
+	// Log startup configuration
+	infoLog.Printf("Configuration loaded: port=%d, env=%s, staticDir=%s, htmlDir=%s", 
+		cfg.port, cfg.env, cfg.staticDir, cfg.htmlDir)
+
+	// Create application with injected dependencies
+	app := &application{
+		config:      cfg,
+		logger: struct {
+			info  *log.Logger
+			error *log.Logger
+		}{
+			info:  infoLog,
+			error: errorLog,
+		},
+		serviceName: "buylater.email",
+		// Future service dependencies will be initialized here:
+		// database: initializeDatabase(cfg),
+		// mailer:   initializeMailer(cfg),
+	}
+
+	// Validate all required dependencies are properly initialized
+	if err := app.validateDependencies(); err != nil {
+		log.Fatalf("Dependency validation failed: %v", err)
+	}
+
+	app.logger.info.Println("Application dependencies initialized successfully")
+	return app
+}
+
+// validateDependencies ensures all required dependencies are properly configured
+func (app *application) validateDependencies() error {
+	// Validate configuration
+	if app.config.port <= 0 || app.config.port > 65535 {
+		return fmt.Errorf("invalid port: %d", app.config.port)
+	}
+	if app.config.env == "" {
+		return fmt.Errorf("environment not specified")
+	}
+	if app.config.staticDir == "" {
+		return fmt.Errorf("static directory not specified")
+	}
+	if app.config.htmlDir == "" {
+		return fmt.Errorf("HTML directory not specified")
+	}
+
+	// Validate loggers
+	if app.logger.info == nil {
+		return fmt.Errorf("info logger not initialized")
+	}
+	if app.logger.error == nil {
+		return fmt.Errorf("error logger not initialized")
+	}
+
+	// Validate service name
+	if app.serviceName == "" {
+		return fmt.Errorf("service name not specified")
+	}
+
+	// Future dependency validation will be added here:
+	// if app.database == nil { return fmt.Errorf("database not initialized") }
+	// if app.mailer == nil { return fmt.Errorf("mailer not initialized") }
+
+	return nil
+}
+
 // about displays information about the buylater.email service
 // This will be handled by the TemplateHandler type to demonstrate reusable custom handlers
cmd/web/main.go
@@ -35,27 +35,8 @@ func main() {
 		cfg.htmlDir = envHtmlDir
 	}
 
-	// Create loggers for different types of messages
-	infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
-	errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
-
-	// Log startup configuration
-	infoLog.Printf("Configuration loaded: port=%d, env=%s, staticDir=%s, htmlDir=%s", 
-		cfg.port, cfg.env, cfg.staticDir, cfg.htmlDir)
-
-	// Create an instance of the application struct, containing the application-wide
-	// dependencies and configuration settings.
-	app := &application{
-		config: cfg,
-		logger: struct {
-			info  *log.Logger
-			error *log.Logger
-		}{
-			info:  infoLog,
-			error: errorLog,
-		},
-		serviceName: "buylater.email",
-	}
+	// Initialize application dependencies
+	app := initializeApplication(cfg)
 
 	// Initialize a new servemux (router) - this stores the mapping between
 	// URL patterns and their corresponding handlers.
@@ -87,8 +68,8 @@ func main() {
 	// 3. Method handler with business logic
 	mux.Handle("POST /submit", http.HandlerFunc(app.processSubmit))
 
-	// 4. Simple function handler converted to http.Handler
-	mux.Handle("GET /confirm/{token}", http.HandlerFunc(confirmWithToken))
+	// 4. Method handler with dependency injection
+	mux.Handle("GET /confirm/{token}", http.HandlerFunc(app.confirmWithToken))
 
 	// 5. Another custom handler type instance for the about page
 	mux.Handle("GET /about", &TemplateHandler{
docs/project_plan.md
@@ -20,7 +20,7 @@ Phase 1 lays the groundwork by methodically working through the Let's Go book, c
 | 1.10 | The HTTP Handler Interface              | Completed | Medium | 2.10    | [task_1.10.md](todo/task_1.10.md) |
 | 1.11 | Managing Configuration Settings         | Completed | Medium | 3.1     | [task_1.11.md](todo/task_1.11.md) |
 | 1.12 | Structured Logging                      | Completed | Medium | 3.2     | [task_1.12.md](todo/task_1.12.md) |
-| 1.13 | Dependency Injection                    | Pending   | Medium | 3.3     | [task_1.13.md](todo/task_1.13.md) |
+| 1.13 | Dependency Injection                    | Completed | Medium | 3.3     | [task_1.13.md](todo/task_1.13.md) |
 | 1.14 | Centralized Error Handling             | Pending   | Medium | 3.4     | [task_1.14.md](todo/task_1.14.md) |
 | 1.15 | Isolating the Application Routes        | Pending   | Small  | 3.5     | [task_1.15.md](todo/task_1.15.md) |