# Specification Pattern in Go - Detailed Explanation ## What is the Specification Pattern? The Specification Pattern is a design pattern that allows you to encapsulate business rules and domain logic in reusable, composable objects. Instead of scattering business rules throughout your codebase, you create specification objects that represent individual rules that can be combined using logical operations (AND, OR, NOT). ## Why Use It? 1. **Single Source of Truth**: Business rules are defined in one place 2. **Reusability**: Rules can be reused across different parts of the application 3. **Composability**: Simple rules can be combined to create complex rules 4. **Testability**: Each specification can be tested in isolation 5. **Flexibility**: Easy to modify or extend business rules ## How It Works in Our Go Implementation ### Core Components #### 1. Specification Interface ```go type Specification[T any] interface { IsSatisfiedBy(candidate T) bool And(other Specification[T]) Specification[T] Or(other Specification[T]) Specification[T] Not() Specification[T] GetConditions() []Condition GetSpec() *Spec } ``` This interface defines the contract that all specifications must implement. The generic type `T` allows specifications to work with any type of object. #### 2. SimpleSpecification - The Workhorse ```go type SimpleSpecification[T any] struct { spec *Spec } ``` This is the main implementation that evaluates conditions against objects using reflection. #### 3. Condition Structure ```go type Condition struct { Field string // Field name to check (e.g., "expirationDate") Operator string // Comparison operator (e.g., "<=", "==", ">") Value interface{} // Value to compare against } ``` #### 4. Spec Structure - The Building Block ```go type Spec struct { Condition *Condition // Single condition LogicalGroup *LogicalGroup // Group of conditions (AND/OR/NOT) } ``` ### How It Evaluates Conditions The magic happens in the `getFieldValue` method. When you specify a condition like `Lte("expirationDate", currentTime)`, the system needs to: 1. **Find the field**: Look for a getter method like `ExpirationDate()` or `GetExpirationDate()` 2. **Extract the value**: Call the method to get the actual value 3. **Compare values**: Use the appropriate comparison based on the operator #### Field Resolution Strategy ```go func (s *SimpleSpecification[T]) getFieldValue(candidate T, fieldName string) (interface{}, error) { // 1. Try "GetFieldName" method first getterName := "Get" + strings.Title(fieldName) method := v.MethodByName(getterName) // 2. Try field name as method (e.g., "ExpirationDate") method = v.MethodByName(fieldName) // 3. Try direct field access (for exported fields only) field := v.FieldByName(fieldName) if field.IsValid() && field.CanInterface() { return field.Interface(), nil } } ``` This approach handles different naming conventions and respects Go's visibility rules. ### Logical Operations #### AND Operation ```go func (s *SimpleSpecification[T]) And(other Specification[T]) Specification[T] { if otherSpec, ok := other.(*SimpleSpecification[T]); ok { // Both are SimpleSpecifications - combine their specs return NewSimpleSpecification[T](SpecBuilder.And(s.spec, otherSpec.spec)) } // Different types - create a composite specification return &CompositeSpecification[T]{ left: s, right: other, op: "AND", } } ``` #### CompositeSpecification - Handling Mixed Types When you try to combine different specification types, instead of panicking, we create a `CompositeSpecification` that: 1. **Stores both specifications**: Keeps references to both left and right specs 2. **Evaluates independently**: Calls `IsSatisfiedBy` on each specification 3. **Combines results**: Applies the logical operation (AND/OR) to the results ```go func (c *CompositeSpecification[T]) IsSatisfiedBy(candidate T) bool { switch c.op { case "AND": return c.left.IsSatisfiedBy(candidate) && c.right.IsSatisfiedBy(candidate) case "OR": return c.left.IsSatisfiedBy(candidate) || c.right.IsSatisfiedBy(candidate) } } ``` ### Building Specifications We provide convenient helper functions that mirror the PHP/NestJS implementations: ```go // Simple conditions spec := Eq("name", "Apple") // name == "Apple" spec := Gt("price", 10.0) // price > 10.0 spec := Lte("expirationDate", time) // expirationDate <= time // Logical combinations spec := And( Eq("status", "active"), Or( Gt("score", 80), In("role", []interface{}{"admin", "moderator"}) ), Not(Eq("deleted", true)) ) ``` ### Real-World Usage: ItemExpirationSpec Here's how we use it to check if items are expired: ```go func (s *ItemExpirationSpec) IsExpired(item *entities.ItemEntity, currentTime time.Time) bool { return s.GetSpec(currentTime).IsSatisfiedBy(item) } func (s *ItemExpirationSpec) GetSpec(currentTime time.Time) Specification[*entities.ItemEntity] { // Create condition: expirationDate <= currentTime spec := Lte("expirationDate", currentTime) return NewSimpleSpecification[*entities.ItemEntity](spec) } ``` ### Repository Integration The specification integrates seamlessly with repositories: ```go func (r *FileItemRepository) FindWhere(ctx context.Context, spec specifications.Specification[*entities.ItemEntity]) ([]*entities.ItemEntity, error) { var filteredItems []*entities.ItemEntity for _, item := range items { if spec.IsSatisfiedBy(item) { filteredItems = append(filteredItems, item) } } return filteredItems, nil } ``` ## Key Benefits of This Implementation ### 1. Type Safety Using generics (`Specification[T any]`) ensures type safety at compile time. ### 2. Flexibility - Supports different field access patterns (getters, direct fields) - Handles various data types (strings, numbers, dates, etc.) - Allows complex nested conditions ### 3. No Panic Zone This implementation gracefully handles mixed types using `CompositeSpecification`. ### 4. Reflection Safety The field resolution respects Go's visibility rules and provides clear error messages when fields can't be accessed. ### 5. Performance - Caches reflection information when possible - Short-circuits evaluation (stops at first false in AND, first true in OR) - Minimal overhead for simple conditions ## Common Patterns and Examples ### Checking Expired Items ```go expirationSpec := NewItemExpirationSpec() isExpired := expirationSpec.IsExpired(item, time.Now()) ``` ### Finding Active Users ```go activeUserSpec := And( Eq("status", "active"), Gte("lastLogin", thirtyDaysAgo), Not(Eq("role", "banned")) ) ``` ### Complex Business Rules ```go premiumCustomerSpec := Or( And( Eq("subscriptionType", "premium"), Gte("subscriptionEndDate", time.Now()), ), And( Eq("totalPurchases", 1000), Gte("customerSince", oneYearAgo), ), ) ``` ## Troubleshooting Common Issues ### 1. Field Not Found **Error**: `field expirationDate not found or not accessible` **Solution**: Ensure your entity has a getter method like `ExpirationDate()` or `GetExpirationDate()` ### 2. Type Mismatch **Error**: Comparison fails between different types **Solution**: The system tries to convert types automatically, but ensure your condition values match the expected field types ### 3. Performance Concerns **Issue**: Reflection is slower than direct field access **Solution**: For performance-critical paths, consider implementing custom specifications or caching results ## Best Practices 1. **Use getter methods**: Prefer `GetFieldName()` or `FieldName()` methods over direct field access 2. **Keep conditions simple**: Complex business logic should be in domain services, not specifications 3. **Test specifications**: Write unit tests for your specifications to ensure they work correctly 4. **Document business rules**: Add comments explaining what each specification represents 5. **Reuse specifications**: Create specification factories for commonly used rules ## Comparison with PHP/NestJS Our Go implementation maintains the same API surface as the PHP and NestJS versions: - **Same helper functions**: `Eq()`, `Gt()`, `Lte()`, `And()`, `Or()`, `Not()` - **Same condition structure**: `[field, operator, value]` - **Same logical operations**: AND, OR, NOT combinations - **Same usage patterns**: Build specs, then call `IsSatisfiedBy()` The main difference is that Go's static typing and lack of runtime metadata requires more sophisticated reflection handling, which we've encapsulated in the `getFieldValue` method. This implementation provides a robust, production-ready specification pattern that follows Go best practices while maintaining compatibility with the PHP and NestJS implementations.