0

I'm working on a variation of the standard visitor pattern with the following three requirements:

  • For each node, all super classes of the node should be visited first
  • For each class, all implemented interfaces should be visited first
  • Each visit to a class or interface should be able to cancel the visit to subclasses/implementing classes

My question is twofold. 1) is this a known method or is there a similar pattern around with a different name? 2) are there any obvious improvements or problems with this approach?

I will here detail my approach with an abstract example.

The class hierarchy visited is a Collection of ConcreteItems. The ConcreteItem implements a CrossCuttingConcern and extends an AbstractItem class.

class Collection implements Visitable {
  public final List<ConcreteItem> concreteItems;

  public Collection(ConcreteItem ...concreteItems) {
    this.concreteItems = asList(concreteItems);
  }

  public Decent accept(Visitor visitor) {
    return visitor.visit(this);
  }
}

class AbstractItem implements Visitable {
  public Decent accept(Visitor visitor) {
    return visitor.visit(this);
  }
}

interface CrossCuttingConcern {
  default Decent acceptCrossCuttingConcern(Visitor visitor) {
    return visitor.visit(this);
  }
}

class ConcreteItem extends AbstractItem implements CrossCuttingConcern {
  public Decent accept(Visitor visitor) {
    // This will visit the abstract super type, interface, and concrete class in turn.
    // If any of those returns Decent.STOP, then the remaining ones are not visited.
    return Decent.allUntilStop(
        () -> super.accept(visitor),
        () -> this.acceptCrossCuttingConcern(visitor),
        () -> visitor.visit(this)
    );
  }
}

Now the Visitor and Visitable implementations are modified to return a type called Decent (yeah, maybe not the best name for it). A visit method returns STOP if it wants the visitor to stop descending down the class hierarchy. i.e. if you only want to visit AbstractItems you return Decent.STOP from visit(AbstractItem).

interface Visitor {
  Decent visit(Collection collection);
  Decent visit(AbstractItem abstractItem);
  Decent visit(CrossCuttingConcern interfaceItem);
  Decent visit(ConcreteItem concreteItem);
}

interface Visitable {
  Decent accept(Visitor visitor);
}

enum Decent {
  STOP,
  CONTINUE;

  public static Decent allUntilStop(Supplier<Decent> ...fns) {
    for (Supplier<Decent> fn : fns) {
      if (fn.get() == STOP) {
        return STOP;
      }
    }
    return CONTINUE;
  }

  public static BinaryOperator<Decent> product() {
    return (a, b) -> a == CONTINUE && b == CONTINUE ? CONTINUE : STOP;
  }
}

Now the default visitor adapter implementation returns Decent.CONTINUE and prints debugging information used in the example below.

class VisitorAdapter implements Visitor {

  @Override
  public Decent visit(Collection collection) {
    System.out.println("visiting Collection: " + collection);
    // iterate over all concrete items and return STOP if one of the visit(items) does so
    return collection.concreteItems.stream()
        .map(a -> a.accept(this))
        .reduce(Decent.product())
        .orElse(CONTINUE); // return CONTINUE if collection contains zero items
  }

  @Override
  public Decent visit(AbstractItem abstractItem) {
    System.out.println("visiting AbstractItem: " + abstractItem);
    return CONT;
  }

  @Override
  public Decent visit(CrossCuttingConcern interfaceItem) {
    System.out.println("visiting CrossCuttingConcern: " + interfaceItem);
    return CONT;
  }

  @Override
  public Decent visit(ConcreteItem concreteItem) {
    System.out.println("visiting ConcreteItem: " + concreteItem);
    return CONT;
  }
}

This example demonstrates the working requirements:

public static void main(String[] args) {
  Collection collection = new Collection(new ConcreteItem(), new ConcreteItem());

  System.out.println("Visit all");
  new VisitorAdapter().visit(collection);
  System.out.println("");

  System.out.println("Stop at AbstractItem")
  new VisitorAdapter() {
    public Decent visit(AbstractItem abstractItem) {
      super.visit(abstractItem);
      return STOP;
    }
  }.visit(collection);
  System.out.println("");

  System.out.println("Stop at CrossCuttingConcern");
  new VisitorAdapter() {
    public Decent visit(CrossCuttingConcern interfaceItem) {
      super.visit(interfaceItem);
      return STOP;
    }
  }.visit(collection);
  System.out.println("");
}

Providing the following output:

Visit all
visiting Collection: Collection@7f31245a
visiting AbstractItem: ConcreteItem@16b98e56
visiting CrossCuttingConcern: ConcreteItem@16b98e56
visiting ConcreteItem: ConcreteItem@16b98e56
visiting AbstractItem: ConcreteItem@7ef20235
visiting CrossCuttingConcern: ConcreteItem@7ef20235
visiting ConcreteItem: ConcreteItem@7ef20235

Stop at AbstractClass
visiting Collection: Collection@7f31245a
visiting AbstractItem: ConcreteItem@16b98e56
visiting AbstractItem: ConcreteItem@7ef20235

Stop at Interface
visiting Collection: Collection@7f31245a
visiting AbstractItem: ConcreteItem@16b98e56
visiting CrossCuttingConcern: ConcreteItem@16b98e56
visiting AbstractItem: ConcreteItem@7ef20235
visiting CrossCuttingConcern: ConcreteItem@7ef20235

So; does this look familiar, or is there a simpler way to achieve my requirements?

StanislavL
  • 56,971
  • 9
  • 68
  • 98
havardh
  • 43
  • 4
  • What is the problem you try to solve with this design? I don't think this could be called ``visitor`` pattern. – Gonzalo Jan 25 '18 at 12:17
  • I'm implement filters (`extends VisitorAdapter`) which looks at the entire `Collection` and builds a new `Collection`. And I am using the `visit` methods to determine which `ConcreteItems` to include in the new `Collection`. Now, some of my filters needs to look at `ConcreteItems` while others only needs to look at the `AbstractItem` or the `CrossCuttingConcern`. The above approach simplifies the implementation of the filters, but I am trying to assess if that simplification comes at a cost of not using a familiar pattern, as you are suggesting that this no longer is the `visitor` pattern. :) – havardh Jan 25 '18 at 12:27
  • 1
    First check whether your requirement complies with the intent of the visitor pattern. The intent of the visitor pattern is you have an element hierarchy. You want to to perform different operations on the elements. The element hierarchy should remain unchanged. If your usecase fits in with the intent you can consider visitor as a good candidate. Also notice that visitor is a rarely used pattern. – Ravindra Ranwala Jan 25 '18 at 12:30
  • Visitor visits objects rather than classes. Are you interested in the traversing aspect of the pattern? I don't see clearly the question. Can you state it in one sentence? – Fuhrmanator Jan 26 '18 at 12:27
  • Ah, that is a good point @Fuhrmanator, I'm actually most interested in the classes and the traversal of the class hierarchy, not the object hierarchy. Now, sorry if my question is unclear. I tried to sum it up in the last line, let me rephrase. Would you recognize the code above as being the visitor pattern, or am I trying too hard and there is a simpler approach I should look into to solve my problem? – havardh Jan 26 '18 at 14:05
  • It's still not very clear to me. Patterns are tools that you apply when you have the problem they solve (you start with the problem). Your question is like, "look at my tools (the code) and see if it's a hammer (the visitor pattern)". Visitor encapsulates functionality in Visitor classes so adding (new) functionality is easy, without changes to the visited objects. It doesn't seem like the **problem** you're trying to solve. So, yes, I think you're trying too hard. Forget visitor (visit/accept) and just treat it as a structure-traversing algorithm (with reflection?). – Fuhrmanator Jan 26 '18 at 15:31
  • Show how a filter works concretely (on paper) and then see if you can code it once (simply). I'm thinking you just need filters to work like strategies (algorithms). `Collection filter(Collection c){}` is a simple function that will do what your requirements say in the answer to @Gonzalo. If you want to have different filters, then make an interface for that method and make different implementations? Does it make sense? – Fuhrmanator Jan 26 '18 at 15:33
  • Thanks for the discussion @Fuhrmanator. We already had the interface and implementations of filters that you mentioned. But those tended to include both the filter logic and the infrastructure for building the `Collection` (our actual object structure is more complex than the example above). I've ended up moving the Class traversal down into the `VisitorAdapter` and using this as a delegate in my filters. This now lets my `Visitor` and `Visitable` use the standard pattern and lets me write filters which only contain the filtering logic. – havardh Jan 29 '18 at 06:47

0 Answers0