mardi 29 septembre 2020

How to write SOLID code with the Factory Method pattern?

I am studying Design Patterns and I am having some troubles with the implementation of the Factory Method.

The code written is based on these 2 articles:

My code: github

The program receives a message and how it should log in a string format ("MEMORY", "FILE", ...) and a factory method is responsible for choosing which class to implement.

Problem

If the factory method is responsible for choosing which class to implement, this responsibility should be inside of the class, like this:

    class LoggerFactory
    {
        public ILogger CreateLogger(string loggerMedium) 
        {
            return loggerMedium switch
            {
                "MEMORY" => new InMemoryLogger(),
                "FILE" => new FileLogger(),
                "DB" => new DBLogger(),
                "REMOTE" => new RemoteServiceLogger()
            };
        }
    }

What if I have an enourmous list of loggers? Everytime I need to add a logger, I have to edit this factory class, adding a new condition. It can become a huge switch case violating the Open/Closed principle.

Trying to remove this switch/case statement, my factory now has a dictionary and two methods:

  • RegisterLogger (responsible for adding the classes into the dictionary)
  • CreateLogger (returns the registered object, associated with the input string)
    class LoggerFactory : ILoggerFactory
    {
        private readonly Dictionary<string, Func<ILogger>> loggers;

        public LoggerFactory()
        {
            loggers = new Dictionary<string, Func<ILogger>>();
        }

        public ILogger this[string loggerMedium] => CreateLogger(loggerMedium);

        public ILogger CreateLogger(string loggerMedium) => loggers[loggerMedium]();

        public void RegisterLogger(string loggerMedium, Func<ILogger> factoryMethod)
        {
            if (string.IsNullOrEmpty(loggerMedium)) return;
            if (factoryMethod is null) return;

            loggers[loggerMedium] = factoryMethod;
        }
    }

and at the startup, with dependency injection I register the loggers:

        _serviceProvider = new ServiceCollection()
                .AddSingleton<ILoggerFactory>(_ =>
                {
                    // Logic here to register loggers, so that it is possible to extend the factory without modifying it
                    // OCP (open/closed principle)
                    LoggerFactory factory = new LoggerFactory();

                    factory.RegisterLogger("MEMORY", () => new InMemoryLogger());
                    factory.RegisterLogger("FILE", () => new FileLogger());
                    factory.RegisterLogger("DB", () => new DBLogger());
                    factory.RegisterLogger("REMOTE_SERVICE", () => new RemoteServiceLogger());

                    return factory;
                })
                .BuildServiceProvider();
  1. By adding the RegisterLogger method I also added one more responsibility to the factory. Does that violates the Single Responsibility Principle?
  2. I don't think that the logic to register new loggers should really be at the startup. Is it clean?
  3. How keep it clean if the logic to choose which class to implement is really complex and can't be solved by registering in a dictionary?

Aucun commentaire:

Enregistrer un commentaire