refactor: remove outdated server design documentation
Remove 557-line server-design.md from _go-learning/architecture - content is now covered in updated architecture documentation with real implementation examples and test coverage.
This commit is contained in:
@@ -0,0 +1,636 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user