Featured image of post BP8 - Visitor - Learn Design Pattern From Simple Things

BP8 - Visitor - Learn Design Pattern From Simple Things

Have you had a chance to watch The Spy Gone North? The spy infiltrated North Korea's secret locations, and collected its secrets effectively by applying the Visitor Pattern!

Visitor is a behavioral design pattern: πŸ–€πŸ–€πŸ–€πŸ€πŸ€

What is Visitor?

Visitor is a technique that allows you to separate an algorithm from the objects on which it operates. It enables you to define a new operation without modifying the classes of the objects that you work with.

Suppose you have a collection of locations in North Korea, such as nuclear facilities, military bases, etc. You want to collect their secrets without altering them because they are unchangeable. So the plan is to send a Spy who can visit each location and extract their secrets. The location will accept the Spy and reveal itself to him, and the Spy will remember the appropriate secret for that location. This way, you don’t have to interfere with (add new code to) the locations every time you want to collect different secrets. You just need to send a Spy that knows how to collect that secret. This makes the plan simpler and easier to maintain.

Visitor diagram

Why use Visitor?

Visitor lets you add new behaviors to existing classes without modifying them. You can create a visitor object that implements a certain operation and pass it to other objects that accept this visitor. The objects then call the visitor’s method that corresponds to their own class and execute the operation.

Advantages:

  • Open/Closed Principle. You can introduce a new behavior that can work with objects of different classes without changing these classes.
  • Single Responsibility Principle. You can move multiple versions of the same behavior into the same class.
  • A visitor object can accumulate some useful information while working with various objects. This might be handy when you want to traverse some complex object structure, such as an object tree, and apply the visitor to each object in this structure.

Disadvantages:

  • You need to update all visitors each time a class gets added to or removed from the element hierarchy.
  • Visitors might lack the access to some private fields and methods of the elements that they are supposed to work with.

Question: I hate the Visitor, what are the alternatives?

Answer: A simpler solution is to just add the operations directly into the classes of the objects that need them. However, this might violate the Single Responsibility Principle and make the classes too large and complex.

Visitor

When to use Visitor?

Question: When should I use Visitor?

Answer: You should use Visitor when you need to perform an operation on a set of objects with different classes, and you don’t want to change their classes.

Input:

You are a spy who wants to infiltrate North Korea’s secret locations and collect their secrets. You have access to two locations: a nuclear facility and a military base. Each location has a secret attribute that you want to get.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from abc import abstractmethod


class NorthKorea:
    def __init__(self):
        self.secret = None

    def accept(self, visitor):
        visitor.visit(self)


class NuclearFacility(NorthKorea):
    def __init__(self):
        self.secret = "North Korea tests nuclear drones and builds uranium facility."


class MilitaryBase(NorthKorea):
    def __init__(self):
        self.secret = "North Korea hides 16 missile bases and builds new one."

Expected Output:

You want to create a spy object that can visit any location and store its secret in a diary. After visiting all locations, you want to print your diary.

1
2
3
4
[
    'North Korea tests nuclear drones and builds uranium facility.',
    'North Korea hides 16 missile bases and builds new one.'
]

How to implement Visitor?

Non-Visitor implementation:

A simple way to implement this scenario is to add a get_secret method to each location class and call it from the spy object.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from abc import abstractmethod


class NorthKorea:
    def __init__(self):
        self.secret = None

    @abstractmethod
    def get_secret(self):
        pass


class NuclearFacility(NorthKorea):
    def __init__(self):
        self.secret = "North Korea tests nuclear drones and builds uranium facility."

    def get_secret(self):
        return self.secret


class MilitaryBase(NorthKorea):
    def __init__(self):
        self.secret = "North Korea hides 16 missile bases and builds new one."

    def get_secret(self):
        return self.secret


class Spy:
    def __init__(self):
        self.diary = []

    def visit(self, location):
        self.diary.append(location.get_secret())


# Example usage
if __name__ == '__main__':
    locations = [NuclearFacility(), MilitaryBase()]
    spy = Spy()
    for location in locations:
        spy.visit(location)
    print(spy.diary)

Visitor implementation:

A better way to implement this scenario is to use the Visitor design pattern. We can create a Spy class that implements a visit method for each location class. The visit method will access the secret attribute of the location and store it in the diary. Then, we can make each location class accept a visitor object and call its visit method with itself as an argument.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from abc import abstractmethod


class NorthKorea:
    def __init__(self):
        self.secret = None

    @abstractmethod
    def accept(self, visitor):
        pass


class NuclearFacility(NorthKorea):
    def __init__(self):
        self.secret = "North Korea tests nuclear drones and builds uranium facility."

    def accept(self, visitor):
        visitor.visit(self)


class MilitaryBase(NorthKorea):
    def __init__(self):
        self.secret = "North Korea hides 16 missile bases and builds new one."

    def accept(self, visitor):
        visitor.visit(self)


class Spy:
    def __init__(self):
        self.diary = []

    def visit(self, location):
        if isinstance(location, NuclearFacility):
            self.diary.append(location.secret)
        elif isinstance(location, MilitaryBase):
            self.diary.append(location.secret)


# Example usage
if __name__ == '__main__':
    locations = [NuclearFacility(), MilitaryBase()]
    spy = Spy()
    for location in locations:
        location.accept(spy)
    print(spy.diary)

This way, we can separate the logic of collecting secrets from the classes of the locations. We can also add new types of locations or visitors without changing the existing classes.

This code reflects the Visitor design pattern because it follows these steps:

  • The NorthKorea class declares an accept method that takes a visitor object as an argument.
  • The NuclearFacility and MilitaryBase classes inherit from the NorthKorea class and override the accept method to call the visitor’s visit method with themselves as an argument.
  • The Spy class implements a visit method that performs an operation on each type of location object.
  • The main function creates a spy object and passes it to each location object’s accept method.

Visitor is a useful design pattern when you want to add new behaviors to existing classes without modifying them. It allows you to separate an algorithm from the objects on which it operates and apply it to different types of objects. However, it also has some drawbacks, such as requiring you to update all visitors when you change the element hierarchy or limiting the access to some private fields and methods of the elements.

Source Code

Made with the laziness πŸ¦₯
by a busy guy