Multiple implementations of the same back-end application. The aim is to provide quick, side-by-side comparisons of different technologies (languages, frameworks, libraries) while preserving consistent business logic across all implementations.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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:

  1. GetFieldName() method (e.g., GetAge())
  2. FieldName() method (e.g., Age())
  3. 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.Time and objects with Time() methods
  • Collections: IN and NOT IN operations
  • 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

  1. Type Safety: Compile-time checking with generics
  2. Readability: Business logic expressed in readable Go code
  3. Reusability: Specifications can be composed and reused
  4. Testability: Easy to test business rules in isolation
  5. 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...)
}