d9e5668cec
Fixes and improvements from exhaustive audit: Consistency fixes in SKILL.md: - 'Pipe & Filter' → 'Pipe and Filter' (one stray '&' found and corrected) - 'Singleton for factory instance' → clarified to 'register factory as singleton-scoped via DI container' (less misleading wording) - Documentation Format section updated with note that SKILL.md itself is the authoritative source for related-pattern combinations Coverage fix — Related Patterns sections: - Added '## Related Patterns' to ALL 94 pattern files (was 0/94) - Each section lists 3–6 related patterns with relationship descriptions - Covers: why they're related, when to prefer one vs the other, and which are often confused SOLID principles → Creational → Structural → Behavioral → Architectural → Concurrency → Functional → Resilience → Data Access → Messaging → Testing → Error Handling → Microservice — all 13 categories covered Code verification: - Python: 0 failures (all 85 testable blocks pass) - Go: 0 failures (all 76 testable blocks pass) - JavaScript: 0 failures (all 78 testable blocks pass) - All 239 code blocks verified correct after edits Final skill state: - 108 files, 36,524 lines across 13 reference categories - 94/94 pattern files have Related Patterns sections - 2,815-line SKILL.md with 67 decision trees, 23 scenarios, 0 broken references, 0 naming inconsistencies
9.8 KiB
9.8 KiB
Specification Pattern
Problem
Business rules for filtering, validation, and selection are scattered across the codebase — duplicated in repository queries, service methods, and UI validation. Rules cannot be combined, reused, or tested independently. Adding a new rule requires changes in multiple places.
Solution
The Specification pattern encapsulates a single business rule into a reusable object with an is_satisfied_by(candidate) method. Specifications can be combined using logical operators (AND, OR, NOT) to build complex composite rules from simple building blocks.
When to Use
- Business rules need to be reused across querying, validation, and construction.
- Rules need to be combined dynamically at runtime.
- You want to test business rules in isolation.
- The domain has complex filtering logic that changes frequently.
When to Avoid
- Simple conditions that are used in only one place.
- Performance-critical code where the abstraction overhead matters.
- When the rules are static and few in number.
Pseudocode
INTERFACE Specification<T>:
METHOD is_satisfied_by(candidate: T) -> bool
METHOD and(other: Specification) -> Specification:
RETURN AndSpec(self, other)
METHOD or(other: Specification) -> Specification:
RETURN OrSpec(self, other)
METHOD not() -> Specification:
RETURN NotSpec(self)
CLASS AndSpec IMPLEMENTS Specification:
METHOD is_satisfied_by(candidate):
RETURN left.is_satisfied_by(candidate)
AND right.is_satisfied_by(candidate)
Python
from abc import ABC, abstractmethod
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
category: str
in_stock: bool
class Specification(ABC):
@abstractmethod
def is_satisfied_by(self, item: Product) -> bool: ...
def and_(self, other: "Specification") -> "Specification":
return AndSpec(self, other)
def or_(self, other: "Specification") -> "Specification":
return OrSpec(self, other)
def not_(self) -> "Specification":
return NotSpec(self)
class AndSpec(Specification):
def __init__(self, left: Specification, right: Specification) -> None:
self._left = left
self._right = right
def is_satisfied_by(self, item: Product) -> bool:
return self._left.is_satisfied_by(item) and self._right.is_satisfied_by(item)
class OrSpec(Specification):
def __init__(self, left: Specification, right: Specification) -> None:
self._left = left
self._right = right
def is_satisfied_by(self, item: Product) -> bool:
return self._left.is_satisfied_by(item) or self._right.is_satisfied_by(item)
class NotSpec(Specification):
def __init__(self, spec: Specification) -> None:
self._spec = spec
def is_satisfied_by(self, item: Product) -> bool:
return not self._spec.is_satisfied_by(item)
# --- Concrete specifications ---
class InStockSpec(Specification):
def is_satisfied_by(self, item: Product) -> bool:
return item.in_stock
class PriceBelowSpec(Specification):
def __init__(self, max_price: float) -> None:
self._max_price = max_price
def is_satisfied_by(self, item: Product) -> bool:
return item.price < self._max_price
class CategorySpec(Specification):
def __init__(self, category: str) -> None:
self._category = category
def is_satisfied_by(self, item: Product) -> bool:
return item.category == self._category
def filter_products(products: list[Product], spec: Specification) -> list[Product]:
return [p for p in products if spec.is_satisfied_by(p)]
def main() -> None:
products = [
Product("Laptop", 999.99, "electronics", True),
Product("Mouse", 29.99, "electronics", True),
Product("Desk", 249.99, "furniture", True),
Product("Monitor", 399.99, "electronics", False),
Product("Chair", 189.99, "furniture", False),
Product("Keyboard", 79.99, "electronics", True),
Product("Lamp", 39.99, "furniture", True),
]
in_stock = InStockSpec()
affordable = PriceBelowSpec(100.0)
electronics = CategorySpec("electronics")
print("--- In stock ---")
for p in filter_products(products, in_stock):
print(f" {p.name}: ${p.price:.2f} [{p.category}]")
print("\n--- Affordable (< $100) ---")
for p in filter_products(products, affordable):
print(f" {p.name}: ${p.price:.2f}")
print("\n--- Affordable electronics in stock ---")
spec = in_stock.and_(affordable).and_(electronics)
for p in filter_products(products, spec):
print(f" {p.name}: ${p.price:.2f}")
print("\n--- Electronics OR affordable ---")
spec2 = electronics.or_(affordable)
for p in filter_products(products, spec2):
print(f" {p.name}: ${p.price:.2f} [{p.category}]")
print("\n--- NOT in stock ---")
for p in filter_products(products, in_stock.not_()):
print(f" {p.name}: ${p.price:.2f}")
if __name__ == "__main__":
main()
Output:
--- In stock ---
Laptop: $999.99 [electronics]
Mouse: $29.99 [electronics]
Desk: $249.99 [furniture]
Keyboard: $79.99 [electronics]
Lamp: $39.99 [furniture]
--- Affordable (< $100) ---
Mouse: $29.99
Keyboard: $79.99
Lamp: $39.99
--- Affordable electronics in stock ---
Mouse: $29.99
Keyboard: $79.99
--- Electronics OR affordable ---
Laptop: $999.99 [electronics]
Mouse: $29.99 [electronics]
Monitor: $399.99 [electronics]
Keyboard: $79.99 [electronics]
Lamp: $39.99 [furniture]
--- NOT in stock ---
Monitor: $399.99
Chair: $189.99
JavaScript
class Product {
constructor(name, price, category, inStock) {
this.name = name;
this.price = price;
this.category = category;
this.inStock = inStock;
}
}
// --- Specification base ---
class Specification {
isSatisfiedBy(_item) {
throw new Error("Not implemented");
}
and(other) {
return new AndSpec(this, other);
}
or(other) {
return new OrSpec(this, other);
}
not() {
return new NotSpec(this);
}
}
class AndSpec extends Specification {
constructor(left, right) {
super();
this._left = left;
this._right = right;
}
isSatisfiedBy(item) {
return this._left.isSatisfiedBy(item) && this._right.isSatisfiedBy(item);
}
}
class OrSpec extends Specification {
constructor(left, right) {
super();
this._left = left;
this._right = right;
}
isSatisfiedBy(item) {
return this._left.isSatisfiedBy(item) || this._right.isSatisfiedBy(item);
}
}
class NotSpec extends Specification {
constructor(spec) {
super();
this._spec = spec;
}
isSatisfiedBy(item) {
return !this._spec.isSatisfiedBy(item);
}
}
// --- Concrete specs ---
class InStockSpec extends Specification {
isSatisfiedBy(item) {
return item.inStock;
}
}
class PriceBelowSpec extends Specification {
constructor(maxPrice) {
super();
this._maxPrice = maxPrice;
}
isSatisfiedBy(item) {
return item.price < this._maxPrice;
}
}
class CategorySpec extends Specification {
constructor(category) {
super();
this._category = category;
}
isSatisfiedBy(item) {
return item.category === this._category;
}
}
function filterProducts(products, spec) {
return products.filter((p) => spec.isSatisfiedBy(p));
}
// --- Main ---
const products = [
new Product("Laptop", 999.99, "electronics", true),
new Product("Mouse", 29.99, "electronics", true),
new Product("Desk", 249.99, "furniture", true),
new Product("Monitor", 399.99, "electronics", false),
new Product("Chair", 189.99, "furniture", false),
new Product("Keyboard", 79.99, "electronics", true),
new Product("Lamp", 39.99, "furniture", true),
];
const inStock = new InStockSpec();
const affordable = new PriceBelowSpec(100);
const electronics = new CategorySpec("electronics");
console.log("--- In stock ---");
for (const p of filterProducts(products, inStock)) {
console.log(` ${p.name}: $${p.price.toFixed(2)} [${p.category}]`);
}
console.log("\n--- Affordable (< $100) ---");
for (const p of filterProducts(products, affordable)) {
console.log(` ${p.name}: $${p.price.toFixed(2)}`);
}
console.log("\n--- Affordable electronics in stock ---");
const spec = inStock.and(affordable).and(electronics);
for (const p of filterProducts(products, spec)) {
console.log(` ${p.name}: $${p.price.toFixed(2)}`);
}
console.log("\n--- Electronics OR affordable ---");
const spec2 = electronics.or(affordable);
for (const p of filterProducts(products, spec2)) {
console.log(` ${p.name}: $${p.price.toFixed(2)} [${p.category}]`);
}
console.log("\n--- NOT in stock ---");
for (const p of filterProducts(products, inStock.not())) {
console.log(` ${p.name}: $${p.price.toFixed(2)}`);
}
Output:
--- In stock ---
Laptop: $999.99 [electronics]
Mouse: $29.99 [electronics]
Desk: $249.99 [furniture]
Keyboard: $79.99 [electronics]
Lamp: $39.99 [furniture]
--- Affordable (< $100) ---
Mouse: $29.99
Keyboard: $79.99
Lamp: $39.99
--- Affordable electronics in stock ---
Mouse: $29.99
Keyboard: $79.99
--- Electronics OR affordable ---
Laptop: $999.99 [electronics]
Mouse: $29.99 [electronics]
Monitor: $399.99 [electronics]
Keyboard: $79.99 [electronics]
Lamp: $39.99 [furniture]
--- NOT in stock ---
Monitor: $399.99
Chair: $189.99
Related Patterns
- Composite — Specifications compose via AND, OR, NOT — this IS the Composite pattern applied to boolean predicates.
- Repository — Repository.find(spec) takes a Specification, decoupling query criteria from query execution.
- Strategy — Each Specification is a Strategy for one evaluation criterion.
- Guard Clause — Guard Clause uses Specifications to validate inputs at function boundaries.
- Chain of Responsibility — Rules evaluated in sequence (ordered Specifications) can be implemented as Chain of Responsibility.