191
behavioral DDD

Specification

Reference Wikipedia ↗
Specification — class diagram
Plate 191 class diagram

The Specification pattern encapsulates business rules in objects, allowing for dynamic combinations and reuse. It’s a way to separate complex logic concerning data from the objects that hold that data. This enables you to define a variety of rules, combine them, and then apply these rules to objects without directly embedding the logic within the object itself.

This pattern is particularly useful in applications with complex validation or filtering requirements. It allows for greater flexibility and maintainability, as rules can be added, modified, or combined without altering the core classes. It’s a core pattern in Domain-Driven Design for enriching model objects with behavior and logic.

Usage

The Specification pattern is commonly used in:

  • Data Validation: Defining rules for acceptable data formats, ranges, or dependencies. For example, ensuring an email address is valid or a password meets complexity requirements.
  • Business Rules Engines: Implementing complex decision-making logic based on various criteria.
  • Querying and Filtering: Constructing dynamic queries to retrieve data based on specific conditions. This is common in ORM frameworks and data access layers.
  • Access Control: Determining whether a user has permission to perform a certain action based on their role and the resource they are trying to access.

Examples

  1. Hibernate (Java ORM): Hibernate utilizes Specifications as part of its Criteria API. Users define criteria by creating Predicate objects representing constraints on entity properties. These predicates can be combined using logical operators like AND and OR to form complex queries. The Criteria object essentially is a Specification, defining the conditions for data retrieval.

  2. Doctrine (PHP ORM): Similar to Hibernate, Doctrine allows building queries using a Where clause which essentially models a Specification. You can define conditions on entity attributes and combine them using logical operators, enabling flexible filtering of results. The QueryBuilder’s where() method accepts specifications, dynamically building up the query.

Specimens

15 implementations
Specimen 191.01 Dart View specimen ↗

The Specification pattern is a functional approach to defining complex data validation or filtering rules. It encapsulates a condition as an object, allowing for reusable and composable logic. Instead of embedding validation directly within a class, Specifications define what criteria data must meet, separate from how the data is handled.

This Dart implementation uses a generic Specification class with a satisfies method that takes an object and returns a boolean. Concrete specifications are created by extending this class and overriding satisfies. The and, or, and not methods allow for combining specifications, promoting code reuse and readability. This approach aligns with Dart’s support for functional programming and encourages a declarative style.

// specification.dart

abstract class Specification<T> {
  bool satisfies(T item);

  Specification<T> and(Specification<T> other) {
    return _AndSpecification(this, other);
  }

  Specification<T> or(Specification<T> other) {
    return _OrSpecification(this, other);
  }

  Specification<T> not() {
    return _NotSpecification(this);
  }
}

class _AndSpecification<T> implements Specification<T> {
  final Specification<T> first;
  final Specification<T> second;

  _AndSpecification(this.first, this.second);

  @override
  bool satisfies(T item) => first.satisfies(item) && second.satisfies(item);
}

class _OrSpecification<T> implements Specification<T> {
  final Specification<T> first;
  final Specification<T> second;

  _OrSpecification(this.first, this.second);

  @override
  bool satisfies(T item) => first.satisfies(item) || second.satisfies(item);
}

class _NotSpecification<T> implements Specification<T> {
  final Specification<T> spec;

  _NotSpecification(this.spec);

  @override
  bool satisfies(T item) => !spec.satisfies(item);
}

class IsPositive extends Specification<int> {
  @override
  bool satisfies(int number) => number > 0;
}

class IsEven extends Specification<int> {
  @override
  bool satisfies(int number) => number % 2 == 0;
}

void main() {
  final isPositiveAndEven = IsPositive().and(IsEven());

  print(isPositiveAndEven.satisfies(4));   // true
  print(isPositiveAndEven.satisfies(5));   // false
  print(isPositiveAndEven.satisfies(-2));  // false

  final isPositiveOrNegative = IsPositive().or(IsEven().not());

  print(isPositiveOrNegative.satisfies(3)); // true
  print(isPositiveOrNegative.satisfies(2)); // false
  print(isPositiveOrNegative.satisfies(-1)); // true
}