Visitor pattern implementation in case of source code un-availability

前端 未结 6 1533
旧时难觅i
旧时难觅i 2021-01-04 21:48

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 structu

相关标签:
6条回答
  • 2021-01-04 22:10

    I think the best approach is the Option 1: Extend one more inheritance hierarchy on top of third party class and implement the visitor pattern with double dispatch.

    The problem is the number of additional classes you need, but this can be resolved with a dynamic wrapper decorator. The Wrapper Decorator is a way to add interface implementation, methods and properties to already existing obejcts: How to implement a wrapper decorator in Java?

    In this way you need your Visitor interface and put there the visit(L legacy) methods:

    public interface Visitor<L> {
        public void visit(L legacy);
    }
    

    In the AcceptInterceptor you can put the code for the accept method

    public class AcceptInterceptor {
    
        @RuntimeType
        public static Object intercept(@This WrappedAcceptor proxy, @Argument(0) Visitor visitor) throws Exception {
            visitor.visit(proxy);
        }
    }
    

    The WrappedAcceptor interface defines the method to accept a visitor and to set and retrieve the wrapped object

    interface WrappedAcceptor<V> {
       Object getWrapped();
       void setWrapped(Object wrapped);
       void accept(V visitor); 
    }
    

    And finally the utility code to create the Wrapper around any obect:

    Class<? extends Object> proxyType = new ByteBuddy()
     .subclass(legacyObject.getClass(), ConstructorStrategy.Default.IMITATE_SUPER_TYPE_PUBLIC)
     .method(anyOf(WrappedAcceptor.class.getMethods())).intercept(MethodDelegation.to(AcceptInterceptor.class))
     .defineField("wrapped", Object.class, Visibility.PRIVATE)
     .implement(WrappedAcceptor.class).intercept(FieldAccessor.ofBeanProperty())
     .make()
     .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
     .getLoaded();
    WrappedAcceptor wrapper = (WrappedAcceptor) proxyType.newInstance();
    wrapper.setWrapped(legacyObject);
    
    0 讨论(0)
  • 2021-01-04 22:11

    If your library does not has accept methods you need to do it with instanceof. (Normally you do twice single-dispatching in Java to emulate double dispatching; but here we use instanceof to emulate double dispatching).

    Here is the example:

    interface Library {
      public void get1();
      public void get2();
    }
    
    public class Library1 implements Library {
      public void get1() { ... }
      public void get2() { ... }
    }
    
    public class Library2 implements Library {
      public void get1() { ... }
      public void get2() { ... }
    }
    
    interface Visitor {
       default void visit(Library1 l1) {}
       default void visit(Library2 l2) {}
    
       default void visit(Library l) {
          // add here instanceof for double dispatching
          if (l instanceof Library1) {
              visit((Library1) l);
          }
          else if (l instanceof Library2) {
              visit((Library2) l);
          }
       }
    }
    
    // add extra print methods to the library
    public class PrinterVisitor implements Visitor {
       void visit(Library1 l1) {
           System.out.println("I am library1");
       } 
       void visit(Library2 l2) {
           System.out.println("I am library2");
       }        
    }
    

    and now in any method you can write:

    Library l = new Library1();
    PrinterVisitor pv = new PrinterVisitor();
    pv.visit(l);
    

    and it will print to you "I am library1";

    0 讨论(0)
  • 2021-01-04 22:20

    First I had to made a few assumptions about the legacy code, since you didn't provide much details about it. Let's say I need to add a new method to Legacy without reimplementing everything. This is how I'll do it:

    public interface LegacyInterface {
        void A();
    }
    
    public final class LegacyClass implements LegacyInterface {
        @Override
        public void A() {
            System.out.println("Hello from A");
        }
    }
    

    First extends the "contract"

    public interface MyInterface extends LegacyInterface {
        void B();
    }
    

    And implement it in a "decorated" way

    public final class MyClass implements MyInterface {
        private final LegacyInterface origin;
    
        public MyClass(LegacyInterface origin) {
            this.origin = origin;
        }
    
        @Override
        public void A() {
            origin.A();
        }
    
        @Override
        public void B() {
            System.out.println("Hello from B");
        }
    }
    

    The key point is MyInterface extends LegacyInterface: this is the guarantee the implementations will benefit from both the services from the legacy code and your personnal addings.

    Usage

    MyInterface b = new MyClass(new LegacyClass());
    
    0 讨论(0)
  • 2021-01-04 22:22

    There should be a possibility to add new functionality to the classes of some hierarchy, without changing the base class interface. Kinds of possible behavior should be constant, while operations for different classes should execute differently.

    The Visitor Pattern allows to concentrate all that operations in one class. There might be a lot of Concrete Element classes (from the diagram), but for each of them there will be implemented visit() method in Concrete Visitor class that will define his own algorithm.

    Definition and implementation of method for each subclass of Element class:

    public interface Visitor {
        void visit(Element element);
    }
    
    public class ConcreteVisitor implements Visitor {
        public void visit(Element element) {
            // implementation
        }
    }
    

    The Visitor Pattern is easily extended for new operations by implementing this interface by new class with his method implementation.

    The following structure encapsulates the Element class:

    public lass ObjectStructure {
        private Element element;
        // some methods
    }
    

    This ObjectStructure class could aggregate one or several instances of Element. Presentation that Visitor acts on:

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

    And implementation of accept() method in the concrete entity:

    public class ConcreteElement implements Element {
        public void accept(Visitor visitor) {
            visitor.visit();
        }
    }
    

    Using of Visitor Pattern allows to save Element hierarchy from huge logical functionality or complicated configuration.

    It is desirable to add the functionality to all the classes of hierarchy while defining a new Visitor subclasses. But there could be a problem: visit() should be overriden for every hierarchy type. To avoid this it's better to define AbstractVisitor class and all leave his all visit() method bodies empty.

    Conclusion: using this pattern is good when class hierarchy of type Element keeps constant. If new classes add, it usually goes to considerable changes in classes of Visitor type.

    0 讨论(0)
  • 2021-01-04 22:25

    My answer is very similar to Michael von Wenckstern's, with the improvements that we have a named accept method (more like the standard pattern) and that we handle unknown concrete classes -- there's no guarantee that at some point a concrete implementation we haven't seen before won't appear on the classpath. My visitor also allows a return value.

    I've also used a more verbose name for the visit methods -- including the type in the method name, but this isn't necessary, you can call them all visit.

    // these classes cannot be modified and do not have source available
    
    class Legacy {
    }
    
    class Legacy1 extends Legacy {
    }
    class Legacy2 extends Legacy {
    }
    
    // this is the implementation of your visitor    
    
    abstract class LegacyVisitor<T> {
        abstract T visitLegacy1(Legacy1 l);
        abstract T visitLegacy2(Legacy2 l);
    
        T accept(Legacy l) {
            if (l instanceof Legacy1) {
                return visitLegacy1((Legacy1)l);
            } else if (l instanceof Legacy2) {
                return visitLegacy2((Legacy2)l);
            } else {
                throw new RuntimeException("Unknown concrete Legacy subclass:" + l.getClass());
            }
        }
    }
    public class Test {
        public static void main(String[] args) {
            String s = new LegacyVisitor<String>() {
    
                @Override
                String visitLegacy1(Legacy1 l) {
                    return "It's a 1";
                }
    
                @Override
                String visitLegacy2(Legacy2 l) {
                    return "It's a 2";
                }
            }.accept(new Legacy1());
    
            System.out.println(s);
        }
    }
    
    0 讨论(0)
  • 2021-01-04 22:28

    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.
    0 讨论(0)
提交回复
热议问题