Advanced Power of Strategy Pattern in .NET (part 2)

Nikita Ivanov
ByteHide
Published in
5 min readJun 22, 2023

--

This story demonstrates upgrades for the approach that was discussed in Part 1. It is necessary to read Part 1 before starting Part 2.

Levels 2 and 3 add even more flexibility, allowing you to solve a wider range of problems. I hope you’ll enjoy it!

Level 2 — Smart strategies with complex conditions

Suppose we have a more complex method of determining the right strategy to execute. For example, booking a party for company employees based on their experience and achievements:

  • 0–2 years of experience and less than 4 achievements: Standard ticket.
  • 3–6 years of experience and 4–8 achievements: Premium ticket.
  • 6+ years of experience and more than 8 achievements: VIP ticket.

1. Update the RegistrationRequest model:

We replace the TicketType field with new fields that we are basing our decision on.

public sealed record RegistrationRequest
{
public required string FullName { get; init; }
public required string Title { get; init; }
public required int ExperienceAge { get; init; }
public required int AchievementsCount { get; init; }
}

2. Update IRegistrationStrategy:

Update the IRegistrationStrategy interface by adding a new method that checks whether the strategy is applicable, and remove the TicketType field as it is no longer needed.

public interface IRegistrationStrategy
{
public void SubmitRegistration(RegistrationRequest request);
public bool IsApplicable(RegistrationRequest request);
}

3. Implement the `IsApplicable` conditions check in all strategies:

Each strategy should describe the conditions for its execution within its own implementation. This approach aligns with the single-responsibility principle and enhances extendability.

public class StandardRegistrationStrategy : IRegistrationStrategy
{
public void SubmitRegistration(RegistrationRequest request)
{
... some logic ...
}

public bool IsApplicable(RegistrationRequest request)
{
return request.ExperienceAge is >= 3
and <= 6 && request.AchievementsCount is >= 4 and <= 8;
}
}
public class PremiumRegistrationStrategy : IRegistrationStrategy
{
public void SubmitRegistration(RegistrationRequest request)
{
... some logic ...
}
public bool IsApplicable(RegistrationRequest request)
{
return request is { ExperienceAge: <= 2, AchievementsCount: < 4 };
}
}
public class VipRegistrationStrategy : IRegistrationStrategy
{
public void SubmitRegistration(RegistrationRequest request)
{
... some logic ...
}
public bool IsApplicable(RegistrationRequest request)
{
return request is { ExperienceAge: > 6, AchievementsCount: > 8 };
}
}

4. Update RegistrationService:

The responsibility of the registration service is to iterate over all registered strategies and call the IsApplicable method of each one. The first strategy that returns true will be executed.

You can easily modify the code to execute multiple strategies if needed. Additionally, you can throw an exception if more than one strategy satisfies the condition, which is not expected in your logic.

Once again, adapt this idea according to your specific needs. My goal is to introduce the main principles.

public class RegistrationService
{
private readonly IEnumerable<IRegistrationStrategy> _registrationStrategies;

public RegistrationService(IEnumerable<IRegistrationStrategy> registrationStrategies)
{
_registrationStrategies = registrationStrategies;
}
public void SubmitRegistration(RegistrationRequest request)
{
IRegistrationStrategy? strategyToExecute = _registrationStrategies
.FirstOrDefault(x => x.IsApplicable(request));
if (strategyToExecute is null)
{
throw new Exception("No strategy that qualifies the condition found!");
}
strategyToExecute.SubmitRegistration(request);
}
}

Summary

This implementation supports complex conditions for choosing the strategy to execute. Furthermore, by utilizing dependency injection (DI), it can accommodate conditions that are connected with database queries, calls to third-party services, and other service functionalities. It offers great flexibility and usefulness in various scenarios!

Of course, if there is a need to use I/O bound operations in your condition, it is better to make the IsApplicable method asynchronous. Additionally, if the check tends to run slowly, it is beneficial to use something like Task.WhenAll to run all the checks simultaneously without waiting for each other. Feel free to modify all the levels according to your specific requirements. My aim is to explain the fundamental idea behind each one.

Level 3 — Individual parameters

Things can become complicated when we want to add individual parameters for our strategies. Let me provide an example:

  1. The premium ticket enables the user to additionally request a strike ball.
  2. The VIP ticket enables the user to additionally book a transfer and a tennis court.

As we add more ticket types and additional options, the complexity increases. Fortunately, the solution is quite simple. We need something generic that allows us to support a common interface for strategies while also accommodating specific parameters.

The solution is a dictionary. Yes, it’s that simple. By storing parameters as key-value pairs, we achieve a common interface and have an easy way to pass and retrieve specific parameters.

There are indeed many good alternatives to dictionaries. For example, in some projects, I have used JSON and parameter structs instead. The choice depends on your specific needs and personal comfort. It’s important to select the approach that best aligns with your requirements and preferences.

1. Update RegistrationRequest model:

Add new dictionary field.

public sealed record RegistrationRequest
{
...
public Dictionary<string, string> Parameters { get; init; }
}

2. Update strategies:

The example demonstrates how to retrieve a value from a dictionary. Of course, it is possible to use a method that parses the value for a specific type or applies any other logic. This is just a simplified example.

public class PremiumRegistrationStrategy : IRegistrationStrategy
{
...
public void SubmitRegistration(RegistrationRequest request)
{
var isStrikeBallNeeded = request.Parameters.GetValueOrDefault(
PremiumRegistrationParams.IsStrikeBallNeeded);
... some logic ...
}
...
}
public static class PremiumRegistrationParams
{
public const string IsStrikeBallNeeded = "IsStrikeBallNeeded";
}
public class VipRegistrationStrategy : IRegistrationStrategy
{
...
public void SubmitRegistration(RegistrationRequest request)
{
var isTransferNeeded = request.Parameters.GetValueOrDefault(
VipRegistrationParams.IsTransferNeeded);
var isTennisNeeded = request.Parameters.GetValueOrDefault(
VipRegistrationParams.IsTennisCourtNeeded);
... some logic ...
}
...
}
public static class VipRegistrationParams
{
public const string IsTransferNeeded = "IsTransferNeeded";
public const string IsTennisCourtNeeded = "IsTennisCourtNeeded";
}

Summary

Now it is possible to pass custom parameters to each strategy, making our approach highly extendable and flexible. It supports both a common interface and individual behavior, allowing for a wide range of customization options.

Conclusion

My goal was to provide readers with an understanding of a fast and flexible development approach, and I hope it has been useful for you. It would be great to hear your thoughts on how this approach can be further improved, and you are absolutely free to contribute your ideas. Please share your thoughts and suggestions. Thank you for reading!

--

--

Nikita Ivanov
ByteHide

.NET-focused engineer, Web3 enthusiast, Crypto startup participant, content creator 👨‍💻