Factory Method Pattern

Defines an interface for creating an object, but lets subclasses decide which class to instantiate.

Understanding the Factory Method Pattern

Imagine a logging system where you want to log messages to different targets (e.g., console, file, database). You want to make the logging mechanism flexible and easily configurable.. This is essentially the Factory Method pattern.

Key Components:

public interface ILogger
{
    void Log(string message);
}

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

public class FileLogger : ILogger
{
    public void Log(string message)
    {
        // Write to a file
    }
}

public abstract class LoggerFactory
{
    public abstract ILogger CreateLogger();
}

public class ConsoleLoggerFactory : LoggerFactory
{
    public override ILogger CreateLogger()
    {
        return new ConsoleLogger();
    }
}

public class FileLoggerFactory : LoggerFactory
{
    public override ILogger CreateLogger()
    {
        return new FileLogger();
    }
}

By using DI, we can inject the appropriate LoggerFactory into a class that needs logging, allowing for easy configuration and switching of logging implementations.

public class MyClass
{
    private readonly ILogger _logger;

    public MyClass(ILoggerFactory factory)
    {
        _logger = factory.CreateLogger();
    }

    public void DoSomething()
    {
        _logger.Log("Something happened");
    }
}

Register the LoggerFactory in Startup.cs (or Program.cs in .NET 6+):

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<ILoggerFactory, ConsoleLoggerFactory>();
    // ... other services
}

Lets try with another example

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CreationalDesignPatterns
{
    public class FactoryMethodDesignPattern
    {
        public static void Main()
        {
            Console.WriteLine("Enter your starting location point");

            string source= Console.ReadLine();
            Console.WriteLine("Enter your destination location point");
            string destination=Console.ReadLine();

            Console.WriteLine("enter travelling type");
            string travellingType= Console.ReadLine();

          
            RouteCalculatorFactory routeCalculatorFactory = RouteCalculatorFactory.GetFactory(travellingType);
            IRouteCalculator routeCalculator = routeCalculatorFactory.CreateRouteCalculator();


            int distance = routeCalculator.CalculateDistance(source, destination);
            int time = routeCalculator.CalculateTime(source, destination);
            Console.WriteLine($"Distance: {distance} km, Time: {time} minutes");


        }
    }
    public abstract class RouteCalculatorFactory
    {
        public abstract IRouteCalculator CreateRouteCalculator();
        public static RouteCalculatorFactory GetFactory(string travellingType)
        {
            return travellingType.ToLower() switch
            {
                "bus" => new BusRouteFactory(),
                "bike" => new BikeRouteFactory(),
                "walk" => new WalkRouteFactory(),
                _ => throw new InvalidOperationException("Unknown travelling type")
            };
        }
    }
    public class BusRouteFactory : RouteCalculatorFactory
    {
        public override IRouteCalculator CreateRouteCalculator()
        {
            return new BusRoute();
        }
    }
    public class BikeRouteFactory : RouteCalculatorFactory
    {
        public override IRouteCalculator CreateRouteCalculator()
        {
            return new BikeRoute();
        }
    }
    public class WalkRouteFactory : RouteCalculatorFactory
    {
        public override IRouteCalculator CreateRouteCalculator()
        {
           return new WalkRoute();
        }
    }
    public interface IRouteCalculator
    {
        int CalculateDistance(string source, string destination);
        int CalculateTime(string source, string destination);
    }
    public class BusRoute : IRouteCalculator
    {
        public int CalculateDistance(string source, string destination)
        {
            return 150;
        }

        public int CalculateTime(string source, string destination)
        {
            return 60;
        }
    }
    public class BikeRoute : IRouteCalculator
    {
        public int CalculateDistance(string source, string destination)
        {
            return 100;
        }

        public int CalculateTime(string source, string destination)
        {
            return 40;
        }
    }
    public class WalkRoute : IRouteCalculator
    {
        public int CalculateDistance(string source, string destination)
        {
            return 150;
        }

        public int CalculateTime(string source, string destination)
        {
            return 200;
        }
    }
}

Key Elements of the Factory Method Pattern in Your Example:

  1. Abstract Factory Class (RouteCalculatorFactory):
    • You have an abstract class RouteCalculatorFactory with the abstract method CreateRouteCalculator(). This is the factory method that must be implemented by the concrete factories (BusRouteFactory, BikeRouteFactory, WalkRouteFactory).
  2. Concrete Factory Classes:
    • The concrete classes BusRouteFactory, BikeRouteFactory, and WalkRouteFactory inherit from RouteCalculatorFactory and implement the CreateRouteCalculator() method. Each of these classes is responsible for creating an instance of the corresponding IRouteCalculator implementation (BusRoute, BikeRoute, WalkRoute).
  3. Product Interface (IRouteCalculator):
    • The IRouteCalculator interface defines the methods CalculateDistance and CalculateTime, which are implemented by the concrete route classes (BusRoute, BikeRoute, WalkRoute).
  4. Concrete Product Classes:
    • BusRoute, BikeRoute, and WalkRoute are the concrete implementations of the IRouteCalculator interface. Each of these classes provides specific logic for calculating the distance and time for a particular mode of transportation.

Challenge:

Adding a new type of route (e.g., “car”) would require modifying the RouteCalculatorFactory.GetFactory method to include a case for the new type. While this approach is straightforward, it does violate the Open/Closed Principle of SOLID design, which states that classes should be open for extension but closed for modification.

Alternative Approaches to Avoid Modifying RouteCalculatorFactory

Reflection-Based Factory Registration:

Dependency Injection with Factory Registration:

Strategy Pattern:

Reflection-Based Factory Registration

You could use a dictionary to map travel types to factory classes, which you populate at runtime. Here’s a basic example:

public abstract class RouteCalculatorFactory
{
    private static readonly Dictionary<string, Type> _factories = new Dictionary<string, Type>();

    public static void RegisterFactory(string travelType, Type factoryType)
    {
        if (factoryType.IsSubclassOf(typeof(RouteCalculatorFactory)))
        {
            _factories[travelType.ToLower()] = factoryType;
        }
    }

    public static RouteCalculatorFactory GetFactory(string travellingType)
    {
        if (_factories.TryGetValue(travellingType.ToLower(), out var factoryType))
        {
            return (RouteCalculatorFactory)Activator.CreateInstance(factoryType);
        }
        throw new InvalidOperationException("Unknown travelling type");
    }

    public abstract IRouteCalculator CreateRouteCalculator();
}

Usage: Register factories at the start of your application:

RouteCalculatorFactory.RegisterFactory("bus", typeof(BusRouteFactory));
RouteCalculatorFactory.RegisterFactory("bike", typeof(BikeRouteFactory));
RouteCalculatorFactory.RegisterFactory("walk", typeof(WalkRouteFactory));
// New factories can be added without modifying the GetFactory method
2. Dependency Injection with Factory Registration

If you’re using a DI framework (like Microsoft.Extensions.DependencyInjection), you could register each factory with a specific key, and then resolve it based on that key:

public interface IRouteCalculatorFactory
{
    IRouteCalculator CreateRouteCalculator();
}

public class RouteCalculatorFactoryProvider
{
    private readonly IServiceProvider _serviceProvider;

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

    public IRouteCalculatorFactory GetFactory(string travellingType)
    {
        var factory = _serviceProvider.GetServices<IRouteCalculatorFactory>()
                                      .FirstOrDefault(f => f.Supports(travellingType));

        if (factory == null)
            throw new InvalidOperationException("Unknown travelling type");

        return factory;
    }
}

Factory Classes would implement an interface that includes a Supports method to check if the factory supports a specific traveling type:

public interface IRouteCalculatorFactory
{
    bool Supports(string travellingType);
    IRouteCalculator CreateRouteCalculator();
}

public class BusRouteFactory : IRouteCalculatorFactory
{
    public bool Supports(string travellingType) => travellingType.Equals("bus", StringComparison.OrdinalIgnoreCase);
    public IRouteCalculator CreateRouteCalculator() => new BusRoute();
}

Register Factories in DI Container:

services.AddSingleton<IRouteCalculatorFactory, BusRouteFactory>();
services.AddSingleton<IRouteCalculatorFactory, BikeRouteFactory>();
services.AddSingleton<IRouteCalculatorFactory, WalkRouteFactory>();

Now inject the RouteCalculatorFactoryProvider into controller

using APIApplication.FactoryMethodPattern.Providers;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class RouteCalculatorController : ControllerBase
{
    private readonly RouteCalculationFactoryProvider _factoryProvider;

    public RouteCalculatorController(RouteCalculationFactoryProvider factoryProvider)
    {
        _factoryProvider = factoryProvider;
    }

    [HttpGet("calculate")]
    public IActionResult CalculateRoute(string source, string destination, string travellingType)
    {
        try
        {
            var factory = _factoryProvider.GetFactory(travellingType);
            var routeCalculator = factory.CreateRouteCalculation();

            var distance = routeCalculator.CalculateDistance(source, destination);
            var time = routeCalculator.CalculateTime(source, destination);

            return Ok(new { Distance = distance, Time = time });
        }
        catch (InvalidOperationException ex)
        {
            return BadRequest(ex.Message);
        }
    }
}

Leave a comment