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) -> boolthat must be implemented by concrete specifications. - Operator overloading for composition:
&(AND): Combines two specifications.spec1 & spec2creates anAndSpecification.|(OR): Combines two specifications.spec1 | spec2creates anOrSpecification.~(NOT): Negates a specification.~spec1creates aNotSpecification.
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 bothspec1andspec2are satisfied.OrSpecification(spec1, spec2): Satisfied if eitherspec1orspec2(or both) are satisfied.NotSpecification(spec): Satisfied ifspecis 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.