637 lines
14 KiB
Markdown
637 lines
14 KiB
Markdown
|
|
# Template Pattern in Go
|
||
|
|
|
||
|
|
## Pattern Overview
|
||
|
|
|
||
|
|
The Template Pattern (not to be confused with Go's `html/template` package) defines the skeleton of an algorithm in a method, deferring some steps to subclasses or functions. In Go, this is often implemented through interfaces and composition rather than inheritance.
|
||
|
|
|
||
|
|
In this project's context, we also use Go's template system which provides a different kind of template pattern for rendering HTML.
|
||
|
|
|
||
|
|
## Pattern Structure
|
||
|
|
|
||
|
|
```go
|
||
|
|
// Abstract template algorithm
|
||
|
|
type Processor interface {
|
||
|
|
Process() error
|
||
|
|
Validate() error
|
||
|
|
Transform() error
|
||
|
|
Save() error
|
||
|
|
}
|
||
|
|
|
||
|
|
// Concrete implementation
|
||
|
|
type DataProcessor struct {
|
||
|
|
// fields
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *DataProcessor) Process() error {
|
||
|
|
// Template method defines the algorithm
|
||
|
|
if err := p.Validate(); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if err := p.Transform(); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
return p.Save()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Steps can be customized
|
||
|
|
func (p *DataProcessor) Validate() error {
|
||
|
|
// Custom validation
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Real Implementation: Template Manager
|
||
|
|
|
||
|
|
### Template Manager Structure
|
||
|
|
|
||
|
|
```go
|
||
|
|
// internal/templates/manager.go
|
||
|
|
|
||
|
|
// Manager handles template rendering
|
||
|
|
type Manager struct {
|
||
|
|
templates map[string]*template.Template
|
||
|
|
config *config.TemplateConfig
|
||
|
|
mu sync.RWMutex
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewManager creates and initializes template manager
|
||
|
|
func NewManager(config *config.TemplateConfig) (*Manager, error) {
|
||
|
|
m := &Manager{
|
||
|
|
templates: make(map[string]*template.Template),
|
||
|
|
config: config,
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load templates on initialization
|
||
|
|
if err := m.loadTemplates(); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
return m, nil
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Template Loading Algorithm
|
||
|
|
|
||
|
|
```go
|
||
|
|
// loadTemplates follows a template algorithm pattern
|
||
|
|
func (m *Manager) loadTemplates() error {
|
||
|
|
// Step 1: Find template files
|
||
|
|
files, err := filepath.Glob(m.config.Dir + "/*.html")
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("glob templates: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 2: For each template file
|
||
|
|
for _, file := range files {
|
||
|
|
name := filepath.Base(file)
|
||
|
|
|
||
|
|
// Step 3: Create new template
|
||
|
|
tmpl := template.New(name)
|
||
|
|
|
||
|
|
// Step 4: Add custom functions
|
||
|
|
tmpl = tmpl.Funcs(m.customFunctions())
|
||
|
|
|
||
|
|
// Step 5: Parse main template
|
||
|
|
tmpl, err = tmpl.ParseFiles(file)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("parse template %s: %w", name, err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 6: Parse partials
|
||
|
|
partialsPattern := filepath.Join(m.config.PartialsDir, "*.html")
|
||
|
|
tmpl, err = tmpl.ParseGlob(partialsPattern)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("parse partials: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 7: Cache template
|
||
|
|
m.templates[name] = tmpl
|
||
|
|
}
|
||
|
|
|
||
|
|
log.Printf("Loaded %d templates", len(m.templates))
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Template Rendering Algorithm
|
||
|
|
|
||
|
|
```go
|
||
|
|
// Render follows a consistent algorithm for all templates
|
||
|
|
func (m *Manager) Render(w io.Writer, name string, data interface{}) error {
|
||
|
|
// Step 1: Acquire read lock
|
||
|
|
m.mu.RLock()
|
||
|
|
defer m.mu.RUnlock()
|
||
|
|
|
||
|
|
// Step 2: Hot reload check (development)
|
||
|
|
if m.config.HotReload {
|
||
|
|
// Temporarily upgrade to write lock
|
||
|
|
m.mu.RUnlock()
|
||
|
|
m.mu.Lock()
|
||
|
|
m.loadTemplates() // Reload templates
|
||
|
|
m.mu.Unlock()
|
||
|
|
m.mu.RLock()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 3: Get template from cache
|
||
|
|
tmpl, ok := m.templates[name]
|
||
|
|
if !ok {
|
||
|
|
return fmt.Errorf("template not found: %s", name)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 4: Execute template
|
||
|
|
err := tmpl.Execute(w, data)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("template execution: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Custom Functions
|
||
|
|
|
||
|
|
```go
|
||
|
|
// customFunctions returns template helper functions
|
||
|
|
func (m *Manager) customFunctions() template.FuncMap {
|
||
|
|
return template.FuncMap{
|
||
|
|
// String manipulation
|
||
|
|
"lower": strings.ToLower,
|
||
|
|
"upper": strings.ToUpper,
|
||
|
|
"title": strings.Title,
|
||
|
|
|
||
|
|
// Date formatting
|
||
|
|
"formatDate": func(date string) string {
|
||
|
|
if date == "" {
|
||
|
|
return "Present"
|
||
|
|
}
|
||
|
|
t, err := time.Parse("2006-01", date)
|
||
|
|
if err != nil {
|
||
|
|
return date
|
||
|
|
}
|
||
|
|
return t.Format("Jan 2006")
|
||
|
|
},
|
||
|
|
|
||
|
|
// Collections
|
||
|
|
"join": strings.Join,
|
||
|
|
|
||
|
|
// Conditionals
|
||
|
|
"eq": func(a, b interface{}) bool {
|
||
|
|
return a == b
|
||
|
|
},
|
||
|
|
|
||
|
|
// HTML
|
||
|
|
"safe": func(s string) template.HTML {
|
||
|
|
return template.HTML(s)
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Template Method Pattern Example
|
||
|
|
|
||
|
|
### Data Processing Pipeline
|
||
|
|
|
||
|
|
```go
|
||
|
|
// DataProcessor defines template method
|
||
|
|
type DataProcessor struct {
|
||
|
|
data []byte
|
||
|
|
}
|
||
|
|
|
||
|
|
// Process is the template method (algorithm skeleton)
|
||
|
|
func (p *DataProcessor) Process() error {
|
||
|
|
// Step 1: Validate
|
||
|
|
if err := p.Validate(); err != nil {
|
||
|
|
return fmt.Errorf("validation: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 2: Parse
|
||
|
|
parsed, err := p.Parse()
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("parsing: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 3: Transform
|
||
|
|
transformed, err := p.Transform(parsed)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("transform: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 4: Save
|
||
|
|
if err := p.Save(transformed); err != nil {
|
||
|
|
return fmt.Errorf("save: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Customizable steps
|
||
|
|
func (p *DataProcessor) Validate() error {
|
||
|
|
if len(p.data) == 0 {
|
||
|
|
return errors.New("empty data")
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *DataProcessor) Parse() (interface{}, error) {
|
||
|
|
var result interface{}
|
||
|
|
err := json.Unmarshal(p.data, &result)
|
||
|
|
return result, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *DataProcessor) Transform(data interface{}) (interface{}, error) {
|
||
|
|
// Transform logic
|
||
|
|
return data, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *DataProcessor) Save(data interface{}) error {
|
||
|
|
// Save logic
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Interface-Based Template Method
|
||
|
|
|
||
|
|
```go
|
||
|
|
// Define steps as interface
|
||
|
|
type Validator interface {
|
||
|
|
Validate() error
|
||
|
|
}
|
||
|
|
|
||
|
|
type Parser interface {
|
||
|
|
Parse([]byte) (interface{}, error)
|
||
|
|
}
|
||
|
|
|
||
|
|
type Transformer interface {
|
||
|
|
Transform(interface{}) (interface{}, error)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Pipeline uses interfaces for customization
|
||
|
|
type Pipeline struct {
|
||
|
|
validator Validator
|
||
|
|
parser Parser
|
||
|
|
transformer Transformer
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewPipeline(v Validator, p Parser, t Transformer) *Pipeline {
|
||
|
|
return &Pipeline{
|
||
|
|
validator: v,
|
||
|
|
parser: p,
|
||
|
|
transformer: t,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Process is template method
|
||
|
|
func (p *Pipeline) Process(data []byte) (interface{}, error) {
|
||
|
|
// Fixed algorithm, customizable steps
|
||
|
|
if err := p.validator.Validate(); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
parsed, err := p.parser.Parse(data)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
result, err := p.transformer.Transform(parsed)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
return result, nil
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Template Pattern in Handler Processing
|
||
|
|
|
||
|
|
### Request Processing Template
|
||
|
|
|
||
|
|
```go
|
||
|
|
// Handler follows template method for all requests
|
||
|
|
func (h *CVHandler) processRequest(
|
||
|
|
w http.ResponseWriter,
|
||
|
|
r *http.Request,
|
||
|
|
templateName string,
|
||
|
|
) error {
|
||
|
|
// Step 1: Get preferences (same for all)
|
||
|
|
prefs := middleware.GetPreferences(r)
|
||
|
|
|
||
|
|
// Step 2: Validate language (same for all)
|
||
|
|
lang := r.URL.Query().Get("lang")
|
||
|
|
if lang == "" {
|
||
|
|
lang = prefs.CVLanguage
|
||
|
|
}
|
||
|
|
if err := validateLanguage(lang); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 3: Prepare data (same algorithm, different data)
|
||
|
|
data, err := h.prepareTemplateData(lang)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 4: Render template (different template name)
|
||
|
|
if err := h.tmpl.Render(w, templateName, data); err != nil {
|
||
|
|
return TemplateError(err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handlers use the template
|
||
|
|
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if err := h.processRequest(w, r, "index.html"); err != nil {
|
||
|
|
h.HandleError(w, r, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *CVHandler) CVContent(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if err := h.processRequest(w, r, "partials/cv_content.html"); err != nil {
|
||
|
|
h.HandleError(w, r, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Function-Based Template Pattern
|
||
|
|
|
||
|
|
### Using Higher-Order Functions
|
||
|
|
|
||
|
|
```go
|
||
|
|
// Template function accepts customization functions
|
||
|
|
func ProcessWithTemplate(
|
||
|
|
validate func() error,
|
||
|
|
transform func() (interface{}, error),
|
||
|
|
save func(interface{}) error,
|
||
|
|
) error {
|
||
|
|
// Template algorithm
|
||
|
|
if err := validate(); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
data, err := transform()
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
return save(data)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage with closures
|
||
|
|
err := ProcessWithTemplate(
|
||
|
|
func() error {
|
||
|
|
// Custom validation
|
||
|
|
return validateInput(input)
|
||
|
|
},
|
||
|
|
func() (interface{}, error) {
|
||
|
|
// Custom transformation
|
||
|
|
return transformData(input)
|
||
|
|
},
|
||
|
|
func(data interface{}) error {
|
||
|
|
// Custom save
|
||
|
|
return db.Save(data)
|
||
|
|
},
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
## Template Caching Pattern
|
||
|
|
|
||
|
|
### Cache Management
|
||
|
|
|
||
|
|
```go
|
||
|
|
// Template cache with thread-safe access
|
||
|
|
type TemplateCache struct {
|
||
|
|
templates map[string]*template.Template
|
||
|
|
mu sync.RWMutex
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get retrieves from cache (or loads if missing)
|
||
|
|
func (c *TemplateCache) Get(name string) (*template.Template, error) {
|
||
|
|
// Try read lock first
|
||
|
|
c.mu.RLock()
|
||
|
|
tmpl, ok := c.templates[name]
|
||
|
|
c.mu.RUnlock()
|
||
|
|
|
||
|
|
if ok {
|
||
|
|
return tmpl, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Not found, load with write lock
|
||
|
|
c.mu.Lock()
|
||
|
|
defer c.mu.Unlock()
|
||
|
|
|
||
|
|
// Double-check after acquiring write lock
|
||
|
|
if tmpl, ok := c.templates[name]; ok {
|
||
|
|
return tmpl, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load template
|
||
|
|
tmpl, err := template.ParseFiles(name)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
// Cache it
|
||
|
|
c.templates[name] = tmpl
|
||
|
|
return tmpl, nil
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Benefits
|
||
|
|
|
||
|
|
1. **Consistency**: Algorithm is consistent across all uses
|
||
|
|
2. **Customization**: Steps can be customized without changing algorithm
|
||
|
|
3. **Code Reuse**: Common algorithm logic is reused
|
||
|
|
4. **Maintainability**: Changes to algorithm are centralized
|
||
|
|
5. **Testability**: Steps can be tested independently
|
||
|
|
|
||
|
|
## Real-World Use Cases
|
||
|
|
|
||
|
|
### 1. HTTP Request Processing
|
||
|
|
|
||
|
|
```go
|
||
|
|
// All requests follow same template
|
||
|
|
func (h *Handler) handleRequest(
|
||
|
|
w http.ResponseWriter,
|
||
|
|
r *http.Request,
|
||
|
|
process func() (interface{}, error),
|
||
|
|
) {
|
||
|
|
// 1. Authentication
|
||
|
|
user := authenticate(r)
|
||
|
|
|
||
|
|
// 2. Authorization
|
||
|
|
if !authorize(user, r) {
|
||
|
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. Process (customizable)
|
||
|
|
result, err := process()
|
||
|
|
if err != nil {
|
||
|
|
h.handleError(w, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// 4. Respond
|
||
|
|
json.NewEncoder(w).Encode(result)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Data Migration
|
||
|
|
|
||
|
|
```go
|
||
|
|
// Migration template
|
||
|
|
type Migration interface {
|
||
|
|
Up() error
|
||
|
|
Down() error
|
||
|
|
}
|
||
|
|
|
||
|
|
type MigrationRunner struct {
|
||
|
|
migrations []Migration
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *MigrationRunner) Run() error {
|
||
|
|
for _, m := range r.migrations {
|
||
|
|
// Template: Begin → Execute → Commit/Rollback
|
||
|
|
tx := db.Begin()
|
||
|
|
|
||
|
|
if err := m.Up(); err != nil {
|
||
|
|
tx.Rollback()
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
tx.Commit()
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Test Setup/Teardown
|
||
|
|
|
||
|
|
```go
|
||
|
|
// Test template
|
||
|
|
type TestCase struct {
|
||
|
|
Name string
|
||
|
|
Setup func() error
|
||
|
|
Run func() error
|
||
|
|
Teardown func() error
|
||
|
|
}
|
||
|
|
|
||
|
|
func RunTestCase(tc *TestCase) error {
|
||
|
|
// Template algorithm
|
||
|
|
if err := tc.Setup(); err != nil {
|
||
|
|
return fmt.Errorf("setup: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
err := tc.Run()
|
||
|
|
|
||
|
|
// Always teardown, even on error
|
||
|
|
if teardownErr := tc.Teardown(); teardownErr != nil {
|
||
|
|
return fmt.Errorf("teardown: %w", teardownErr)
|
||
|
|
}
|
||
|
|
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Best Practices
|
||
|
|
|
||
|
|
### ✅ DO
|
||
|
|
|
||
|
|
```go
|
||
|
|
// Define clear algorithm skeleton
|
||
|
|
func (p *Processor) Process() error {
|
||
|
|
if err := p.step1(); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if err := p.step2(); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
return p.step3()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Use interfaces for flexibility
|
||
|
|
type Step interface {
|
||
|
|
Execute() error
|
||
|
|
}
|
||
|
|
|
||
|
|
// Document the template algorithm
|
||
|
|
// Process executes the full processing pipeline:
|
||
|
|
// 1. Validate input
|
||
|
|
// 2. Transform data
|
||
|
|
// 3. Save result
|
||
|
|
func (p *Processor) Process() error {
|
||
|
|
// ...
|
||
|
|
}
|
||
|
|
|
||
|
|
// Make steps testable independently
|
||
|
|
func TestValidate(t *testing.T) {
|
||
|
|
p := &Processor{}
|
||
|
|
err := p.Validate()
|
||
|
|
// test validation logic
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### ❌ DON'T
|
||
|
|
|
||
|
|
```go
|
||
|
|
// DON'T make algorithm too rigid
|
||
|
|
// Allow customization where appropriate
|
||
|
|
|
||
|
|
// DON'T mix concerns
|
||
|
|
// Keep template method focused on algorithm,
|
||
|
|
// not implementation details
|
||
|
|
|
||
|
|
// DON'T over-complicate
|
||
|
|
// If algorithm is simple, don't force template pattern
|
||
|
|
```
|
||
|
|
|
||
|
|
## Testing Template Methods
|
||
|
|
|
||
|
|
```go
|
||
|
|
func TestTemplateManager_Render(t *testing.T) {
|
||
|
|
// Test template algorithm
|
||
|
|
cfg := &config.TemplateConfig{
|
||
|
|
Dir: "testdata/templates",
|
||
|
|
PartialsDir: "testdata/partials",
|
||
|
|
HotReload: false,
|
||
|
|
}
|
||
|
|
|
||
|
|
manager, err := NewManager(cfg)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test each step
|
||
|
|
t.Run("LoadTemplates", func(t *testing.T) {
|
||
|
|
if len(manager.templates) == 0 {
|
||
|
|
t.Error("expected templates to be loaded")
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("Render", func(t *testing.T) {
|
||
|
|
var buf bytes.Buffer
|
||
|
|
data := map[string]string{"name": "Test"}
|
||
|
|
|
||
|
|
err := manager.Render(&buf, "test.html", data)
|
||
|
|
if err != nil {
|
||
|
|
t.Errorf("render failed: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
if buf.Len() == 0 {
|
||
|
|
t.Error("expected rendered output")
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Related Patterns
|
||
|
|
|
||
|
|
- **Strategy Pattern**: Both allow algorithm customization
|
||
|
|
- **Factory Pattern**: Often used with template for object creation
|
||
|
|
- **Handler Pattern**: Uses template method for request processing
|
||
|
|
|
||
|
|
## Further Reading
|
||
|
|
|
||
|
|
- [Template Method Pattern](https://refactoring.guru/design-patterns/template-method)
|
||
|
|
- [Go Templates](https://pkg.go.dev/text/template)
|
||
|
|
- [html/template Package](https://pkg.go.dev/html/template)
|