Search code examples
javadesign-patternsvisitor-pattern

Visitor pattern implementation in case of source code un-availability


One of the reasons to consider the Visitor_pattern:

A practical result of this separation is the ability to add new operations to existing object structures without modifying those structures.

Assume that you don't have the source code of third party libraries and you have added one operation on related objects.

Since you don't have object, your elements (Third party classes) can't be modified to add Visitor.

In this case, double dispatch is not possible.

So which option is generally preferred?

Option 1: Extend one more inheritance hierarchy on top of third party class and implement pattern as show in picture with double dispatch?

For a given hierarchy of Class B which extends Class A, you can add

ElementA extends A
ElementB extends B

Now ConcreteElements are derived from ElementA instead of class A.

Cons: The number of classes will grow.

Option 2: Use Visitor class a central helper class and get the work done with single dispatch.

Cons: We are not really following Visitor patter as per UML diagram.

Correct if I am wrong.


Solution

  • You could combine a Wrapper and Visitor to solve your problems. Using the wrapper to add a visit method allows you to increase the usability of these objects. Of course you get the full advantages (less dependency on the legacy classes) and disadvantages (additional objects) of a wrapper.


    Here's a worked-up example in JAVA (because it is pretty strict, does not do double-dispatch by itself, and I'm quite familiar with it):

    1) Your legacy Objects

    Assuming you have your legacy objects Legacy1 and Legacy2which you cannot change, which have specific business methods:

    public final class Legacy1 {
        public void someBusinessMethod1(){
            ...
        }
    }
    

    and

    public final class Legacy2 {
        public void anotherBusinessMethod(){
            ...
        }
    }
    

    2) Prepare the Wrapper

    You just wrap them in a VisitableWrapper which has a visit method that takes your visitor, like:

    public interface VisitableWrapper {
        public void accept(Visitor visitor);
    }
    

    With the following implementations:

    public class Legacy1Wrapper implements VisitableWrapper {
    
        private final Legacy1 legacyObj;
    
        public Legacy1Wrapper(Legacy1 original){
            this.legacyObj = original;
        }
    
        public void accept(Visitor visitor){
             visitor.visit(legacyObj);
        }
    }
    

    and

    public class Legacy2Wrapper implements VisitableWrapper {
    
        private final Legacy2 legacyObj;
    
        public Legacy2Wrapper(Legacy2 original){
            this.legacyObj = original;
        }
    
        public void accept(Visitor visitor){
             visitor.visit(legacyObj);
        }
    }
    

    3) Visitor, at the ready!

    Then your own Visitors can be set to visit the wrapper like so:

    public interface Visitor {
         public void visit(Legacy1 leg);
         public void visit(Legacy2 leg);
    }
    

    With an implementation like so:

    public class SomeLegacyVisitor{
    
        public void visit(Legacy1 leg){
            System.out.println("This is a Legacy1! let's do something with it!");
            leg.someBusinessMethod1();
        }
    
        public void visit(Legacy2 leg){
            System.out.println("Hum, this is a Legacy 2 object. Well, let's do something else.");
            leg.anotherBusinessMethod();
        }
    }
    

    4) Unleash the power

    Finally in your code, this framework would work like this:

    public class TestClass{
        // Start off with some legacy objects
        Legacy1 leg1 = ...
        Legacy2 leg2 = ...
    
        // Wrap all your legacy objects into a List:
        List<VisitableWrapper> visitableLegacys = new ArrayList<>();
        visitableLegacys.add(new Legacy1Wrapper(legacy1));
        visitableLegacys.add(new Legacy2Wrapper(legacy2));
    
        // Use any of your visitor implementations!
        Visitor visitor = new SomeLegacyVisitor();
        for(VisitableWrapper wrappedLegacy: visitableLegacys){
            wrappedLegacy.accept(visitor);
        }
    }
    

    The expected output:

    This is a Legacy1! let's do something with it!
    Hum, this is a Legacy 2 object. Well, let's do something else.
    

    Drawbacks:

    1. Quite a lot of boilerplate. Use Lombok if you develop in Java.
    2. Quite a lot of wrapper objects instances. May or may not be a problem for you.
    3. You need to know the specific type of the objects beforehand. This implies you know their subtype, they aren't bundles in a List. If that's the case, you have no other option but to use reflection.