143
behavioral DDD

Policy

Reference Wikipedia ↗
Policy — class diagram
Plate 143 class diagram

The Policy pattern encapsulates a set of business rules or logic into separate classes, allowing for greater flexibility and maintainability. It defines a family of algorithms (policies) and makes them interchangeable, enabling the selection of the appropriate algorithm at runtime based on context. Rather than hardcoding the logic within a single class or method, the Policy pattern promotes loose coupling and easier modification of behavior without altering the core client code.

Usage

The Policy pattern is commonly used in scenarios where business rules are complex and subject to change, or when different users or contexts require different behavior. Specific usage examples include:

  • Access Control: Determining whether a user has permission to perform a certain action based on their role and other factors.
  • Pricing Rules: Applying different pricing calculations depending on customer type, location, or purchase volume.
  • Validation Logic: Implementing varied validation rules based on input data source or user preferences.
  • Workflow Management: Executing different steps in a workflow based on the current state of the process.
  • Gaming AI: Modifying AI behavior (e.g., aggression level) based on game difficulty or player actions.

Examples

  1. Spring Security (Java): Spring Security utilizes policies to define access control rules. AccessDecisionManager interfaces and VoteBased or AffirmativeBased access control strategies allow developers to define multiple AccessDecisionVoter implementations, each representing a specific policy (e.g., role-based, IP address-based). These voters are then dynamically combined to determine access.

  2. GraphQL Authorization (JavaScript/Node.js): Many GraphQL server libraries offer mechanisms for implementing authorization policies. For instance, Apollo Server allows you to define resolver functions with rules that dictate which users can access specific fields or data. These rules can be implemented as separate policy classes or functions, promoting modularity and reusability. A policy might check user roles, ownership of data, or other contextual information before granting access.

  3. Kubernetes Admission Controllers: Kubernetes uses admission controllers which can be implemented as policies to enforce specific constraints on resources before they are persisted. These policies can cover security, resource limits, and compliance requirements, ensuring that the cluster operates according to defined rules.

Specimens

15 implementations
Specimen 143.01 Dart View specimen ↗

The Policy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It allows you to change the algorithm used at runtime without modifying the client code. This is achieved by defining a common interface for all algorithms (the “policy”) and then having concrete policy classes implement that interface. The context object holds a reference to the policy and delegates the execution of the algorithm to it.

In this Dart example, we have a DiscountPolicy interface and concrete policies like FixedAmountDiscount, and PercentageDiscount. The ShoppingCart class (the context) accepts a DiscountPolicy during construction and uses it to calculate the final price. This demonstrates Dart’s use of interfaces and dependency injection for flexible algorithm selection.

// Define the policy interface
abstract class DiscountPolicy {
  double applyDiscount(double originalPrice);
}

// Concrete policy: Fixed amount discount
class FixedAmountDiscount implements DiscountPolicy {
  final double amount;

  FixedAmountDiscount(this.amount);

  @override
  double applyDiscount(double originalPrice) {
    return originalPrice - amount;
  }
}

// Concrete policy: Percentage discount
class PercentageDiscount implements DiscountPolicy {
  final double percentage;

  PercentageDiscount(this.percentage);

  @override
  double applyDiscount(double originalPrice) {
    return originalPrice * (1 - percentage / 100);
  }
}

// Context: Shopping Cart
class ShoppingCart {
  final DiscountPolicy discountPolicy;
  final List<double> items;

  ShoppingCart(this.discountPolicy, this.items);

  double calculateTotalPrice() {
    double total = items.fold(0.0, (sum, item) => sum + item);
    return discountPolicy.applyDiscount(total);
  }
}

void main() {
  // Example usage with different policies
  final cart1 = ShoppingCart(FixedAmountDiscount(10.0), [100.0, 50.0, 25.0]);
  print('Cart 1 Total: ${cart1.calculateTotalPrice()}'); // Output: Cart 1 Total: 165.0

  final cart2 = ShoppingCart(PercentageDiscount(20.0), [100.0, 50.0, 25.0]);
  print('Cart 2 Total: ${cart2.calculateTotalPrice()}'); // Output: Cart 2 Total: 130.0
}