Visitor Pattern in Rust

Migo·2024년 6월 10일
0

Fluent Rust

목록 보기
18/23
post-thumbnail

Problem

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.



Solution

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);
    }
}

Conclusion

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.



Relation to DIP

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.

profile
Dude with existential crisis

0개의 댓글