using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace APIApplication.FactoryMethodPattern.Providers
{
public interface IRouteCalculationFactory
{
public IRouteCalculation CreateRouteCalculation();
public bool IsSupports(string type);
}
public class BusRouteFactory : IRouteCalculationFactory
{
public IRouteCalculation CreateRouteCalculation()
{
return new BusRoute();
}
public bool IsSupports(string travellingType)
=> travellingType.Equals("bus", StringComparison.OrdinalIgnoreCase);
}
public class BikeRouteFactory : IRouteCalculationFactory
{
public IRouteCalculation CreateRouteCalculation()
{
return new BikeRoute();
}
public bool IsSupports(string travellingType) => travellingType.Equals("bike", StringComparison.OrdinalIgnoreCase);
}
public class WalkRouteFactory : IRouteCalculationFactory
{
public IRouteCalculation CreateRouteCalculation()
{
return new WalkRoute();
}
public bool IsSupports(string travellingType) => travellingType.Equals("walk", StringComparison.OrdinalIgnoreCase);
}
public interface IRouteCalculation
{
int CalculateDistance(string source, string destination);
int CalculateTime(string source, string destination);
}
public class BusRoute : IRouteCalculation
{
public int CalculateDistance(string source, string destination)
{
return 150;
}
public int CalculateTime(string source, string destination)
{
return 60;
}
}
public class BikeRoute : IRouteCalculation
{
public int CalculateDistance(string source, string destination)
{
return 100;
}
public int CalculateTime(string source, string destination)
{
return 40;
}
}
public class WalkRoute : IRouteCalculation
{
public int CalculateDistance(string source, string destination)
{
return 150;
}
public int CalculateTime(string source, string destination)
{
return 200;
}
}
}
public class RouteCalculationFactoryProvider
{
private readonly IEnumerable<IRouteCalculationFactory> _routeCalculationFactories;
public RouteCalculationFactoryProvider(IEnumerable<IRouteCalculationFactory> routeCalculationFactories)
{
_routeCalculationFactories = routeCalculationFactories;
}
public IRouteCalculationFactory GetFactory(string travellingType)
{
var factory = _routeCalculationFactories.FirstOrDefault(f => f.IsSupports(travellingType));
if (factory == null)
{
throw new InvalidOperationException("Unknown travelling type");
}
return factory;
}
}
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);
}
}
}
How IEnumerable<T> Injection Works in .NET Core
When you register multiple implementations of an interface in the DI container, the framework automatically groups these implementations into an IEnumerable<T> when you inject them.
Step-by-Step Explanation:
- Registering Multiple Implementations:
- In the
Program.csorStartup.csfile, you registered multiple classes (BusRouteFactory,BikeRouteFactory,WalkRouteFactory) that all implement theIRouteCalculatorFactoryinterface
- In the
builder.Services.AddSingleton<IRouteCalculatorFactory, BusRouteFactory>();
builder.Services.AddSingleton<IRouteCalculatorFactory, BikeRouteFactory>();
builder.Services.AddSingleton<IRouteCalculatorFactory, WalkRouteFactory>();
- Here, each
AddSingleton<IRouteCalculatorFactory, SomeFactory>()call registers a concrete factory type as anIRouteCalculatorFactoryservice in the DI container. - Resolving
IEnumerable<IRouteCalculatorFactory>:- When a class (like
RouteCalculatorFactoryProvider) requests anIEnumerable<IRouteCalculatorFactory>in its constructor, the DI container:- Looks for all the services registered as
IRouteCalculatorFactory. - Automatically creates an
IEnumerable<IRouteCalculatorFactory>and injects it into the constructor.
- Looks for all the services registered as
public RouteCalculatorFactoryProvider(IEnumerable<IRouteCalculatorFactory> factories) { _factories = factories; }- At runtime,
_factorieswill contain all the instances of theBusRouteFactory,BikeRouteFactory, andWalkRouteFactorythat were registered.
- When a class (like
Why It Works
- Built-in Feature of .NET Core DI: This behavior is a built-in feature of the .NET Core DI system. When you register multiple services of the same interface type, and then request an
IEnumerable<T>of that interface, the DI container automatically collects all the registered services into that enumerable. - Automatic Grouping: This automatic grouping allows you to easily work with multiple implementations of an interface without needing to manually handle the registrations.
Practical Implications
- Flexibility: You can add or remove implementations without changing the consuming code. Just register or unregister services in the DI container.
- Scalability: As your application grows, you can continue to add new implementations of
IRouteCalculatorFactory, and they will automatically be included in theIEnumerable<IRouteCalculatorFactory>wherever it is injected.
Summary
In .NET Core, when you inject an IEnumerable<T> of an interface, the DI container automatically provides all the registered implementations of that interface. This allows you to inject multiple services easily and work with them in a flexible and scalable way. The RouteCalculatorFactoryProvider constructor receives all the factories because they were all registered as services in the DI container.
The Factory Method pattern is a widely used creational design pattern that provides an interface for creating objects in a super-class but allows subclasses to alter the type of objects that will be created. While it offers many benefits, it also comes with some challenges and potential downsides:
1. Increased Complexity
- Multiple Classes and Subclasses: The Factory Method pattern introduces multiple classes and subclasses, which can make the codebase more complex. For each new type, a new factory class and potentially a new product class must be created.
- Overhead in Simple Scenarios: In cases where the object creation logic is simple, using the Factory Method pattern can add unnecessary complexity. A simple constructor or a static method might suffice, but the pattern forces the creation of additional classes.
2. Tight Coupling Between Factory and Products
- Strong Dependency: The factory classes are tightly coupled to the product classes they create. If the product classes change frequently, the factories need to be updated as well, leading to potential maintenance issues.
- Difficulty in Changing Factories: If you want to replace a factory with another one, you may need to update all the places where the factory is used, leading to tight coupling between client code and specific factory implementations.
3. Violation of the Open/Closed Principle
- Modification of Existing Code: When a new product type is introduced, existing factory classes might need to be modified to accommodate the new type. This violates the Open/Closed Principle, which states that software entities should be open for extension but closed for modification.
- Need for Continuous Updates: Each time a new product is added, the factory method or switch cases in the factory may need to be updated, leading to potential bugs or missed cases.
4. Difficulty in Managing Multiple Factories
- Complexity in Managing Factories: In large applications with many product types, managing multiple factory classes can become cumbersome. This is especially true if the factories have dependencies or if the selection logic for the appropriate factory is complex.
- Scalability Issues: As the number of product types increases, the number of factory classes also increases, which can make the system harder to maintain and scale.
5. Limited Flexibility
- Rigid Structure: The Factory Method pattern is often rigid in its structure, making it difficult to adapt to changes in the product creation process. If the creation process becomes more complex or needs to involve multiple steps, the pattern may not be flexible enough to accommodate those changes.
- Inheritance-Based: The pattern relies heavily on inheritance, which can lead to less flexible and harder-to-maintain code. Modern design practices often favor composition over inheritance to achieve greater flexibility.
6. Potential Performance Overhead
- Indirection and Abstraction: The Factory Method pattern introduces an additional layer of abstraction, which can lead to a slight performance overhead due to the indirection involved in object creation.
- Object Creation Overhead: If the factory method involves complex logic or the creation of large numbers of objects, it might introduce performance concerns, especially in performance-critical applications.
7. Testing Challenges
- Mocking Factories: When writing unit tests, mocking factories can be tricky, especially if the factory method involves complex logic or dependencies. Testing can become more complicated if the factory method is tightly coupled with other parts of the system.
- Increased Number of Classes to Test: Since the Factory Method pattern introduces more classes (factories and products), there are more units that need to be tested, increasing the overall testing effort.
Summary
While the Factory Method pattern provides a robust way to create objects and promotes the use of interfaces and polymorphism, it comes with challenges such as increased complexity, tight coupling, potential violation of the Open/Closed Principle, and testing difficulties. It’s essential to carefully consider these challenges and evaluate whether the Factory Method pattern is the best fit for your specific use case. In some situations, simpler alternatives like using constructors or static factory methods might be more appropriate.