Say you have domain events and some of them may need to be sent to email, slack or the like.
As they share some functionality, you may be thinking of creating traits and have events implement them:
trait TEmailSender{
fn send(&self) ->Result<(),Error>;
}
// Events that have been declared and used in production
struct EventA;
struct EventB;
struct EventC;
But then, it poses 2 different problems. Fistly, altering existing code may introduce bugs - which nobody wants to take. Either that or, you simply don't have right to change the existing code but just use them.
Secondly, sending notification-like functionality to Events itself seem to violate SRP.
How do you implement new features without tinkering with the existing logics? - visitor comes to a rescue.
In our example, we want to send the notification to both email and slack. For that, we'll create the trait that could handle existing structs:
// Visitor trait
trait TSender {
fn send_event_a(&self, e: &EventA);
fn send_event_b(&self, o: &EventB);
fn send_event_c(&self, o: &EventC);
}
// Element that events will implement
trait TNotification {
fn accept(&self, sender: &dyn TSender);
}
// allow visitor to be passed to each events
impl TNotification for EventA {
fn accept(&self, sender: &dyn TSender) {
sender.send_event_a(self);
}
}
impl TNotification for EventB {
fn accept(&self, sender: &dyn TSender) {
sender.send_event_b(self);
}
}
impl TNotification for EventC {
fn accept(&self, sender: &dyn TSender) {
sender.send_event_c(self);
}
Now, for email and slack will implement our visitor trait, TSender
struct EmailSender;
impl TSender for EmailSender {
fn send_event_a(&self, e: &EventA) {
println!("Sending email for EventA");
}
fn send_event_b(&self, e: &EventB) {
println!("Sending email for EventB");
}
fn send_event_c(&self, e: &EventC) {
println!("Sending email for EventC");
}
}
struct SlackSender;
impl TSender for SlackSender {
fn send_event_a(&self, e: &EventA) {
println!("Sending slack message for EventA");
}
fn send_event_b(&self, e: &EventB) {
println!("Sending slack message for EventB");
}
fn send_event_c(&self, e: &EventC) {
println!("Sending slack message for EventC");
}
}
At this point, you can see that I did not change any of the existing code related to events. Let's see how it works in action
fn main() {
// bot them in the vector as collection
let events: Vec<Box<dyn TNotification>> =
vec![Box::new(EventA), Box::new(EventB), Box::new(EventC)];
// we have both visitors
let email_visitor = EmailSender;
let slack_visitor = SlackSender;
// iterating over event, pass visitor
for event in events {
event.accept(&email_visitor);
event.accept(&slack_visitor);
}
}
In Rust, thanks to the characteristic of trait where you can easily attach and detach them to concrete implementations, it's very easy to implement visitor pattern without touching on the ounce of existing code.
You may find it unpleasant to see trait(or interface) having explicit dependencies on concrete implementation rather than on abstracts, saying "it violates Dependency inversion principle(DIP)
!"
And I know you SHOULD program to abstraction rather than implementation.
But, at the core of DIP is that your domain model, encompassing data and logics, can stand well to changes in other layers(adapter layers for example.)
In this regard, Yes, Visitor
pattern seems to violate DIP but when organized in the right place, it doesn't introduce the incessant changes in upstream requirement.