Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Visitor and Composite patterns to build a filtered stream

I am using a composite pattern with multiple leaf node classes which have specialist operations and a visitor pattern to allow those operations to be performed. In this example I've left out all the obvious accept methods for clarity.

interface Command {
    public int getCost();
}

class SimpleCommand implements Command {
    private int cost;

    public int getCost() {
        return cost;
    }
}

class MultiCommand implements Command {
    private Command subcommand;
    private int repeated;

    public int getCost() {
        return repeated * subcommand.getCost();
    }

    public void decrement() {
        if (repeated > 0)
            repeated--;
    }
}

class CommandList implements Command {
    private List<Command> commands;

    public int getCost() {
        return commands.stream().mapToInt(Command::getCost).sum();
    }

    public void add(Command command) {
        commands.add(command);
    }
}

interface CommandVisitor {
    default void visitSimpleCommand(SimpleCommandCommand command) { }
    default void visitMultiCommand(MultiCommand multiCommand) { }
    default void visitCommandList(CommandList commandList) { }
}

It's now possible to build visitors to perform operations such as decrement. However I find it easier to create a general purpose visitor that streams objects of a certain class so that any operation can be performed on them:

class MultiCommandCollector implements CommandVisitor {
    private final Stream.Builder<MultiCommand> streamBuilder = Stream.builder();

    public static Stream<MultiCommand> streamFor(Command command) {
        MultiCommandVisitor visitor = new MultiCommandVisitor();
        command.accept(visitor);
        return visitor.streamBuilder.build();
    }

    public void visitMultiCommand(MultiCommand multiCommand) {
        builder.accept(multiCommand);
    }
}

This is used as you would expect. For example:

MultiCommandCollector.streamFor(command).forEach(MultiCommand::decrement);

This has one significant limitation: it can't be used to alter the hierarchy as the stream is processed. For example, the following fails:

CommandListCollector.streamFor(commandList).forEach(cl -> cl.add(command));

I can't think of an alternative elegant design that would allow this.

My question is: is there a natural extension to this design to allow a general purpose visitor that can also alter the hierarchy? In other words, is there a way that the visitor can visit one member then refresh the hierarchy before visiting the next? Is this compatible with the use of streams?

like image 337
sprinter Avatar asked Nov 08 '22 17:11

sprinter


1 Answers

In my prior experience, Visitor pattern is useful to either query or to recreate the hierarchy. The querying part is obvious - you would simply listen for specific types of sub-objects and then build the query result as it fits. The other question, changing the hierarchy, is more difficult.

It may really get hard to change the hierarchy while iterating through it. Therefore, I know of two useful techniques that work fine in practice.

  1. While visiting the hierarchy, build the list of objects to change. Do not change them until the visiting is completed. Concrete visitor can build the list of objects of interest as its private member. Once it completes the visit, it would expose the list of objects as its result. Only then start iterating through the resulting list and make changes to the objects.
  2. While visiting the hierarchy, as you visit an element, create a copy of the element. If the element needs to be changed, then construct the changed version. Otherwise, if the elements needs not change, just return it as the new element. After entire visit is done, you will have the new hierarchy with all modifications made as intended. The old hierarchy could be dereferenced then, and garbage collector will collect those elements which have been replaced with new ones.

The first algorithm is applicable when elements are mutable. The second algorithm is applicable when elements are immutable.

Hope this helps.

like image 172
Zoran Horvat Avatar answered Nov 14 '22 22:11

Zoran Horvat