I meet Visitor pattern
My first encounter with Visitor pattern dated back 8 years ago when I was working on Resource Map and we needed to give our users the ability to update and retrieve data via text SMS through the platform.
Resource Map uses treetop, a powerful syntactic analyzer library, to define allowable SMS commands using Parsing expression grammar (PEG) which freed us from writing the parser code manually because treetop would automatically generate one for us based on the our grammar file. However, for that same reason we also could not and should not modified the generated code to plug our new functionalities for updating and retrieving data as it would hurt the maintainability of the module.
In a pairng session, I and Ary, a friend, a mentor and code wizard 🎩 who went on to create the amazing Crystal programming language, decided to refactor the new functionalities to Visitor pattern such that changing the commands grammar would no longer affect the logic for data update and retrieval.
Recently, I came across a toy problem of creating a simple message responder called “Bob bot” which could respond as follow:
- Bob bot answers ‘Sure.’ if you ask him a question.
- He answers ‘Woah, chill out!’ if you yell at him (ALL CAPS).
- He says ‘Fine. Be that way!’ if you address him without actually saying anything.
- He answers ‘Whatever.’ to anything else.
This is a simple evaluation problem BUT there is a constraint which is you cannot use if
, unless
or case
in your response code.
My mind whispered “use lo.....op
”. We can indeed replace the conditional expression if we can make conditions enumerable thus changing conditional to iteration. The conditions for the problem being if a message is a Question, a Yell, a Silence or Anything else. Let’s call them message types. When receiving a message we could iterate through all message types, check if the message is a valid message of that type and return a response accordingly.
But… why message type should be responsible for responding task? What if we need to add another bot say AliceBot
which responds differently from BobBot
for each type of message. Dang💥, this design doesn’t work!
Wait… what we need here is a way to separate the message types from the algorithms applied to them and allow both entities to evolve separately so that making changes in future in regard to adding new bot would be a breeze. Visitor pattern comes to a rescue.
Visitor lets you define a new operation without changing the classes of the elements on which it operates.
In fact, BobBot
itself should know how to respond to each type of message and it should not be the responsibility of message types. Let’s refactor to Visitor pattern.
Meet the Visitor!
Visitor class defines methods to be implemented by its sub-class (i.e. respond_to_question
, respond_to_yell
, respond_to_silence
, respond_to_anything
), an initializer, a default
instance helper and the respond
method that enumerates through all message types and let the first valid message type instance accept the visitor and return the result. If none of the message types are valid for processing it will raise RuntimeError
.
Each message type must respond to valid?
to check if a passed-in message is legit for the type and accept(visitor)
which delegate back to visitor
with proper method call to one of the methods to be implemented by the its sub-class as mentioned above. For example, if the message is a Question it should call visitor.respond_to_question
.
BobBot inherits Visitor class and overrides all the methods to be implemented defined in the Visitor class.
Alice Bot? The implementation of AliceBot
is as trivial as BobBot
.
Now, the message types: we define a Base
type to include common field and initializer. Our message types of interest i.e. Question
, Yell
,Silence
and Anything
should override valid?
and accept(visitor)
as needed.
Test it out:
bob = BobBot.default
bob.respond('question?') # "Sure."
bob.respond('HEY') # "Woah, chill out!"
bob.respond('') # "Fine. Be that way!"
bob.respond('anything') # "Whatever."
bob.respond(nil) # RuntimeError (Can't respond to: nil)alice = AliceBot.default
alice.respond('question?') # "Yeah, I'm listening."
alice.respond('HEY') # "🤐"
alice.respond('') # "I can't hear you."
alice.respond('anything') # "No problem."
alice.respond(nil) # RuntimeError (Can't respond to: nil)
👍 Visitor pattern, we crossed path once more. You reminded me of the good memory I had with the lads at InSTEDD iLab. I’m always grateful to know each and everyone of you at InSTEDD and proud to have you all be a part of the treasure in my memory.
I hope you enjoy the article and find good use of Visitor pattern in your own project. Please don’t hesitate to reach out if you have any question for me. Please 👏👏👏 and share if you like or find it useful.