|
| 1 | +# Strategy Pattern |
| 2 | + |
| 3 | +## Definition |
| 4 | + |
| 5 | +*The strategy pattern (also known as the policy pattern) is a behavioral software design pattern that enables selecting an algorithm at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.* |
| 6 | + |
| 7 | +Source: [Wikipedia](https://en.wikipedia.org/wiki/Strategy_pattern) |
| 8 | + |
| 9 | +## Example |
| 10 | + |
| 11 | +### Model |
| 12 | + |
| 13 | + |
| 14 | + |
| 15 | +### Code |
| 16 | + |
| 17 | +```csharp |
| 18 | + |
| 19 | +internal class BuySubscriptionCommandHandler : ICommandHandler<BuySubscriptionCommand, Guid> |
| 20 | + { |
| 21 | + private readonly IAggregateStore _aggregateStore; |
| 22 | + |
| 23 | + private readonly IPayerContext _payerContext; |
| 24 | + |
| 25 | + private readonly ISqlConnectionFactory _sqlConnectionFactory; |
| 26 | + |
| 27 | + internal BuySubscriptionCommandHandler( |
| 28 | + IAggregateStore aggregateStore, |
| 29 | + IPayerContext payerContext, |
| 30 | + ISqlConnectionFactory sqlConnectionFactory) |
| 31 | + { |
| 32 | + _aggregateStore = aggregateStore; |
| 33 | + _payerContext = payerContext; |
| 34 | + _sqlConnectionFactory = sqlConnectionFactory; |
| 35 | + } |
| 36 | + |
| 37 | + public async Task<Guid> Handle(BuySubscriptionCommand command, CancellationToken cancellationToken) |
| 38 | + { |
| 39 | + var priceList = await PriceListFactory.CreatePriceList(_sqlConnectionFactory.GetOpenConnection()); |
| 40 | + |
| 41 | + var subscription = SubscriptionPayment.Buy( |
| 42 | + _payerContext.PayerId, |
| 43 | + SubscriptionPeriod.Of(command.SubscriptionTypeCode), |
| 44 | + command.CountryCode, |
| 45 | + MoneyValue.Of(command.Value, command.Currency), |
| 46 | + priceList); |
| 47 | + |
| 48 | + _aggregateStore.AppendChanges(subscription); |
| 49 | + |
| 50 | + return subscription.Id; |
| 51 | + } |
| 52 | + } |
| 53 | + |
| 54 | +public static class PriceListFactory |
| 55 | + { |
| 56 | + public static async Task<PriceList> CreatePriceList(IDbConnection connection) |
| 57 | + { |
| 58 | + var priceListItemList = await GetPriceListItems(connection); |
| 59 | + |
| 60 | + var priceListItems = priceListItemList |
| 61 | + .Select(x => |
| 62 | + new PriceListItemData( |
| 63 | + x.CountryCode, |
| 64 | + SubscriptionPeriod.Of(x.SubscriptionPeriodCode), |
| 65 | + MoneyValue.Of(x.MoneyValue, x.MoneyCurrency), |
| 66 | + PriceListItemCategory.Of(x.CategoryCode))) |
| 67 | + .ToList(); |
| 68 | + |
| 69 | + // This is place for selecting pricing strategy based on provided data and the system state. |
| 70 | + IPricingStrategy pricingStrategy = new DirectValueFromPriceListPricingStrategy(priceListItems); |
| 71 | + |
| 72 | + return PriceList.Create( |
| 73 | + priceListItems, |
| 74 | + pricingStrategy); |
| 75 | + } |
| 76 | + |
| 77 | + public static async Task<List<PriceListItemDto>> GetPriceListItems(IDbConnection connection) |
| 78 | + { |
| 79 | + var priceListItems = await connection.QueryAsync<PriceListItemDto>("SELECT " + |
| 80 | +$"[PriceListItem].[CountryCode] AS [{nameof(PriceListItemDto.CountryCode)}], " + |
| 81 | +$"[PriceListItem].[SubscriptionPeriodCode] AS [{nameof(PriceListItemDto.SubscriptionPeriodCode)}], " + |
| 82 | +$"[PriceListItem].[MoneyValue] AS [{nameof(PriceListItemDto.MoneyValue)}], " + |
| 83 | +$"[PriceListItem].[MoneyCurrency] AS [{nameof(PriceListItemDto.MoneyCurrency)}], " + |
| 84 | +$"[PriceListItem].[CategoryCode] AS [{nameof(PriceListItemDto.CategoryCode)}] " + |
| 85 | +"FROM [payments].[PriceListItems] AS [PriceListItem] " + |
| 86 | +"WHERE [PriceListItem].[IsActive] = 1"); |
| 87 | + |
| 88 | + var priceListItemList = priceListItems.AsList(); |
| 89 | + return priceListItemList; |
| 90 | + } |
| 91 | + } |
| 92 | + |
| 93 | +public class PriceList : ValueObject |
| 94 | + { |
| 95 | + private readonly List<PriceListItemData> _items; |
| 96 | + |
| 97 | + private readonly IPricingStrategy _pricingStrategy; |
| 98 | + |
| 99 | + private PriceList( |
| 100 | + List<PriceListItemData> items, |
| 101 | + IPricingStrategy pricingStrategy) |
| 102 | + { |
| 103 | + _items = items; |
| 104 | + _pricingStrategy = pricingStrategy; |
| 105 | + } |
| 106 | + |
| 107 | + public static PriceList Create( |
| 108 | + List<PriceListItemData> items, |
| 109 | + IPricingStrategy pricingStrategy) |
| 110 | + { |
| 111 | + return new PriceList(items, pricingStrategy); |
| 112 | + } |
| 113 | + |
| 114 | + public MoneyValue GetPrice( |
| 115 | + string countryCode, |
| 116 | + SubscriptionPeriod subscriptionPeriod, |
| 117 | + PriceListItemCategory category) |
| 118 | + { |
| 119 | + CheckRule(new PriceForSubscriptionMustBeDefinedRule(countryCode, subscriptionPeriod, _items, category)); |
| 120 | + |
| 121 | + return _pricingStrategy.GetPrice(countryCode, subscriptionPeriod, category); |
| 122 | + } |
| 123 | + } |
| 124 | + |
| 125 | +public interface IPricingStrategy |
| 126 | + { |
| 127 | + MoneyValue GetPrice( |
| 128 | + string countryCode, |
| 129 | + SubscriptionPeriod subscriptionPeriod, |
| 130 | + PriceListItemCategory category); |
| 131 | + } |
| 132 | + |
| 133 | +public class DiscountedValueFromPriceListPricingStrategy : IPricingStrategy |
| 134 | + { |
| 135 | + private readonly List<PriceListItemData> _items; |
| 136 | + |
| 137 | + private readonly MoneyValue _discountValue; |
| 138 | + |
| 139 | + public DiscountedValueFromPriceListPricingStrategy( |
| 140 | + List<PriceListItemData> items, |
| 141 | + MoneyValue discountValue) |
| 142 | + { |
| 143 | + _items = items; |
| 144 | + _discountValue = discountValue; |
| 145 | + } |
| 146 | + |
| 147 | + public MoneyValue GetPrice(string countryCode, SubscriptionPeriod subscriptionPeriod, PriceListItemCategory category) |
| 148 | + { |
| 149 | + var priceListItem = _items.Single(x => |
| 150 | + x.CountryCode == countryCode && x.SubscriptionPeriod == subscriptionPeriod && |
| 151 | + x.Category == category); |
| 152 | + |
| 153 | + return priceListItem.Value - _discountValue; |
| 154 | + } |
| 155 | + } |
| 156 | + |
| 157 | + public class DirectValuePricingStrategy : IPricingStrategy |
| 158 | + { |
| 159 | + private readonly MoneyValue _directValue; |
| 160 | + |
| 161 | + public DirectValuePricingStrategy(MoneyValue directValue) |
| 162 | + { |
| 163 | + _directValue = directValue; |
| 164 | + } |
| 165 | + |
| 166 | + public MoneyValue GetPrice(string countryCode, SubscriptionPeriod subscriptionPeriod, PriceListItemCategory category) |
| 167 | + { |
| 168 | + return _directValue; |
| 169 | + } |
| 170 | + } |
| 171 | + |
| 172 | +public class DirectValueFromPriceListPricingStrategy : IPricingStrategy |
| 173 | + { |
| 174 | + private readonly List<PriceListItemData> _items; |
| 175 | + |
| 176 | + public DirectValueFromPriceListPricingStrategy(List<PriceListItemData> items) |
| 177 | + { |
| 178 | + _items = items; |
| 179 | + } |
| 180 | + |
| 181 | + public MoneyValue GetPrice( |
| 182 | + string countryCode, |
| 183 | + SubscriptionPeriod subscriptionPeriod, |
| 184 | + PriceListItemCategory category) |
| 185 | + { |
| 186 | + var priceListItem = _items.Single(x => |
| 187 | + x.CountryCode == countryCode && x.SubscriptionPeriod == subscriptionPeriod && |
| 188 | + x.Category == category); |
| 189 | + |
| 190 | + return priceListItem.Value; |
| 191 | + } |
| 192 | + } |
| 193 | +``` |
| 194 | +### Description |
| 195 | + |
| 196 | +Let's introduce the concepts of the strategy pattern, so we can understand how the above example fits this pattern. |
| 197 | + |
| 198 | +* **Client** - The calling code. |
| 199 | +* **Context** - An object which maintains a reference to one of the *concrete strategies* and communicates with the *client*. |
| 200 | +* **Strategy interface** - An interface or abstract class that the *client* can use to set a concrete strategy at run-time, through the *context*. |
| 201 | +* **Concrete strategies** - One or more implementations of the *strategy interface*. |
| 202 | + |
| 203 | +--- |
| 204 | + |
| 205 | +If we have a close look at our example of buying a `Subscription`, we can notice the elements of the strategy pattern. |
| 206 | + |
| 207 | +* `BuySubscriptionCommandHandler` is the calling code! Also the handler, indirectly via the `PriceListFactory` sets the current *strategy* of `PriceList`, so their combined interaction represents the **Client**. |
| 208 | +* `PriceList` is the object which maintains a reference to a pricing strategy, so it represents the **Context**. |
| 209 | +* `IPricingStrategy` represents the **Strategy interface**. |
| 210 | +* `DiscountedValueFromPriceListPricingStrategy`, `DirectValueFromPriceListPricingStrategy` and `DirectValuePricingStrategy` are the implementations of `IPricingStrategy` so they represent the **Concrete strategies**. |
| 211 | + |
| 212 | +--- |
| 213 | + |
| 214 | + The interaction of the `BuySubscriptionCommandHandler` and `PriceListFactory` is a good example of leveraging multiple design patterns. Check out [Factory Pattern](../Factory-Pattern/) to learn more. |
| 215 | + |
| 216 | +Strategy should not be confused with [Decorator](../Decorator-Pattern/)!!! |
| 217 | +*A strategy lets you change the guts of an object, while decorator lets you change the skin.* |
0 commit comments