# 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

```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

```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.
