2

I am building an app (.NET, but whatever) which is supposed to be able to send notifications to users.

Each Notification can either be a Reminder or a Newsletter, and I'll possibly add more types in the future. Each type is handled differently. Reminders are periodical, so I need to keep track of who is reminded of what, while newsletter is just broadcasted with each publish to all users.

Additionally, I can send notifications using any of several notification mediums or however you want to call those (Email, Push etc.). Those are modeled in the domain something like this:

public class Email
{
    public string Sender { get; set; }
    public string Recipient { get; set; } // email address on the user object
    public string Subject { get; set; }
    public string Body { get; set; }
}

public class Push
{
    public string Title { get; set; }
    public string Content { get; set; }
    public string DeviceId { get; set; } // some token on the user object
}

I'll most likely add more NotificationMedium types in the future as well.

So, each concrete notification can be of just one concrete NotificationType and can be sent via one or many NotificationMediums, with both evolving in variety independently.

I already have an IEmailSender and an IPushSender, both being implemented by concrete senders relying on specific third party services, and receiving one of those domain objects from above.

My first thought was to implement a Bridge Pattern, but I'm not really sure how that would work. I'm mostly insecure about how I'd unify Email, Push and other "mediums", since they do vary in detail (Push doesn't have a sender, and I don't want to make something rely on things it doesn't need).

I'm also pretty sure that each Notification must have its type stored in the database, maybe have some NotificationType table or even an enum. But this would mean that I'd have to have some way of converting a Notification to EmailNotification or PushNotification in the end, which would completely defeat the purpose of the Bridge Pattern.

What I'd like is to have a mechanism which recieves a Notification, handles it internally based on its NotificationType, and then send it off via one or more predefined NotificationMediums, which I could add or remove without needing to affect anything but the class that brings all of these concepts together.

P.S. The class that brings all of these concepts together will most probably be a CQRS Command Handler, since I'm already using MediatR in my project. I look at it as a kind of "composition root" for this use case, so I don't believe that changes anything in the big picture, but I though I'd state it just in case.

Any suggestions with this?

5
  • 3
    "I already have an IEmailSender and an IPushSender" - so, there isn't a unified ISender interface? Also, could you share what IEmailSender and IPushSender look like? I think the key shift in thinking that will help you is that you're not looking for a pattern per se, but for a way to rethink this process - what you need, I think, is to collect the data for each notification type in one place, encapsulate those into a Command object (or just a lambda), and pass that along somewhere else to be processed in a later step by a notification medium. Sep 23, 2021 at 22:02
  • @FilipMilovanović No, there isn't a unified ISender. Both senders have a public void method, but the inputs aren't the same. Somehing like IEmailSender.Send(Email message) and IPushSender.Send(Push message). Email sending requires four parameters (sender - from config, recipient - from User and subject/body - from Notification). Push is similar, but without the sender.
    – dzenesiz
    Sep 23, 2021 at 23:08
  • 2
    So, in that case, what you could do is wrap the notification in a Command (or a lambda) that returns some unified representation of the notification's content (you mentioned in a comment that it can be just a string, but if it needs more structure, it could be a dictionary of key-value pairs + maybe some common metadata), then push it to some sort of notification queue that's accessible to the code that calls Send. Pull out stuff specific to notification content out of Email and Push types (so things like Body, Content, possibly Title), let the object popped from the queue supply it. 1/2 Sep 23, 2021 at 23:29
  • 1
    How would doing something like that affect the interface of your senders? If it unifies them enough, then you could potentially have a polymorphic ISender. If they are still different, you could either try to do a similar thing (encapsulate in an object that removes the interface differences), or apply Visitor to the senders - however, in terms of extensibility, Visitor is the same as just switching on the sender type (easy to add operations, but hard to add new sender types), so I'd just use a switch statement - it should be confined to one place. 2/2 Sep 23, 2021 at 23:29
  • 1
    P.S. Unless the sender types themselves represent the "operations" side of the visitor (the Visitor-derived classes, that implement the Visit method) - that could work well if the notification queue item is the "Element", especially if you only have a single Element type - the queue item. P.P.S. For clarification about Visitor, see my answer here (especially the bit at the very bottom). Sep 23, 2021 at 23:35

2 Answers 2

4

You don't need the bridge pattern. You only need a method for initializing the content / body and the title / subject of a NotificationMedium from a Notification. For this, you may provide Content and Title as abstract string properties in the base class Notification and override them accordingly in derived classes Reminder and Newsletter.

In NotificationMedium (as a base class of Push and Email), provide an abstract initialization method InitFromNotification(Notification n), and implement it like this:

In Email:

 void InitFromNotification(Notification n)
 {
      this.Subject = n.Title;
      this.Body = n.Content;
 }     
  

In Push:

 void InitFromNotification(Notification n)
 {
      this.Title = n.Title;
      this.Content = n.Content;
 }     

Now, your handler class can take arbitrary notifications media and fill and send them with arbitrary notfications generically

       medium.InitFromNotification(myNotification);
       // another abstract method, implemented differently in different media
       medium.InitReceiverInformation(...);
       medium.Send();

To your question about different the different requirements for different types of notifications: sending reminders regularly to individual users is one use case. Sending newsletters to all users once is a different one. So if that can be separated that strict, it is probably not necessary to generalize this into one command handler - you may choose to have an individual one for each use case.

However, if you have more notification types, or, for example, also newsletters which are send repeatedly, you could try to generalize this, give a Notification an attribute like RepetitionType with values OnlyOneTime or InIntervals, so allowing to decide if a newsletter will be send repeatedly as well. And if RepetitionType becomes a type of its own, and InIntervals becomes a derived class with certain parameters (like the interval size), then you have indeed introduced the bridge pattern here.

2
  • That does work for the medium part of the equation. But what about the type? I'll send a Type.Reminder only if a user hasn't been reminded in a specific period (which comes form database). On the other hande, I'll broadcast Type.Newsletter to all users, and then forget about it. Would that logic be in the handler, in some kind of a switch statement, or could I encapsulate it and combine any type with any medium in the same handler? Thanks for what you wrote so far.
    – dzenesiz
    Sep 23, 2021 at 21:07
  • 1
    @dzenesiz: see my edit.
    – Doc Brown
    Sep 23, 2021 at 21:55
3

I might be wrong here, but it seem to me that you are trying to abstract something that is completely separate just because it has the same name. It is similar to the "product" problem - you have multiple departments and for each one, subscription means a different thing - sales cares about naming and price, warehouse care about how much they have in stock, etc.

You are talking about Newsletters and Reminders. Seems like these two are from different domains. One seems to lean on sales, the other one on customer support. We can define what Newsletter looks like and what Reminder looks like.

Newsletter has probably a date, title and content.

Reminder has probably a date, type, date due(?), recipient, title and content

These are two completely different things. One is intended to be sent out and forgotten, the other one is intended to reach a specific person.

Now that we defined each entity, we can move on to different mediums. You can have a generic EmailSender, which will send an email. You can also have a generic PushNotifier, which will send push notifications. What you then need is to have an adapter from Newsleter to Email and Push and an adapter from Reminder to Email and Push.

You could however invert this and have visitor pattern for each type of channel -> EmailSender.send(Newsletter)|send(Reminder) and PushNotifier.send(Newsletter)|send(Reminder). This however seems a bit odd, because both channels have to know about multiple domains...

I would go with first approach for this reason - keep the domains seperate and make an adapter from each type of notification to each type of channel.

3
  • I also suspect that I might be overreaching with this. What does hold true is that any and all disparate data can be unified in a single string - the content. With this, I care only about what the end user receives, and that, I think, can be the same. An email has a subject and a body, a push has a title and content. And any data - dates, numbers etc. can be included in the body. I did discard the Visitor pattern because it's, as you say, odd in this situation. If I do go with something similar to what you wrote, I'll definitely remember to accept your answer. Thanks!
    – dzenesiz
    Sep 23, 2021 at 21:15
  • 1
    I think it is an oversimplification to say that you can reduce both to title and content. You will probably have newsletters in a separate table and won't keep track of all the notifications sent, whereas you will keep track of reminders. If you expand the idea of newsletters beyond just spamming user, you can track click through rate of the campaign, how it affected the sales, etc. Reminders don't hold this value and never should. That is why I would avoid treating everything the same. Unless you are doing some toy project in which case be free to experiment with as much as you can :)
    – Blaž Mrak
    Sep 23, 2021 at 21:40
  • The oversimplification is only from the system to the user, disregarding how the system is handling each of these types internally. Of course, some advanced use-cases where I'd need properties other than Title and Content are imaginable, and I do think there is a value in considering them.
    – dzenesiz
    Sep 23, 2021 at 23:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.