Skip to content

Specifications

The Specification pattern is a design pattern that allows you to encapsulate business logic for selecting objects that match certain criteria into separate, composable components. Each specification represents a single rule or a combination of rules. This pattern is particularly useful for building complex queries or validation logic in a clean, maintainable, and reusable way.

What is a Specification?

A specification object has one core method, typically is_satisfied_by(candidate), which returns True if a given candidate object meets the criteria defined by the specification, and False otherwise.

Castlecraft Engineer provides an abstract base class Specification[T] that your concrete specifications can inherit from. T is a generic type parameter representing the type of the candidate object the specification will evaluate.

Key Benefits

  • Encapsulation of Logic: Business rules for selection or validation are encapsulated within specification classes, making the codebase cleaner and easier to understand.
  • Reusability: Specifications can be reused across different parts of your application.
  • Composability: Simple specifications can be combined using logical operators (AND, OR, NOT) to create more complex specifications. This avoids deeply nested conditional logic in your services or repositories.
  • Testability: Each specification can be tested in isolation.
  • Readability: Using specifications can make complex conditional logic more readable, e.g., user_spec = IsUserActive() & HasAdminRole() | IsSystemUser().

The Specification Abstraction

The castlecraft_engineer.abstractions.specification.Specification class provides:

  • An abstract method is_satisfied_by(self, candidate: T) -> bool that must be implemented by concrete specifications.
  • Operator overloading for composition:
    • & (AND): Combines two specifications. spec1 & spec2 creates an AndSpecification.
    • | (OR): Combines two specifications. spec1 | spec2 creates an OrSpecification.
    • ~ (NOT): Negates a specification. ~spec1 creates a NotSpecification.

Concrete Composite Specifications

The library provides these built-in composite specifications that are automatically used when you combine specifications with operators:

  • AndSpecification(spec1, spec2): Satisfied if both spec1 and spec2 are satisfied.
  • OrSpecification(spec1, spec2): Satisfied if either spec1 or spec2 (or both) are satisfied.
  • NotSpecification(spec): Satisfied if spec is not satisfied.

Example Usage

Let's say we have a Product class and we want to find products that are in stock and cost less than $100.

from typing import Generic, TypeVar
from castlecraft_engineer.abstractions.specification import Specification

# Candidate object
class Product:
    def __init__(self, name: str, price: float, in_stock: bool):
        self.name = name
        self.price = price
        self.in_stock = in_stock

# Concrete Specifications
class ProductIsInStock(Specification[Product]):
    def is_satisfied_by(self, candidate: Product) -> bool:
        return candidate.in_stock

class ProductPriceLessThan(Specification[Product]):
    def __init__(self, max_price: float):
        self.max_price = max_price

    def is_satisfied_by(self, candidate: Product) -> bool:
        return candidate.price < self.max_price

# Using the specifications
product1 = Product(name="Laptop", price=1200.00, in_stock=True)
product2 = Product(name="Mouse", price=25.00, in_stock=True)
product3 = Product(name="Keyboard", price=75.00, in_stock=False)
product4 = Product(name="Webcam", price=50.00, in_stock=True)

# Create individual specifications
in_stock_spec = ProductIsInStock()
price_lt_100_spec = ProductPriceLessThan(100.00)

# Combine them using the AND operator
affordable_and_in_stock_spec = in_stock_spec & price_lt_100_spec
# This is equivalent to:
# from castlecraft_engineer.abstractions.specification import AndSpecification
# affordable_and_in_stock_spec = AndSpecification(in_stock_spec, price_lt_100_spec)

print(f"Product 2 satisfies: {affordable_and_in_stock_spec.is_satisfied_by(product2)}") # True
print(f"Product 4 satisfies: {affordable_and_in_stock_spec.is_satisfied_by(product4)}") # True
print(f"Product 1 satisfies: {affordable_and_in_stock_spec.is_satisfied_by(product1)}") # False (price too high)
print(f"Product 3 satisfies: {affordable_and_in_stock_spec.is_satisfied_by(product3)}") # False (not in stock)

# Using NOT
not_in_stock_spec = ~in_stock_spec
print(f"Product 3 satisfies not_in_stock_spec: {not_in_stock_spec.is_satisfied_by(product3)}") # True

Use Cases

  • Filtering collections in memory: Iterating through a list of objects and selecting those that satisfy a specification.
  • Querying databases: Specifications can be translated into database query criteria (e.g., SQL WHERE clauses or ORM filters). This is a more advanced use case and often requires a "Specification Visitor" or a similar mechanism to perform the translation. Castlecraft Engineer does not provide this translation out-of-the-box, but the Specification ABC is compatible with such extensions.
  • Validation: Checking if an object meets certain validation rules before processing.
  • Business rule definition: Clearly defining and isolating business rules.