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 anAndSpecification
.|
(OR): Combines two specifications.spec1 | spec2
creates anOrSpecification
.~
(NOT): Negates a specification.~spec1
creates 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 bothspec1
andspec2
are satisfied.OrSpecification(spec1, spec2)
: Satisfied if eitherspec1
orspec2
(or both) are satisfied.NotSpecification(spec)
: Satisfied ifspec
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.