In modern .NET Core development, combining the Strategy Pattern, Factory Method, and IEnumerable<IService>
built-in Dependency Injection provides a flexible, scalable solution. This pattern combination is ideal for handling dynamic behaviors, such as choosing different algorithms at runtime without modifying core logic.
This article will break down how to implement this approach with an example of sending notifications using different strategies (e.g., Email, SMS).
Key Concepts:
Strategy Pattern: Encapsulates a family of algorithms (or behaviors) and makes them interchangeable at runtime. Each algorithm is isolated within its own class.
Factory Method: Provides a way to instantiate objects dynamically based on runtime conditions.
IEnumerable<IService>
in .NET Core Dependency Injection: This allows for the injection of multiple implementations of a service interface (IService) into a class. It provides a way to access all registered implementations, enabling the consumer to iterate over the services and select the appropriate implementation based on specific needs.
Step-by-Step Example: Notification System
Let’s assume we need to send notifications via different channels like Email, SMS, and Push Notifications. Based on user preference, we dynamically choose the appropriate strategy.
1. Define the Strategy Interface
public interface INotificationStrategy
{
string Key { get; } // String key to identify the strategy
void Send(string message);
}
Each notification strategy (Email, SMS, etc.) implements the INotificationStrategy
interface.
2. Implement Concrete Strategies
public class EmailNotificationStrategy : INotificationStrategy
{
public string Key => "Email"; // Unique key for Email strategy
public void Send(string message) => Console.WriteLine($"Email sent: {message}");
}
public class SmsNotificationStrategy : INotificationStrategy
{
public string Key => "SMS"; // Unique key for SMS strategy
public void Send(string message) => Console.WriteLine($"SMS sent: {message}");
}
Each strategy encapsulates a specific notification behavior.
3. Factory Method: Inject All Strategies with IEnumerable<IService>
The factory dynamically chooses the appropriate strategy at runtime using IEnumerable<INotificationStrategy>
.
public class NotificationFactory
{
private readonly IEnumerable<INotificationStrategy> _strategies;
public NotificationFactory(IEnumerable<INotificationStrategy> strategies)
{
_strategies = strategies;
}
public INotificationStrategy GetStrategy(string notificationType)
{
// Use FirstOrDefault to find the strategy by key
var strategy = _strategies.FirstOrDefault(s => s.Key == notificationType);
return strategy ?? throw new ArgumentException("Invalid notification type");
}
}
Here, IEnumerable<INotificationStrategy>
injects all available strategies into the factory. This approach allows easy iteration over all strategies to pick the one that can handle the given notification type.
4. Register Services in DI Container
In the Startup.cs
or wherever services are configured, register all concrete strategies and the factory:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<INotificationStrategy, EmailNotificationStrategy>();
services.AddTransient<INotificationStrategy, SmsNotificationStrategy>();
services.AddSingleton<NotificationFactory>();
}
This ensures that each strategy is registered, and the NotificationFactory
can access them via IEnumerable<INotificationStrategy>
.
5. Service Using the Factory
public class NotificationService
{
private readonly NotificationFactory _factory;
public NotificationService(NotificationFactory factory)
{
_factory = factory;
}
public void SendNotification(string type, string message)
{
var strategy = _factory.GetStrategy(type);
strategy.Send(message);
}
}
Finally, the NotificationService
uses the factory to send a notification based on the user’s preference.
How IEnumerable Enhances These Patterns
Typical Strategy Pattern with Factory Method before IEnumerable<IService>
// Strategy interface
public interface INotification
{
void Send(string message);
}
// Concrete strategies
public class EmailNotification : INotification
{
public void Send(string message)
{
Console.WriteLine($"Email Notification: {message}");
}
}
public class SmsNotification : INotification
{
public void Send(string message)
{
Console.WriteLine($"SMS Notification: {message}");
}
}
// Factory for creating notifications
public static class NotificationFactory
{
public static INotification Create(string type)
{
return type switch
{
"Email" => new EmailNotification(),
"SMS" => new SmsNotification(),
_ => throw new NotImplementedException()
};
}
}
// Context class
public class NotificationService
{
private INotification _notification;
public void SetNotification(string type)
{
_notification = NotificationFactory.Create(type);
}
public void Notify(string message)
{
_notification?.Send(message);
}
}
// Client code
class Program
{
static void Main(string[] args)
{
var notificationService = new NotificationService();
// Set different notification strategies
notificationService.SetNotification("Email");
notificationService.Notify("Welcome to our service!");
notificationService.SetNotification("SMS");
notificationService.Notify("Your order has been shipped!");
}
}
Comparison
Feature | Typical Strategy Pattern + Factory Method | Using IEnumerable<INotification>
|
---|---|---|
Strategy Resolution | Factory method creates a specific strategy | Automatically injects all strategies via DI container |
Flexibility | Limited flexibility; adding new strategies requires updating the factory | More flexible; dynamically resolves strategies at runtime based on conditions |
Extensibility | Adding a new strategy requires modifying the factory code | Easily extendable; simply register new strategies in DI without changing existing code |
Adherence to Open/Closed | Violates the Open/Closed Principle when adding new strategies | Fully adheres to the Open/Closed Principle; new strategies can be added without modifying existing classes |
Tight Coupling | Factory is tightly coupled to specific strategy implementations | Loosely coupled; context only depends on the interface |
Conclusion
The combination of Strategy Pattern, Factory Method, and `.NET Core’s built-in DI with IEnumerable makes your system more flexible, extensible, and maintainable. The factory can dynamically choose from multiple strategies injected by DI, keeping your code clean and following SOLID principles.
This pattern is particularly useful in systems where behavior may change frequently, and new strategies need to be added without modifying core components.
Top comments (0)