4.7 KiB
Specification Pattern Implementation
The Specification pattern allows you to encapsulate business logic for filtering and querying data. Instead of writing SQL queries directly, you build specifications using Go code that can later be converted to SQL, used for in-memory filtering, or other purposes.
Core Components
1. Basic Data Structures (condition_spec.go)
Condition
Represents a single comparison operation:
type Condition struct {
Field string // Field name (e.g., "age", "name")
Operator string // Comparison operator (e.g., "=", ">", "IN")
Value interface{} // Value to compare against
}
LogicalGroup
Combines multiple conditions with logical operators:
type LogicalGroup struct {
Operator string // "AND", "OR", or "NOT"
Conditions []Condition // Simple conditions
Spec *Spec // Nested specification for complex logic
}
Spec
The main specification container (can hold either a single condition or a logical group):
type Spec struct {
Condition *Condition // For simple conditions
LogicalGroup *LogicalGroup // For complex logic
}
2. Builder Functions
These functions create specifications in a fluent, readable way:
// Simple conditions
userSpec := Eq("name", "John") // name = "John"
ageSpec := Gt("age", 18) // age > 18
roleSpec := In("role", []interface{}{"admin", "moderator"}) // role IN ("admin", "moderator")
// Logical combinations
complexSpec := And(
Eq("status", "active"),
Or(
Gt("age", 21),
Eq("role", "admin"),
),
)
3. Specification Interface (simple_specification.go)
The Specification[T] interface provides methods for:
- Evaluation:
IsSatisfiedBy(candidate T) bool - Composition:
And(),Or(),Not() - Introspection:
GetConditions(),GetSpec()
How It Works
1. Building Specifications
// Create a specification for active users over 18
spec := And(
Eq("status", "active"),
Gt("age", 18),
)
2. Evaluating Specifications
The SimpleSpecification uses reflection to evaluate conditions against Go objects:
user := &User{Name: "John", Age: 25, Status: "active"}
specification := NewSimpleSpecification[*User](spec)
isMatch := specification.IsSatisfiedBy(user) // true
3. Field Access
The implementation looks for field values in this order:
GetFieldName()method (e.g.,GetAge())FieldName()method (e.g.,Age())- Exported struct field
4. Type Comparisons
The system handles different data types intelligently:
- Numbers: Direct comparison with type conversion
- Strings: Lexicographic comparison
- Time: Special handling for
time.Timeand objects withTime()methods - Collections:
INandNOT INoperations - Nil values: Proper null handling
Examples
Simple Usage
// Find all active users
activeSpec := Eq("status", "active")
spec := NewSimpleSpecification[*User](activeSpec)
users := []User{{Status: "active"}, {Status: "inactive"}}
for _, user := range users {
if spec.IsSatisfiedBy(&user) {
fmt.Println("Active user:", user.Name)
}
}
Complex Logic
// Find admin users OR users with high scores
complexSpec := Or(
Eq("role", "admin"),
And(
Gt("score", 90),
Eq("status", "active"),
),
)
Time Comparisons
// Find expired items
expiredSpec := Lte("expirationDate", time.Now())
Key Benefits
- Type Safety: Compile-time checking with generics
- Readability: Business logic expressed in readable Go code
- Reusability: Specifications can be composed and reused
- Testability: Easy to test business rules in isolation
- Flexibility: Can be converted to SQL, used for filtering, etc.
Performance Considerations
- Reflection is used for field access (consider caching for high-frequency operations)
- Complex nested specifications may impact performance
- Time comparisons handle multiple formats but may be slower than direct comparisons
Common Patterns
Repository Integration
type UserRepository interface {
FindWhere(ctx context.Context, spec Specification[*User]) ([]*User, error)
}
Business Rule Encapsulation
func ActiveUserSpec() *Spec {
return And(
Eq("status", "active"),
Neq("deletedAt", nil),
)
}
Dynamic Query Building
func BuildUserSearchSpec(filters UserFilters) *Spec {
conditions := []*Spec{}
if filters.Status != "" {
conditions = append(conditions, Eq("status", filters.Status))
}
if filters.MinAge > 0 {
conditions = append(conditions, Gte("age", filters.MinAge))
}
return And(conditions...)
}