In this article, we will delve into the world of the Visitor Design Pattern. This pattern is a powerful tool for enhancing the functionality of existing software systems without extensively modifying their components. We’ll explore the principles behind the Visitor Design Pattern, its applications, and we’ll implement a practical example to illustrate its usage.
Table of contents
Open Table of contents
Sections
Unveiling the Visitor Design Pattern
The Visitor Design Pattern revolves around the idea that when dealing with a large and established software system comprised of multiple components, it’s often better to add new functionalities without altering the existing components. This approach keeps the existing code intact and reduces the risk of introducing unexpected issues.
To illustrate this concept, let’s take a look at a simple diagram:
Key Components of the Visitor Design Pattern
In this diagram, we have a system comprising four components, which are well-established and serve different purposes. We wish to add a new component responsible for generating reports based on these four components. Our goal is to avoid extensive modifications to the existing components. We want to externalize this new functionality.
The solution is to create a separate component for report generation. This component will access the existing functionality rather than becoming a part of the established components. By doing this, we maintain a separation between the algorithm, encapsulated in the report generator component, and the functionality present in the four components.
In the context of the Visitor Design Pattern, we identify two key concepts:
Visitor:
This is the new component that contains the functionality to be added. In our case, it’s the report generator.
Element (or Visitable):
These are the existing components that should accept the visitor’s operations. We must modify these elements to enable them to accept visitor calls. This separation allows us to add new functionalities without extensive modifications to the existing components. It’s a cleaner and more maintainable way to enhance a system.
Practical Implementation: Generating Reports
Let’s bring this concept to life by implementing a small program. We’ll create a report generator responsible for extracting information from existing contracts and generating monthly and yearly reports. The system will manage different types of contracts, each with specific attributes.
Here’s an outline of our program:
ReportElement
public interface ReportElement {
<R> R accept(ReportVisitor<R> visitor);
}
ReportVisitor
public interface ReportVisitor<R> {
public R visit(FixedPriceContract contract);
public R visit(TimeAndMaterialContract contract);
public R visit(SupportContract contract);
}
The we need to create the Element Classes (Visitables)
FixedPriceContract
public class FixedPriceContract implements ReportElement {
long costPerYear;
public FixedPriceContract(long costPerYear) {
this.costPerYear = costPerYear;
}
@Override
public <R> R accept(ReportVisitor<R> visitor) {
return visitor.visit(this);
}
}
TimeAndMaterialContract
public class TimeAndMaterialContract implements ReportElement {
long costPerHour;
long hours;
public TimeAndMaterialContract(long costPerHour, long hours) {
this.costPerHour = costPerHour;
this.hours = hours;
}
@Override
public <R> R accept(ReportVisitor<R> visitor) {
return visitor.visit(this);
}
}
Support Contract
public class SupportContract implements ReportElement{
long costPerMonth;
public SupportContract(long costPerMonth) {
this.costPerMonth = costPerMonth;
}
@Override
public <R> R accept(ReportVisitor<R> visitor) {
return visitor.visit(this);
}
}
Then we need to create the visitors classes:
MonthlyCostReportVisitor
public class MonthlyCostReportVisitor implements ReportVisitor<Long>{
@Override
public Long visit(FixedPriceContract contract) {
return contract.costPerYear/12;
}
@Override
public Long visit(TimeAndMaterialContract contract) {
return contract.costPerHour*contract.hours;
}
@Override
public Long visit(SupportContract contract) {
return contract.costPerMonth;
}
}
YearlyCostReportVisitor
public class YearlyCostReportVisitor implements ReportVisitor<Long>{
@Override
public Long visit(FixedPriceContract contract) {
return contract.costPerYear;
}
@Override
public Long visit(TimeAndMaterialContract contract) {
return contract.costPerHour* contract.hours;
}
@Override
public Long visit(SupportContract contract) {
return contract.costPerMonth*12;
}
}
Client Code
public class Client {
public static void main(String[] args) {
FixedPriceContract fixedPriceContract = new FixedPriceContract(10000);
SupportContract supportContract = new SupportContract(12344);
TimeAndMaterialContract timeAndMaterialContract = new TimeAndMaterialContract(12, 12178);
TimeAndMaterialContract timeAndMaterialContractNicer = new TimeAndMaterialContract(118, 12178);
List<ReportElement> projects = List.of(fixedPriceContract, supportContract, timeAndMaterialContract, timeAndMaterialContractNicer);
MonthlyCostReportVisitor monthlyCostReportVisitor = new MonthlyCostReportVisitor();
YearlyCostReportVisitor yearlyCostReportVisitor = new YearlyCostReportVisitor();
long monthlyCost = 0;
long yearlyCost = 0;
for (ReportElement project : projects) {
monthlyCost += project.accept(monthlyCostReportVisitor);
yearlyCost += project.accept(yearlyCostReportVisitor);
}
System.out.println("Monthly cost is this: " + monthlyCost);
System.out.println("Yearly cost is: " + yearlyCost);
}
}
In our program, we define three types of contracts: FixedPriceContract, TimeAndMaterialsContract, and SupportContract. These contracts represent our established code, and we don’t want to modify them extensively.
We introduce two visitor components: MonthlyCostReportVisitor and YearlyCostReportVisitor. These visitors access the contract elements and generate monthly and yearly cost reports, respectively.
We then create instances of these contracts, add them to a list, and implement the report generation. The result is the sum of monthly and yearly costs for all the contracts.
This simple program showcases how the Visitor Design Pattern allows us to add new functionalities while keeping existing code untouched.
Benefits of the Visitor Design Pattern
The Visitor Design Pattern offers several benefits:
Clean Code:
It maintains a clear separation between existing code and new functionality.
Extensibility:
You can add new operations (visitors) without altering the visited elements.
Reusability:
Visitors can be reused in different contexts, making your code more modular.
Testing:
It simplifies unit testing since you can focus on one aspect at a time.
Conclusion
The Visitor Design Pattern is a valuable approach when you need to extend the functionality of a complex software system without disturbing its existing components. By separating the concerns of your system, you maintain code integrity, readability, and extensibility. The example we’ve provided demonstrates the essence of this pattern and its practical application in real-world scenarios.
This article serves as a comprehensive introduction to the Visitor Design Pattern. It’s a versatile pattern that offers a structured way to enhance software systems, making them more maintainable and extensible. Happy coding! 🚀