3

It is said that visitor pattern is applicable to problems where objects rarely change but we add actions on those objects more frequently.

What if the objects are changing too though? For example we might have a NotificationSource class where it can have many children such as EmailNotificationSource or SmsNotificationSource or whatever. For any incoming messaging system we might want to add another source and extend our notification system.

We can imagine actions on these models or classes may add rapidly too. NotificationSerializer or NotificationSender or whatever functionality we might define on these classes to name a few.

Using switch-cases on each of these to find which NotificationSource object we are deadling with is discouraged and I understand why. It's not much maintainable.

Visitor pattern is not working well too because I don't like a NotificationSender class with endless methods for each child like handleSendingEmailNotification. I don't like the idea of adding accept method to each of the NotificationSource child either. I feel like it's cheating and it violates SRP (And also what is we don't have access to NotificationSource classes? Maybe it's a installed dependency.)

I don't have any scalable (on both children and actions) and maintainable solution to this problem. Can anyone help me with this?

4
  • I don't have any scalable (on both children and actions) and maintainable solution to this problem. Maybe because you are skipping the main problem. Flawed abstraction in NotificationSource. I say this because you already pointed that Using switch-cases on each of these to find which NotificationSource object we are dealing with is discouraged. why do you have to check children's type? Why don't treat them all the same way? Then it replaces a switch with a loop. If for any reason you can't NotificationSource it's already violating SRP.
    – Laiv
    Commented Aug 16, 2021 at 19:36
  • I need to use polymorphism to achieve that. Let's say we do this and add a send method to each NotificationSource. Then we add a serialize method to the same class with the same reason. Then we add renderToString. And so on. All of a sudden we have a class with so many unrelated behaviour violating the SRP principle. Am I right? @Laiv Commented Aug 16, 2021 at 19:55
  • 3
    And so on. All of a sudden we have a class with so many unrelated behaviours violating the SRP principle. Am I right? yes, but encapsulating features into classes results in the same problem. It just decouples it. So, why do you need NotificationSource capable of so many different things? Are these features configurable in runtime? What motivation took you to the Visitor pattern?
    – Laiv
    Commented Aug 16, 2021 at 20:16
  • Can you describe your design? If you want to separate functionalities into separate classes and avoid violating SRP, then you need a way to be able to have find for each type of notification source its handler for each of these hierarchies of action classes. Commented Aug 16, 2021 at 20:57

2 Answers 2

2

The structure of the classic visitor pattern gives each combination of "object type" and "action type" a place for a method which handles the specific combination of both. It does this in a way where the "object types" may be placed in a "freezed black box" library (think of one which comes from a 3rd party), where you don't have control over their content or updates, but it still allows you to control the list of extending actions.

However, if the list of object types in such a system is extended "frequently" by you, putting those types into a "freezed library" makes no sense. Writing an individual method for each and every combination of object type and action is usually not desirable and not necessary. In reality, one will usually detect commonalities in those actions, and create abstractions to make the actions work orthogonally to those object types. This may require to change the object type design to some degree, but since it is under your own control, this is usually not much of a problem.

Sticking to your example:

  • Lets say you have a class hierarchy of NotificationSource child classes, as you described it. Now think of NotificationSender: instead of providing one Send method per child object in NotificationSender (like SendEmail, SendSms, SendTwitter), you would probably try to implement a generic NotificationSender.Send method, which takes an abstract object NotificationSource and sends it somehow. This leads to the "template method pattern": NotificationSource needs to provide some virtual methods which are used by NotificationSender to make it possible to implement Send it in a generic way.

  • Same holds for an action like NotificationSerializer: serializing functionality is usually available by standard libs and ORMs, which use meta information, annotations and reflection for allowing a generic implementation.

There may be other ways of achieving orthogonality in a system, and in some cases the having an individual implementation for an action for each of the child objects may be necessary. But this will depend on the specific case and I don't think it makes much sense to dicuss this with hypothetical, contrived examples.

So in short, when you have both sides - the list of object types and the list of actions - under your control, there is often no need to use the visitor pattern. Instead, you can redesign both sides to achieve more orthogonality and make the visitor pattern superfluous.

0

The original question sounds like there is a problem with in-memory objects changing frequently does not work well with the visitor pattern. If your visitor was implemented as a pure function and the system scaled properly with parallelization this could scale infinitely.

However I don't think this is what you are asking. It sounds more like you are asking object types that change frequently. Where you are asking how to build a robust, easy to maintain class system to handle this scaling.

I agree with @doc brown's answer. To add on that, defining the abstract NotificationSource/Sender is the focal point for which all subclasses must extend. If your subclasses use the super type that brings the benefits of polymorphism.

My suggestion is to normalize the payload for which you are notifying your visitors for. This means it does not matter for which Source your notification is coming from, every Source must abide by the structure and provide the normalized payload of data in order for your Senders (visitors) to properly handle that payload.

But perhaps a Source need to interact with a specific Sender counterpart -- it does not make sense to normalize a particular payload for a specific use case. In this case, registering your X-Sender to a Y-Source can help pair these 1:1 relationships. Of course, this design does not scale as well. But it should be an exceptional case.

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.