diff --git a/README.md b/README.md index 5413290..176cae7 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ and divided into three groups. - [State](behavioral/state/README.md) - [Strategy](behavioral/strategy/README.md) - [Template](behavioral/template/README.md) +- [Visitor](behavioral/visitor/README.md) ## Resources diff --git a/behavioral/README.md b/behavioral/README.md index 7e9f412..1121c6c 100644 --- a/behavioral/README.md +++ b/behavioral/README.md @@ -13,3 +13,5 @@ Behavioral design patterns are concerned with algorithms and the assignment of r - [State](state/README.md) - [Strategy](strategy/README.md) - [Template](template/README.md) +- [Visitor](visitor/README.md) + diff --git a/behavioral/visitor/README.md b/behavioral/visitor/README.md new file mode 100644 index 0000000..ec5310b --- /dev/null +++ b/behavioral/visitor/README.md @@ -0,0 +1,49 @@ +# Visitor + +Visitor is a behavioral design pattern that allows adding new behaviors to existing class hierarchy without altering any existing code. + +## Problem + +Let an app which works with geographic information structured as one colossal graph. Each node of the graph may + represent a complex entity such as a city, but also more granular things like industries, sightseeing areas, _etc._. The nodes are connected with others if there’s a road between the real objects that they represent. Under the hood, each node type is represented by its own class, while each specific node is an object. + + At some point, you got a task to implement exporting the graph into XML format. At first, the job seemed pretty straightforward. You planned to add an export method to each node class and then leverage recursion to go over each node of the graph, executing the export method. The solution was simple and elegant: thanks to polymorphism, you weren't coupling the code which called the export method to concrete classes of nodes. + +Unfortunately, the system architect refused to allow you to alter existing node classes. He said that the code was already in production and he didnt want to risk breaking it because of a potential bug in your changes. + +Besides, he questioned whether it makes sense to have the XML export code within the node classes. The primary job of these classes was to work with geodata. The XML export behavior would look alien there. + +There was another reason for the refusal. It was highly likely that after this feature was implemented, someone from the marketing department would ask you to provide the ability to export into a different format, or request some other weird stuff. This would force you to change those precious and fragile classes again. + +## Solution + +The Visitor pattern suggests that you place the new behavior into a separate class called `visitor`, instead of trying to integrate it into existing classes. The original object that had to perform the behavior is now passed to one of the visitor’s methods as an argument, providing the method access to all necessary data contained within the object. + +Now, what if that behavior can be executed over objects of different classes? For example, in our case with XML + export, the actual implementation will probably be a little bit different across various node classes. Thus, the visitor class may define not one, but a set of methods, each of which could take arguments of different types. + + But how exactly would we call these methods, especially when dealing with the whole graph? These methods have different signatures, so we can’t use polymorphism. To pick a proper visitor method that’s able to process a given object, we’d need to check its class. Doesn't this sound like a nightmare? + +You might ask, why don’t we use method overloading? That’s when you give all methods the same name, even if they support different sets of parameters. Unfortunately, even assuming that our programming language supports it at all (as Java and C# do), it won’t help us. Since the exact class of a node object is unknown in advance, the overloading mechanism won’t be able to determine the correct method to execute. It’ll default to the method that takes an object of the base Node class. + +However, the Visitor pattern addresses this problem. It uses a technique called Double Dispatch, which helps to execute the proper method on an object without cumbersome conditionals. Instead of letting the client select a proper version of the method to call, how about we delegate this choice to objects we’re passing to the visitor as an argument? Since the objects know their own classes, they’ll be able to pick a proper method on the visitor less awkwardly. They “accept” a visitor and tell it what visiting method should be executed. + +I confess. We had to change the node classes after all. But at least the change is trivial and it lets us add further behaviors without altering the code once again. + +Now, if we extract a common interface for all visitors, all existing nodes can work with any visitor you introduce into the app. If you find yourself introducing a new behavior related to nodes, all you have to do is implement a new visitor class. + +## How to Implement + +1. Declare the visitor interface with a set of “visiting” methods, one per each concrete element class that exists in the program. + +1. Declare the element interface. If you’re working with an existing element class hierarchy, add the abstract “acceptance” method to the base class of the hierarchy. This method should accept a visitor object as an argument. + +1. Implement the acceptance methods in all concrete element classes. These methods must simply redirect the call to a visiting method on the incoming visitor object which matches the class of the current element. + +1. The element classes should only work with visitors via the visitor interface. Visitors, however, must be aware of all concrete element classes, referenced as parameter types of the visiting methods. + +1. For each behavior that can’t be implemented inside the element hierarchy, create a new concrete visitor class and implement all of the visiting methods. + + You might encounter a situation where the visitor will need access to some private members of the element class. In this case, you can either make these fields or methods public, violating the element’s encapsulation, or nest the visitor class in the element class. The latter is only possible if you’re lucky to work with a programming language that supports nested classes. + +1. The client must create visitor objects and pass them into elements via “acceptance” methods. \ No newline at end of file diff --git a/behavioral/visitor/__init__.py b/behavioral/visitor/__init__.py new file mode 100644 index 0000000..8edaf42 --- /dev/null +++ b/behavioral/visitor/__init__.py @@ -0,0 +1,3 @@ +""" +Lets you separate algorithms from the objects on which they operate. +""" diff --git a/behavioral/visitor/component.py b/behavioral/visitor/component.py new file mode 100644 index 0000000..4138ebb --- /dev/null +++ b/behavioral/visitor/component.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod + + +# from behavioral.visitor.visitor import Visitor + + +class Component(ABC): + """ + The Component interface declares an `accept` method that should take the + base visitor interface as an argument. + """ + + @abstractmethod + def accept(self, visitor: "Visitor") -> None: + pass + + +class ConcreteComponentA(Component): + """ + Each Concrete Component must implement the `accept` method in such a way + that it calls the visitor's method corresponding to the component's class. + """ + + def accept(self, visitor: "Visitor") -> None: + """ + Note that we're calling `visitConcreteComponentA`, which matches the + current class name. This way we let the visitor know the class of the + component it works with. + """ + + visitor.visit_concrete_component_a(self) + + def exclusive_method_of_concrete_component_a(self) -> str: + """ + Concrete Components may have special methods that don't exist in their + base class or interface. The Visitor is still able to use these methods + since it's aware of the component's concrete class. + """ + return "A" + + +class ConcreteComponentB(Component): + """ + Same here: visitConcreteComponentB => ConcreteComponentB + """ + + def accept(self, visitor: "Visitor") -> None: + visitor.visit_concrete_component_b(self) + + def special_method_of_concrete_component_b(self) -> str: + return "B" diff --git a/behavioral/visitor/main.py b/behavioral/visitor/main.py new file mode 100644 index 0000000..8fa1e9a --- /dev/null +++ b/behavioral/visitor/main.py @@ -0,0 +1,26 @@ +from typing import List + +from behavioral.visitor.component import Component, ConcreteComponentA, ConcreteComponentB +from behavioral.visitor.visitor import Visitor, ConcreteVisitor1, ConcreteVisitor2 + + +def client_code(components: List[Component], visitor: Visitor): + """ + The client code can run visitor operations over any set of elements without + figuring out their concrete classes. The accept operation directs a call to + the appropriate operation in the visitor object. + """ + + for component in components: + component.accept(visitor) + + +if __name__ == "__main__": + components = [ConcreteComponentA(), ConcreteComponentB()] + print("The client code works with all visitors via the base Visitor interface:") + visitor1 = ConcreteVisitor1() + client_code(components, visitor1) + + print("It allows the same client code to work with different types of visitors:") + visitor2 = ConcreteVisitor2() + client_code(components, visitor2) diff --git a/behavioral/visitor/visitor.py b/behavioral/visitor/visitor.py new file mode 100644 index 0000000..de0c36e --- /dev/null +++ b/behavioral/visitor/visitor.py @@ -0,0 +1,46 @@ +from abc import ABC, abstractmethod + +from behavioral.visitor.component import ConcreteComponentA, ConcreteComponentB + + +class Visitor(ABC): + """ + The Visitor Interface declares a set of visiting methods that correspond to + component classes. The signature of a visiting method allows the visitor to + identify the exact class of the component that it's dealing with. + """ + + @abstractmethod + def visit_concrete_component_a(self, element: ConcreteComponentA) -> None: + pass + + @abstractmethod + def visit_concrete_component_b(self, element: ConcreteComponentB) -> None: + pass + + +""" +Concrete Visitors implement several versions of the same algorithm, which can +work with all concrete component classes. + +You can experience the biggest benefit of the Visitor pattern when using it with +a complex object structure, such as a Composite tree. In this case, it might be +helpful to store some intermediate state of the algorithm while executing +visitor's methods over various objects of the structure. +""" + + +class ConcreteVisitor1(Visitor): + def visit_concrete_component_a(self, element: ConcreteComponentA) -> None: + print(f"{element.exclusive_method_of_concrete_component_a()} + ConcreteVisitor1") + + def visit_concrete_component_b(self, element: ConcreteComponentB) -> None: + print(f"{element.special_method_of_concrete_component_b()} + ConcreteVisitor1") + + +class ConcreteVisitor2(Visitor): + def visit_concrete_component_a(self, element: ConcreteComponentA) -> None: + print(f"{element.exclusive_method_of_concrete_component_a()} + ConcreteVisitor2") + + def visit_concrete_component_b(self, element: ConcreteComponentB) -> None: + print(f"{element.special_method_of_concrete_component_b()} + ConcreteVisitor2")