1

I wonder if this Factory method pattern implementation violates the Open-Closed principle from the SOLID principles, because the switch in GetTradeManager has to be updated everytime we add another Trade Manager to the project. If it violates it, how do I make it meet the Open-Closed principle requirements?

services.AddSingleton<LiveTradeManager>();
services.AddSingleton<BacktestTradeManager>();
services.AddSingleton<ITradeManagerFactory, TradeManagerFactory>();

// the types
public interface ITradeManager
{
    Task RunAsync(CancellationToken cancellationToken);
}

public class BacktestTradeManager : ITradeManager
{
    public Task RunAsync(CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

public class LiveTradeManager : ITradeManager
{
    public Task RunAsync(CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

public class TradeManagerFactory : ITradeManagerFactory
{
    private readonly IServiceProvider _serviceProvider;

    public TradeManagerFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public ITradeManager GetTradeManager(TradeManagerType tradeManagerType)
    {
        return tradeManagerType switch
        {
            TradeManagerType.Live => _serviceProvider.GetService<LiveTradeManager>() ?? throw new NullReferenceException(),
            TradeManagerType.Backtest => _serviceProvider.GetService<BacktestTradeManager>() ?? throw new NullReferenceException(),
            _ => throw new ArgumentOutOfRangeException(nameof(tradeManagerType), tradeManagerType, null)
        };
    }
}

Edit:

A Factory Pattern that will satisfy the Open/Closed Principle? doesn't solve my question, because I used to have something similar to:

public class BinanceClientFactory : IBinanceClientFactory
{
    private IServiceProvider Provider { get; }

    public BinanceClientFactory(IServiceProvider provider)
    {
        Provider = provider;
    }

    public IBinanceClient GetBinanceClient(WalletType walletType)
    {
        return walletType switch
        {
            WalletType.Spot => ActivatorUtilities.CreateInstance<BinanceClientSpot>(Provider),
            WalletType.Margin => ActivatorUtilities.CreateInstance<BinanceClientMargin>(Provider),
            WalletType.Futures => ActivatorUtilities.CreateInstance<BinanceClientFutures>(Provider),
            _ => null,
        };
    }
}

which doesn't add the created objects to the IoC container and I had to manually Dispose() them. Which is really not what I want to.

In the link you gave

public static class AnimalFactory
{
    public static Animal CreateAnimal(AnimalInfo aInfo)
    {
        if (aInfo is DogInfo)
            return new Dog(aInfo as DogInfo);
        if (aInfo is CatInfo)
            return new Cat(aInfo as CatInfo);
        return null;
    }
}

they are creating the objects outside the IoC container, which leads to what I described above. I need the IoC container to call Dispose() for me.

nop
  • 4,711
  • 6
  • 32
  • 93
  • 2
    "What do you think" (i.e. soliciting opinions) is a lousy basis for a question on any SE site, but if you want to reformulate it to put your maintainability concern about the current (working) code front and center (rather than whether it adheres to any principle) it could be suitable for [softwareengineering.SE](https://softwareengineering.stackexchange.com). – Jeroen Mostert Mar 03 '21 at 12:41
  • I think this question is already asked. Check: https://stackoverflow.com/questions/7876462/a-factory-pattern-that-will-satisfy-the-open-closed-principle – Bart Mar 03 '21 at 12:46
  • @JeroenMostert, corrected it. It seems like it violates it and it would be nice to hear solutions. – nop Mar 03 '21 at 12:54
  • 1
    Sorry, but this whole question does not make too much sense. Factory method is a creational pattern but in your case it doesn't create anything. For me it does look like that you have created a Service Locator on a top of an IoC container. – Peter Csala Mar 03 '21 at 12:59
  • @PeterCsala, what do you suggest to make it work like one and to follow the open-closed principle? – nop Mar 03 '21 at 13:40
  • Please check that link which was suggested by Bart. It contains a couple of viable alternatives (like using an abstract base class with Create method, or using reflection to create a new instance based on the name of the class) – Peter Csala Mar 03 '21 at 13:45
  • Does this answer your question? [A Factory Pattern that will satisfy the Open/Closed Principle?](https://stackoverflow.com/questions/7876462/a-factory-pattern-that-will-satisfy-the-open-closed-principle) – Ian Kemp Mar 03 '21 at 13:53
  • 1
    You are asking the wrong question. The correct question is "does this code violate OCP **in as minimal a manner as possible to make the system function**?", and the answer is "yes". A factory method will almost always violate OCP, *and that's okay* as long as it's the only part of your system that does. – Ian Kemp Mar 03 '21 at 14:02
  • Thank you for your answers. However, https://stackoverflow.com/questions/7876462/a-factory-pattern-that-will-satisfy-the-open-closed-principle doesn't solve what I need. The reason is that it creates the object outside the container, which means it won't automatically call its `Dispose()`. – nop Mar 03 '21 at 14:13
  • I used to have something similar to https://pastebin.com/ExjGkw0L, but it also doesn't add the created objects to the container, which means I would have to manually handle their disposal, which is not what I need. – nop Mar 03 '21 at 14:16
  • You closed the question, but what's the summarized answer? – nop Mar 03 '21 at 14:23
  • 1
    There have been other very similar questions that have gotten good responses on SO in the past which I based my answer below on (see [here](https://stackoverflow.com/questions/31950362/factory-method-with-di-and-ioc) and [here](https://stackoverflow.com/questions/42402064/using-a-strategy-and-factory-pattern-with-dependency-injection)). Maybe your question is being viewed more controversially because of the "Does this violate this principal?" aspect. It might also help to describe how the factory is used/called. Otherwise, assumptions will be made – devNull Mar 03 '21 at 18:02
  • @devNull, thanks for your answer, I upvoted it, but it's kinda same as mine. I'm trying to find a such built-in Factory method in dotnet's GitHub repo. I always look for real examples there. https://github.com/dotnet/orleans/blob/cafbbe4b2526f86e44f0e431b1070b0554617807/src/Orleans.Runtime/Utilities/FactoryUtility.cs They are using similar to my previous `ActivatorUtilities.CreateInstance`. @PeterCsala is correct that it's not really a Factory method because I have to create a new instance, so it can really be. But I want the IoC container to handle Dispose() itself, so it's not possible – nop Mar 03 '21 at 20:57

1 Answers1

1

One common way of handling this that would avoid violating the Open/Closed Principle that you're concerned with would be to register the types as strategies and use those in the factory. To start, I'd recommend creating an internal strategy interface (internal since it's an implementation detail, not part of the public interface):

internal interface ITradeManagerStrategy : ITradeManager
{
    bool SupportsType(TradeManagerType tradeManagerType);
}

And then have each TradeManager implement this interface (note that they still end up implementing the public interface ITradeManager):

public class BacktestTradeManager : ITradeManagerStrategy
{
    public Task RunAsync(CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public bool SupportsType(TradeManagerType tradeManagerType) => tradeManagerType == TradeManagerType.Backtest;
}

public class LiveTradeManager : ITradeManagerStrategy
{
    public Task RunAsync(CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public bool SupportsType(TradeManagerType tradeManagerType) => tradeManagerType == TradeManagerType.Live;
}

And then your factory can resolve all of the strategies and return the instance (through the public interface) at runtime using the strategy method:

internal class TradeManagerFactory : ITradeManagerFactory
{
    private readonly IEnumerable<ITradeManagerStrategy> _tradeManagerStrategies;

    public TradeManagerFactory(IEnumerable<ITradeManagerStrategy> tradeManagerStrategies)
    {
        _tradeManagerStrategies = tradeManagerStrategies;
    }

    public ITradeManager GetTradeManager(TradeManagerType tradeManagerType)
    {
        return _tradeManagerStrategies.FirstOrDefault(s => s.SupportsType(tradeManagerType))
            ?? throw new ArgumentOutOfRangeException(nameof(tradeManagerType), tradeManagerType, null);
    }
}

Note that this requires registering the internal interfaces with DI along with the factory, for example:

.AddTransient<ITradeManagerStrategy, BacktestTradeManager>()
.AddTransient<ITradeManagerStrategy, LiveTradeManager>()
.AddTransient<ITradeManagerFactory, TradeManagerFactory>()
devNull
  • 3,849
  • 1
  • 16
  • 16
  • This solution is merely moving things around to hide the coupling: you still have the enum and you still have the factory method. – Ian Kemp Mar 03 '21 at 13:57
  • @IanKemp what would you suggest otherwise? I find this solution better than many others I've seen before. More elegant. – sharpc May 26 '23 at 18:11
  • @sharpc the "problematic" code presented in the question, that uses `switch` expressions/statements, achieves exactly the same outcome while being far simpler. I'lll take that over any perceived elegance. – Ian Kemp May 26 '23 at 21:00