How it works
A strategy object represents one interchangeable policy behind a shared interface. It becomes useful when the policy has its own configuration, validation, or several related operations that would be awkward to express as one anonymous function. The caller depends on the strategy contract and coordinates the workflow without knowing which concrete policy was selected.
This design is an ordinary application of composition. The checkout has a discount policy; it is not a subclass of every possible policy. Configuration belongs to the object that owns that rule. Keeping it there makes the same workflow testable with a no-discount strategy and a threshold-based strategy while avoiding type tests or configuration branches inside the checkout itself.
Walk through the example. Checkout receives an IDiscount and delegates its price adjustment to that object. NoDiscount returns the original amount. ThresholdDiscount applies a configured rate only when the amount reaches its threshold. The same subtotal of one hundred twenty therefore produces one hundred twenty under one policy and one hundred eight under the other. Formatting remains outside both strategies.
Watch the boundary. A shared interface needs a shared semantic contract. If one strategy returns a surcharge while another returns a discounted total, the caller cannot substitute them safely despite matching signatures. Specify whether the result is a final amount or an adjustment, how negative input is handled, and whether policies can be combined. Validate configuration such as a discount rate so impossible states do not enter the workflow.
Practice independently. Add a fixed-amount discount strategy with a rule that the final amount cannot fall below zero. Test the same subtotal under all three strategies. Then model a composed policy that applies two adjustments in a documented order and demonstrate that reversing the order can change the result. Decide whether composition belongs in the checkout or in a separate strategy that keeps the caller's contract unchanged.
A working example
using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
var plain = new Checkout(new NoDiscount());
var promotion = new Checkout(new ThresholdDiscount(100m, 0.10m));
Console.WriteLine(plain.Total(120m).ToString("F2", CultureInfo.InvariantCulture));
Console.WriteLine(promotion.Total(120m).ToString("F2", CultureInfo.InvariantCulture));
interface IDiscount { decimal Apply(decimal amount); }
sealed class NoDiscount : IDiscount { public decimal Apply(decimal amount) => amount; }
sealed class ThresholdDiscount : IDiscount
{
private readonly decimal threshold, rate;
public ThresholdDiscount(decimal threshold, decimal rate) { this.threshold = threshold; this.rate = rate; }
public decimal Apply(decimal amount) => amount >= threshold ? amount * (1m - rate) : amount;
}
sealed class Checkout
{
private readonly IDiscount discount;
public Checkout(IDiscount discount) { this.discount = discount; }
public decimal Total(decimal subtotal) => discount.Apply(subtotal);
}
Expected output
120.00 108.00
Give every strategy the same result semantics and keep policy configuration with the policy.