There are several options here, but the two that to me are the most obvious are using a factory or the adapter pattern.
1.1 Use a factory
public class OrderService
{
private readonly IPaymentGatewayFactory _factory;
public void DoOrder(bool isFoo)
{
IPaymentGateway service = _factory.Create(isFoo);
service.Pay();
}
}
Where the factory can be:
public class PaymentGatewayFactory : IPaymentGatewayFactory
{
public PaymentGatewayFactory(PaymentGatewayFoo foo, PaymentGatewayBoo boo) {...}
public IPaymentGateway Create(bool isFoo) =>
isFoo ? this.foo : this.boo;
}
Pros and cons:
1.2 Factory variation
When the list of IPaymentGateway
implementations grows or is dynamic, the injection of a fixed list of payment implementations in the PaymentGatewayFactory
can be unsuitable. Alternatively, you can inject a collection of IPaymentGateway
implementations into the factory and pick the proper implementation based provided information. For instance:
public class PaymentGatewayFactory : IPaymentGatewayFactory
{
public PaymentGatewayFactory(IEnumerable<IPaymentGateway> gateways) {...}
public IPaymentGateway Create(string paymentType) =>
this.gateways.Single(gateway => gateway.Type == paymentType);
}
In this design, instead of having a two-state isFoo
boolean, there's a paymentType
, that can contain many values (e.g. Paypal
, Card
, BankTransfer
, etc). For the factory to deal with this dynamic scenario it needs to match the provided payment type with the injected IPaymentGateway
instances. Here I choose to add an string Type
property to the IPaymentGateway
interface that all implementations must implement. In your case:
class PaymentGatewayFoo : IPaymentGateway { public string Type => "Foo"; }
class PaymentGatewayBoo : IPaymentGateway { public string Type => "Boo"; }
2. Use an adapter
public class OrderService
{
private readonly IPaymentGateway gateway;
public void DoOrder(bool isFoo)
{
this.gateway.Pay(isFoo);
}
}
Where the adapter can be:
public class PaymentGatewayAdapter : IPaymentGateway
{
public PaymentGatewayAdapter(PaymentGatewayFoo foo, PaymentGatewayBoo boo) {...}
public void Pay(bool isFoo)
{
var service = isFoo ? this.foo : this.boo;
service.Pay(isFoo);
}
}
Pros and cons:
- Advantage of this is that the client only needs to be aware of a single abstraction.
- Downside it that this only works when it reasonable to supply the
isFoo
runtime data as method argument to the IPaymentGateway
implementations. You'll often find that the information sent to the Pay
method contains more information, including the part that you want to apply the condition to, but this might not always be the case. If it's not the case, adding this extra piece of information can pollute the IPaymentGateway
abstraction.
- With the adapter implementation you could make the same variation as shown above with the factory where you have more flexability, but the downside of having to pass on extra runtime data that might be unnatural to the abstraction will remain.
Alternative implementations
As you noticed, in my factory and adapter, the implementations are injected directly. Not even by their abstractions, but by their concrete types. This might seem strange, but doing so is completely fine as long as the adapter and factory are part of the application's entry point (a.k.a. the Composition Root).
But other, more dynamic options can be used, such as:
- Injecting a
Func<PaymentType, IPaymentGateway>
delegate to resolve the types.
- Injecting a
Dictionary<PaymentType, IPaymentGateway>
.
- Injecting a collection of IPaymentGateway implementations.
- Injecting the container itself
- Using dynamic filtering, as Armand suggests, but do note that is causes you to add the
Identifier
property to the interface, where it only exists for technical reasons. No consumer (other than the adapter or the factory) is interested in this identifier. It, therefore, doesn't belong to the interface. A better solution is to solve this problem in the Composition Root, possibly by marking the implementations with an attribute, for instance.