Sometimes we need to implement different methods for a POJO with different business requirements. We could certainly create several methods inside the POJO but we would not be utilizing the main benefits of powerful code – cohesion and low coupling. When we want to encapsulate different business requirements inside a cohesive class we can use the Visitor Pattern!
Basically, when using Visitor Pattern we encapsulate the business requirements inside the Visitor classes making our code very flexible.
Get the Design_Patterns_Saga_GitHub_Source_Code
Let’s see a bad example of code if we aren’t using the Visitor Pattern.
1 – Generic Interface: It’s basically the interface that declares the main method to calculate shipping.
public interface CardDevice { public double calculateShipping(); }
2 – Business POJOs: Now we have all the POJOs implementing the Generic interface in order to define the behavior to calculate shipping.
public class Mobile implements CardDevice { @Override public double calculateShipping() { return 6; } } public class WiredPos implements CardDevice { @Override public double calculateShipping() { return 9; } } public class WirelessPos implements CardDevice { @Override public double calculateShipping() { return 7; } }
3 – Invoker: Now we can see the problem. We have the code very coupled with the specific calculateShipping method.
public class CardDeviceOrder implements CardDevice { private List<CardDevice> cardDevices = new ArrayList<>(); public CardDeviceOrder() { super(); } public void addPart(CardDevice cardDevice) { cardDevices.add(cardDevice); } public List<CardDevice> getCardDevices() { return Collections.unmodifiableList(cardDevices); } public double calculateShipping() { double shippingCost = 0; for (CardDevice cardDevice : cardDevices) { shippingCost += cardDevice.calculateShipping(); } return shippingCost; } }
4 – Unit Test: Let’s see if the calculateShipping method is working:
public class BadExampleVisitorTest { public static final double EXPECTED_SHIPPING_COST = 22.0; @Test public void visitorBadExampleTest() { CardDeviceOrder order = new CardDeviceOrder(); order.addPart(new Mobile()); order.addPart(new WiredPos()); order.addPart(new WirelessPos()); double shippingCost = order.calculateShipping(); Assert.assertTrue(EXPECTED_SHIPPING_COST == shippingCost); } }
Now let’s see the same example using the Visitor Pattern. Now you will realize that we will have more flexibility in the code.
1 – Visitor interface: We are going to define every business requirement inside this Visitor in order to manipulate these rules in an uncoupled mode. We are going to implement the rules inside the Visitor implementation.
public interface CardDeviceVisitor { void visit(WirelessPos wirelessPos); void visit(Mobile mobile); void visit(WiredPos wiredPos); void visit(CardDeviceOrder cardDeviceOrder); }
2 – Visitor implementations: You will realize that the business requirements are implemented directly in the Visitor classes. They implement the Visitor generic interface and execute what is necessary for the business.
CardDeviceDisplayVisitor: Displays basic device information.
CardDeviceShippingVisitor: Calculates the shipping from each card device.
public class CardDeviceDisplayVisitor implements CardDeviceVisitor { @Override public void visit(WirelessPos wirelessPos) { System.out.println("We have a: " + wirelessPos); } @Override public void visit(Mobile mobile) { System.out.println("We have a: " + mobile); } @Override public void visit(WiredPos wiredPos) { System.out.println("We have: " + wiredPos); } @Override public void visit(CardDeviceOrder cardDeviceOrder) { System.out.println("We have an: "+ cardDeviceOrder); } } public class CardDeviceShippingVisitor implements CardDeviceVisitor { double shippingValue = 0; @Override public void visit(WirelessPos wirelessPos) { System.out.println("Calculating Wireless Pos shipping."); shippingValue += 15; } @Override public void visit(Mobile mobile) { System.out.println("Calculating Mobile shipping"); shippingValue += 3; } @Override public void visit(WiredPos wiredPos) { System.out.println("Calculating Wired Pos shipping."); shippingValue += 9; } @Override public void visit(CardDeviceOrder order) { System.out.println("If they bought more than 3 things " + "there will be a discount in the shipping."); List<CardDevice> cardDevices = order.getCardDevices(); if (cardDevices.size() > 3) { shippingValue -= 5; } System.out.println("Shipping amount is: " + shippingValue); } public double getShippingValue() { return shippingValue; } }
3 – Business POJOs: We can clearly see that the POJOs are already different. They are not invoking the method to execute the specific business requirements. Now we invoke a method from a generic visitor. It decouples the code and the business requirement is manipulated inside the Visitor. Check it out:
public class Mobile implements CardDevice { @Override public void accept(CardDeviceVisitor visitor) { visitor.visit(this); } } public class WiredPos implements CardDevice { @Override public void accept(CardDeviceVisitor visitor) { visitor.visit(this); } } public class WirelessPos implements CardDevice { @Override public void accept(CardDeviceVisitor visitor) { visitor.visit(this); } }
4 – Orchestrator, Invoker: The CardDeviceOrder class is responsible for orchestrating the cardDevices classes and invoking every method from the Visitors according to the specific Visitor class implementation.
public class CardDeviceOrder implements CardDevice { private List<CardDevice> cardDevices = new ArrayList<>(); public CardDeviceOrder() { super(); } public void addPart(CardDevice cardDevice) { cardDevices.add(cardDevice); } public List<CardDevice> getCardDevices() { return Collections.unmodifiableList(cardDevices); } @Override public void accept(CardDeviceVisitor visitor) { for (CardDevice cardDevice : cardDevices) { cardDevice.accept(visitor); } visitor.visit(this); } }
5 – Unit Tests: Let’s test our Visitors! You will see the business methods being invoked through the Visitor classes.
First, add all the devices to the list of CardDeviceOrder.
Second, instantiate both visitors and pass them into the accept method from CardDeviceOrder.
Third, confirm the calculated shipping value is correct.
public class GoodExampleVisitorTest { public static final double EXPECTED_SHIPPING_COST = 27.0; @Test public void visitorGoodExampleTest() { CardDeviceOrder order = new CardDeviceOrder(); order.addPart(new WirelessPos()); order.addPart(new Mobile()); order.addPart(new WiredPos()); CardDeviceShippingVisitor shipping = new CardDeviceShippingVisitor(); order.accept(shipping); order.accept(new CardDeviceDisplayVisitor()); Assert.assertTrue(EXPECTED_SHIPPING_COST == shipping.getShippingValue()); } }
Summary of actions:
- Created the Visitor generic interface.
- Implemented the Visitor classes with the necessary business requirements.
- Created the business POJOs delegating responsibilities to the Visitor classes.
- Created the orchestrator class responsible to manipulate the invocations.
To practice the Visitor Pattern you can create another Visitor class with another specific business requirement, for example calculating the rent from every card device. Create another test method to make sure it works. Be sure to use TDD (Test Driven Development).