Implement two notification systems for an event with Strategy pattern

Viewed 338

I have a list of subscribes in my database

Subscriber:
string Name      <- required
string Phone     <- required
string EventName <- required
string Email     <- optional 

The subscriber can subscribe on some event by its name (EventName).

When an event occurs, I need to send a notification to all subscribers (where EventName==the event which occurred) via Email or SMS. If Email is not null, then send email. If Email is null, send SMS to Phone.

I my business logic I created this abstraction:

public interface INotification
{
   Task Send(string message, IEnumerable<Subscriber> subscribers);
}

and use it in event handler

var subscribes = _db.Subscribers.Where(x => x.EventName == event);
await _notification.Send(message, subscribes);

Then I'm going to use a Strategy pattern.

public interface INotificationStartegy
{
   Task Send(string message, string to);
}
public interface EmailNotificationStartegy : INotificationStartegy
{
   /// 
}
public interface SmsNotificationStartegy : INotificationStartegy
{
   /// 
}

Here is an example of INotification implementation

public class Notification : INotification
{
  public Notification(SmsNotificationStartegy smsSender, EmailNotificationStartegy emailSender) =>
     (_smsSender, _emailSender);

  public async Task Send(string message, IEnumerable<Subscriber> subscribers)
  {   
    foreach(Subscriber sub in subscribers)
    {
        if (!string.IsNullOrEmpty(sub.Email)
        {
            await _emailSender.Send(message, sub.Email);
        } else {
            await _smsSender.Send(message, sub.Phone);
        }
    }
  }
}

Could you please suggest if it is a proper way to use Strategy? I thought the Strategy pattern can help us to avoid if/switch in a code, but who will decide what strategy to use then? I'm confused, do I really need this INotification abstraction or can I use that forearch directly in my event handler?

Could anyone explain benefits of INotification? Or is it a wrong implementation?

What if I need to add one more strategy, I should always modify Notification? Are there any ways how to avoid it?

1 Answers

I think you misunderstood the strategy pattern. In your implementation you are creating single concrete notification object that is supposed to handle multiple functions for which you tried to implement this pattern. So in my mind, you would have a single Notification strategy, to which you would inject by constructor a single concrete implementation of INotification object, be it EmailNotification or SmsNotification.

To have multiple possible receivers that can decide if they can send a notification would be a job for mediator pattern.

If you really do not want to use mediator, then simplest solution would be to create multiple NotificationStrategy objects, each with single INotification injected and use / iterate through them - 2 different notification types = 2 strategy objects each with different INotification implementation injected.

Thanks to this, no more if-elsing - each INotification implementation decides for itself if notifying is possible for it's particular way with given data.

Another alternative (with some if-else but compatible more or less with Strategy pattern) would be to firstly analyse given data and then, when you know what you have, creating a proper strategy objects with injected adequate INotification concrete objects.

Edit for minimal working example for strategy pattern with multiple dispatchers:

using System.Threading.Tasks;
using System.Collections.Generic;
                    
public class Program
{
    public static async Task Main()
    {
        var subscribers = new List<Subscriber>(); // Data of subscribers - empty just for simplicity
        
        // Decide which strategy is good - sms, email or both?
        // use if-else, switch, pattern matching, external service or something else
        // Combine subscribers for each strategy into lists / other collections
        var smsSubscribers = new List<Subscriber>();
        var emailSubscribers = new List<Subscriber>();
        
        var smsDispatcher = new Dispatcher(new SmsStrategy());
        var emailDispatcher = new Dispatcher(new EmailStrategy());
        
        foreach(var entry in smsSubscribers)
        {
            await smsDispatcher.Send("message...", smsSubscribers);
        }
        
        foreach(var entry in emailSubscribers)
        {
            await emailDispatcher.Send("message...", smsSubscribers);
        }   
    }
    
    public class Dispatcher
    {
        INotificationStrategy _strategy;
        
        public Dispatcher(INotificationStrategy strategy)
        {
            _strategy = strategy;
        }
        
        public Task Send(string message, IEnumerable<Subscriber> subscribers)
        {
            return _strategy.Send(message, subscribers);
        }
    }
    
    public interface INotificationStrategy
    {
       Task Send(string message, IEnumerable<Subscriber> subscribers);
    }
    
    public class EmailStrategy : INotificationStrategy
    {
        public Task Send(string message, IEnumerable<Subscriber> subscribers)
        {
            // Check data, iterate over subscribers, decide if sending EMAIL is possible etc and SEND.
            return Task.CompletedTask; // Only here to remove compiler error
        }
    }
    
    public class SmsStrategy : INotificationStrategy
    {
        public Task Send(string message, IEnumerable<Subscriber> subscribers)
        {
            // Check data, iterate over subscribers, decide if sending SMS is possible etc and SEND.
            return Task.CompletedTask; // Only here to remove compiler error
        }
    }
    
    public class Subscriber
    {
        // Some subscriber data...
    }
}

Mind, that for multiple recipients a mediator patter would be more useful - check the mediatr project.

Related