1

I am trying to understand the Strategy Pattern in order to utilise it for a problem I have.

The current code looks as such where I have an amount that I want to process based on a payment type.

public class PaymentProcessor { private PaymentType paymentType; public void processPayment(double amount) { if (paymentType == PaymentType.CREDIT_CARD) { Console.Writeline("Processing credit card payment of amount " + amount); } else if (paymentType == PaymentType.DEBIT_CARD) { Console.Writeline("Processing debit card payment of amount " + amount); } else if (paymentType == PaymentType.PAYPAL) { Console.Writeline("Processing PayPal payment of amount " + amount); } else { throw Exception(); } } public void setPaymentType(PaymentType paymentType) { this.paymentType = paymentType; } } enum PaymentType { CREDIT_CARD, DEBIT_CARD, PAYPAL } 

So based on the strategy pattern I would need to create an interface for all payments

public interface IPaymentStrategy { void processPayment(double amount); } 

then i need to create concrete implemtations for each class I will only give an example of one but you get the idea.

public class CreditCardPaymentStrategy : IPaymentStrategy { public void processPayment(double amount) { Console.Writeline("Processing credit card payment of amount " + amount); } } 

So all the payment strategies will have a concrete implemntation like above.

Finally using Dependency injection and Dependency Inversion I refactor the payment processor to look like this

public class PaymentProcessor { private PaymentStrategy paymentStrategy; public PaymentProcessor(PaymentStrategy paymentStrategy) { this.paymentStrategy = paymentStrategy; } public void processPayment(double amount) { paymentStrategy.processPayment(amount); } } 

But heres the bit im missing. Where and how do I implement the conditional logic to select the correct concrete implementation to register against the payment strategy based on a payment type?

I have tried looking online. All the examples I see seem to have another class like a factory that news up the concrete version and passes it into the interface. But Im not sure thats the right way to do it as i dont feel like I should be newing up classes that arent POCOS as my DI container should be doing that. So what am I missing in the strategy to conditionally select the concrete type based on the Payment type? Am i even using the right pattern here as i have seen people compare the strategy pattern to Depedency injection. If thats the cose what pattern is better for the conditional selection rather than registering a concrete class with the interface and having to manually change it in the registration every time I want to use a different payment strategy, but be able to switch between strategies at runtime?

6 Answers 6

0

refer to .NET documentations on MS docs in .NET 8 you can use Keyed service Provider and get right implementation of your services using that. you can see more about it here: keyed services

builder.Services.AddKeyedSingleton<IMemoryCache, BigCache>("big"); builder.Services.AddKeyedSingleton<IMemoryCache, SmallCache>("small"); class SmallCacheConsumer(IKeyedServiceProvider keyedServiceProvider) { public object? GetData() => keyedServiceProvider.GetRequiredKeyedService<IMemoryCache>("small"); } 
Sign up to request clarification or add additional context in comments.

2 Comments

What is this syntax of class? I know about record, but your example and the linked one (BigCacheConsumer) use class.
All of this example is used latest version of C# and this syntax of class is a new Primary Constructor proposal that is available in C# 12
0
container.AddSingletonWithKey<IPaymentStrategy>(PaymentType.CREDIT_CARD, sp=> ...); container.AddSingletonWithKey<IPaymentStrategy>(PaymentType.DEBIT_CARD, sp=> ...); container.AddSingletonWithKey<IPaymentStrategy>(PaymentType.PAYPAL, sp=> ...); 

then you can use it like this:

container.GetWithKey<IPaymentStrategy>(PaymentType.PAYPAL).processPayment(amount); 

Whether you create class which links all of those deps or access it directly from container is up to you. I would invoke it directly simply because they can have different lifetime in ioc, for example like this:

container.AddTransientWithKey<IPaymentStrategy>(PaymentType.DEBIT_CARD, sp=> ...); 

or binded to http request scope (because you want some info from it, like UserIdentity, for example):

container.AddScopedWithKey<IPaymentStrategy>(PaymentType.DEBIT_CARD, sp=> ...); 

Then:

public class ComplexPaymentStrategy { private readonly IContainer _container; public ComplexPaymentStrategy(IContainer container){_container = container;} public void ProcessPayment(PaymentType type, double amount) { _container.GetWithKey<IPaymentStrategy>(PaymentType.PAYPAL).processPayment(amount); } } 

And its nice to use, because you are free to choose lifetime of any implementation, whether it is scope, singletone, transient or scoped to some other objects, like caches/db_snapshots/http_context/etc.

3 Comments

using the container directly means using the ServiceLocator pattern which would create ambiguity, hide dependencies, and lower the transparency.
@R.Abbasi all this rules from my experience is big BS. Im yet to encounter horrors of using service locator directly, but I did encounter horrors of NOT using it (for the same reasons you described) and creating monstrosity cthulhu-like constructors. Dynamic patterns should be allowed to use dynamic dependencies, otherwise you get something like recent .net core - 100mb "hello world" for f sake, whereas it could have loaded only things it needed, but "it will lower transparency" argument prevailed.
Believe me, I encountered it and it is a hell. If you are using it in your framework layer it's acceptable otherwise it makes a mess. But I agree with you it can be used in exceptions as long as it's under control. I will post a workaround to get free of the container.
0

In your first class, you set the payment type via a separate method. This method can be called at anytime. In your second class, you set the payment type via the constructor. Once the class is created, you cannot change the payment type anymore.

Your two classes are thus not equivalent to each other. You could change your second class to make it equivalent to your first one:

public class PaymentProcessor { private IPaymentStrategy paymentStrategy; public void setPaymentType(IPaymentStrategy paymentType) { this.paymentStrategy = paymentType; } public void processPayment(double amount) { if(paymentStrategy == null) { throw InvalidOperationException("call SetPaymentType first!"); } paymentStrategy.processPayment(amount); } } 

Note that this approach is not thread-safe (if you want to use the class from different threads).

You might even create an overload for setPaymentType that also accepts the enum:

public void setPaymentType(PaymentType paymentType) { this.paymentStrategy = paymentType switch { PaymentType.CREDIT_CARD => new CreditCardPaymentStrategy(), PaymentType.DEBIT_CARD => new DebitCardPaymentStrategy(), PaymentType.PAYPAL => new PaypalPaymentStrategy(), _ => throw new Excepetion("invalid payment type") }; } 

Comments

0

In addition to @eocron's answer, you can use the factory pattern to get free of the container. Accessing to the container must be under control otherwise it can get the project messy (service locator pattern disadvantages). So you should at least do all your creation via the factory to control the situation.

On the other hand, the factory is more flexible than getting the service with a key because you can make decisions based on more complex conditions. But you have to have access to the actual instances in the factory which makes it difficult.

One way is to add a property in the factory for every strategy. But it isn't good if the instantiation of any of the strategies is heavy.

The other way is to add a property in the factory for every strategy that holds the creator Func of the specific strategy. Then pass the creators in the factory class constructor. Something like this:

public interface IStrategy { } public class StrategyA: IStrategy { } public class StrategyB: IStrategy { } public class StrategyFactory { public Func<StrategyA> StrategyACreator { get; set; } public Func<StrategyB> StrategyBCreator { get; set; } public StrategyFactory(Func<StrategyB> strategyBCreator, Func<StrategyA> strategyACreator) { StrategyBCreator = strategyBCreator; StrategyACreator = strategyACreator; } public IStrategy Create(bool condition) { if (condition) return StrategyACreator(); return StrategyBCreator(); } } 

Then you can register the factory with the help of the container in the startup.

builder.Services.AddScoped<StrategyA>(); builder.Services.AddScoped<StrategyB>(); builder.Services.AddScoped(x=> new StrategyFactory(()=> x.GetRequiredService<StrategyB>(), () => x.GetRequiredService<StrategyA>()) ); 

With this solution, you can be sure that the time of instantiation is on demand.

Comments

0

If you are sure that there won't be two strategies for one payment type you can use a factory pattern. Simply create PaymentStrategyFabric and get strategies from it. Very simplified example is shown below:

public class PaymentStrategyFabric { private readonly Dictionary<PaymentType, IPaymentStrategy> _strategies; public PaymentStrategyFabric() { _strategies = new Dictionary<PaymentType, IPaymentStrategy> { { PaymentType.CREDIT_CARD, new CreditCardPaymentStrategy() }, // and so on } } public IPaymentStrategy GetStrategyByType(PaymentType paymentType) { return _strategies[paymentType]; } } 

You can add different logic to your fabric initialization and different scenarios of recieving a strategy (like TryGetStrategy and etc.).

3 Comments

Do you mean factory pattern?
The flaw of this design is you have to instantiate every strategy. Another flaw is that maybe some of the strategies can't be instantiated when they are not going to be chosen( because the dependencies that they have must be passed in runtime. )
As I mentioned, it is only an idea or/and a simplified example of how it can be done. Of course it could be modified: support for the laziness could be added and dependencies could be passed.
0

You can also inject services directly by using your already defined PaymentType enumeration using a delegate.

public delegate IPaymentStrategy PaymentResolver(PaymentType paymentType); 

Then in your DI wireup... you can do the following.

services.AddSingleton<PaymentResolver>(serviceProvier => paymentType => { switch (paymentType) { PaymentType.CREDIT_CARD => new CreditCardPaymentStrategy(), PaymentType.DEBIT_CARD => new DebitCardPaymentStrategy(), PaymentType.PAYPAL => new PaypalPaymentStrategy(), _ => throw new Excepetion("invalid payment type") } }); 

Then when you need to use an instance you simply inject the delegate and specify the payment type to resolve at runtime. You will still need to register all the strategies but it offers a clean solution.

public class CreditTransaction { private readonly IPaymentStrategy _payStrategy; public CreditTransaction(PaymentResolver paymentResolver) { _payStrategy = paymentResolver(PaymentType.CREDIT_CARD); } } 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.