Mastering the Underdogs: A Guide to Visitor and Decorator Patterns in Software Engineering

Olivier Gamache
6 min readJan 17, 2023

How to decorate and visit with the help of ChatGPT

Photo by Tekton on Unsplash

Design patterns are an essential tool for software engineers, providing a set of reusable solutions to common software design problems. Two design patterns that are often overlooked are the Decorator pattern and the Visitor pattern. These patterns may not be as well-known as others like the Singleton or the Factory pattern, but they can be just as powerful.

In this article, we will explore both the Decorator and Visitor patterns, including their structure and the key elements that make them up. We will also discuss the use cases where these patterns are particularly useful and their implementation in code.

We will start by discussing the Decorator pattern which allows developers to add new behaviour to an individual object, without affecting other objects from the same class. This pattern is particularly useful when you need to add new functionality to an existing class and you want to avoid using inheritance.

Then we will explore the Visitor pattern which allows developers to separate an algorithm from an object structure on which it operates. This pattern is particularly useful when you have a complex object structure and you need to perform many different operations on it.

We will then compare and contrast the two patterns, highlighting their similarities and differences. We will also discuss when it is appropriate to use one pattern over the other, and how they can be used together to create more powerful and flexible software systems.

Finally, we will discuss how both Decorator and Visitor pattern are considered as underdogs in software engineering and the reasons why they are not as popular as other patterns, but why they should be considered as powerful tools in a software engineer’s toolbox.

Decorations

The Decorator design pattern is a structural pattern that allows developers to add new behaviour to an individual object, without affecting other objects from the same class. It does this by wrapping the original object in a new object that provides the additional behaviour.

Here is an example in rust:

The Decorator pattern is used in this snippet by allowing the addition of new behaviour to the Coffee struct without modifying its implementation. In this case, the FancyCoffee struct is a decorator for Coffee. The FancyCoffee struct wraps an instance of the Coffee struct and adds new behavior to it by implementing the Beverage trait.

The FancyCoffee struct takes an instance of Coffee in its constructor and stores it in a coffee field. The FancyCoffee struct also implements the Beverage trait by providing an implementation of the drink method. In this implementation, it first prints "Add milk!!!", and then calls the drink method on the coffee field. This allows the FancyCoffee struct to add new behaviour to the Coffee struct without modifying its implementation.

In the main function, an instance of Coffee is created and then passed to the constructor of FancyCoffee to create an instance of FancyCoffee. When the drink method is called on the FancyCoffee instance, it first prints "Add milk!!!", and then calls the drink method on the wrapped Coffee instance, allowing it to add the new behavior to the original Coffee struct without modifying it.

In this example, the Decorator pattern allows to add new functionality to an existing class without modifying its implementation, making it more flexible and easier to maintain. It’s also easy to add more functionality by adding more decorators.

Photo by Nathan Dumlao on Unsplash

Guests

The Visitor design pattern is a way to separate an algorithm from an object structure on which it operates. It allows you to add new operations to existing objects without modifying the objects themselves. The pattern is particularly useful when you have a complex object structure, and you need to perform many different operations on it.

The pattern has two main components: the Visitor trait, which defines a set of methods for visiting different types of objects, and the Visitable trait, which is implemented by the objects that can be visited.

In a typical implementation, the Visitor trait defines one method for each type of object that can be visited. These methods take the visited object as an argument and perform some operation on it. The Visitable trait defines an accept method, which is called to pass the visitor to the object. This method takes the visitor as an argument and calls the appropriate method on the visitor.

In this way, the Visitor pattern allows you to add new operations to an object hierarchy without modifying the classes of the objects themselves. It provides a way for developers to separate the operations that are performed on an object from the object itself, making it easier to add new operations and maintain existing ones.

In terms of a coffee shop example, imagine you want to be able to calculate the total revenue of the shop, but you also want to be able to calculate the revenue of the shop by type of drink. With the Visitor pattern, you can create two different Visitors, one that calculates the total revenue and another that groups the revenue by type of drink. You can then pass these Visitors to the different types of Beverages (coffee, latte, moka, etc) and they will return the desired information without modifying the Beverages implementation.

In this example, we have defined a coffee shop that sells Coffee, Latte and Moka. Each of these structs implements the Beverage trait, which has an accept method that takes a Visitor as an argument and calls the appropriate method on it.

We have implemented the Visitor trait with a RevenueVisitor struct that has two fields, total_revenue and revenue_by_type, which is a HashMap where the key is the name of the drink and the value is the revenue of that drink.

The visit_coffee and visit_latte methods of the RevenueVisitor struct take a coffee or latte struct as an argument and updates the total_revenue and revenue_by_type fields accordingly. The get_total_revenue and get_revenue_by_type methods are used to retrieve the calculated values.

In the main function, we create instances of coffee and latte and pass them to the accept method of the RevenueVisitor struct. This causes the visit_coffee and visit_latte methods to be called and updates the total_revenue and revenue_by_type fields accordingly. Finally, the get_total_revenue and get_revenue_by_type methods are used to retrieve the calculated values, which are printed to the console.

In this way, the Visitor pattern allows us to separate the calculation of the revenue from the Beverage structs and allows us to add new methods to calculate revenue without modifying the existing Beverage structs. This makes the code more flexible and easier to maintain. Additionally, it allows for the easy addition of new types of beverages (such as Moka) without having to modify the existing code for calculating revenue.

The Visitor pattern allows you to separate the operation that needs to be performed on an object from the object itself. This enables you to add new operations to existing objects without modifying their classes, it also allows you to perform operations on a set of objects that belongs to different classes, since the visitor is the one that knows how to perform the operation on each class.

Photo by Scott Graham on Unsplash

Powerful dogs

Both the Decorator and Visitor patterns are powerful tools in a software engineer’s toolbox, but they are often overlooked in favour of more commonly used patterns like Singleton and Factory. They are powerful tools that can make the design of software systems more flexible and maintainable. They are particularly useful when you need to add new functionality to an existing system and you want to avoid modifying existing code. They also allow for a separation of concerns, making it easier to add new features and maintain existing ones. However, they can be difficult to understand, especially for new or inexperienced developers, which may be the reason why they are not as widely used as other patterns.

In conclusion, while they may not be as well-known or widely used as other patterns, they are still useful tools that should be considered when designing software systems. With the appropriate use-cases, they can make the development process more efficient and the final product more robust.

Photo by Alvan Nee on Unsplash

--

--