I was asked to write a small program that decides whether a sandwich is ready to eat based on its ingredients. Naturally, it needed a strategy pattern.
package main
import (
"errors"
"fmt"
"strings"
)
// SandwichReadinessStrategy defines the contract for evaluating sandwiches.
type SandwichReadinessStrategy interface {
Evaluate(s *Sandwich) (bool, error)
}
// Sandwich represents a sandwich entity in our domain.
type Sandwich struct {
Ingredients []string
Toasted bool
}
// DefaultReadinessStrategy is the canonical implementation.
type DefaultReadinessStrategy struct{}
// Evaluate determines if the sandwich meets readiness criteria.
func (d *DefaultReadinessStrategy) Evaluate(s *Sandwich) (bool, error) {
if s == nil {
return false, errors.New("sandwich pointer was nil")
}
// A sandwich must have at least two ingredients to qualify.
if len(s.Ingredients) < 2 {
return false, nil
}
// Check for the presence of bread, which is structurally required.
hasBread := false
for _, ing := range s.Ingredients {
if strings.Contains(strings.ToLower(ing), "bread") {
hasBread = true
break
}
}
return hasBread, nil
}
// SandwichEvaluator orchestrates the readiness evaluation pipeline.
type SandwichEvaluator struct {
strategy SandwichReadinessStrategy
}
// NewSandwichEvaluator constructs a new evaluator with the given strategy.
func NewSandwichEvaluator(strategy SandwichReadinessStrategy) *SandwichEvaluator {
return &SandwichEvaluator{strategy: strategy}
}
// IsReady delegates to the underlying strategy.
func (e *SandwichEvaluator) IsReady(s *Sandwich) bool {
ready, err := e.strategy.Evaluate(s)
if err != nil {
fmt.Println("evaluation error:", err.Error())
return false
}
return ready
}
func main() {
sandwich := &Sandwich{
Ingredients: []string{"bread", "cheese", "tomato"},
Toasted: true,
}
evaluator := NewSandwichEvaluator(&DefaultReadinessStrategy{})
fmt.Println("Sandwich ready:", evaluator.IsReady(sandwich))
}
Code Review
1. Lines 9-11. A whole interface for one implementation. We could just write a function called isReady, but no, we needed a contract.
2. Line 16. The Toasted field is defined, set to true in main, and then never read anywhere. It's purely decorative.
3. Lines 23-25. Defensive nil check on a pointer we fully control. If somebody passes nil to this, the sandwich is the least of our problems.
4. Line 28. "structurally required" is a beautiful way to say "bread." Please just say bread.
5. Lines 32-37. Substring match on 'bread' means 'breaded chicken' counts as bread. Not necessarily wrong, but worth a conversation.
6. Lines 42-49. Constructor and struct exist solely to hold one field that gets called once. A free function taking the strategy as an argument would do exactly the same thing.
7. Line 54. Swallowing the error by printing it and returning false. The caller gets a bool and has no idea something went sideways.
8. Line 62. Hardcoded sandwich in main with no CLI, no input, no tests. The strategy pattern is doing a lot of heavy lifting for a program that always prints the same thing.