Use Composite to Solve Recursive Issues

A practice of using composite to resolve a recursive issue.

Overview

The task is deceptively simple: we receive an entry-point in a file system that can be either a single file or a directory.

If the entry is a file, we perform the required operation on that file.

If the entry is a directory, we first process the directory itself and then traverse its contents recursively, handling every nested file and sub-directory in the same way.

To keep the solution clean, extensible, and test-friendly I introduced two symmetrical composite hierarchies:

File-system components – File and Directory both implement the common FileSystemComponent interface. – Because they share an abstraction, the traversal code can treat a leaf (file) and a branch (directory) as the same concept and simply call accept() on either of them.

Handlers – Every unit of behaviour (printing, counting, uploading, …) implements the FileSystemHandler interface. – CompositeFileSystemHandler keeps a list of concrete handlers and delegates the call to each one, ensuring that “the right handler handles the right node”.

code

public interface FileSystemComponent {
    void accept(FileSystemHandler handler);
}

public interface FileSystemHandler {
    void handle(FileSystemComponent component);
}
@Data
class File implements FileSystemComponent {
    private String name;

    public File(String name) {
        this.name = name;
    }

    @Override
    public void accept(FileSystemHandler handler) {
        handler.handle(this);
    }
}

@Data
class Directory implements FileSystemComponent {
    private String name;
    private List<File> files = new ArrayList<>();

    public Directory(String name, List<File> components) {
        this.name = name;
        this.files = components;
    }

    @Override
    public void accept(FileSystemHandler handler) {
        handler.handle(this);
        for (FileSystemComponent component : files) {
            component.accept(handler);
        }
    }
}

class CompositeFileSystemComponent implements FileSystemComponent {
    private List<FileSystemComponent> components = new ArrayList<>();

    public CompositeFileSystemComponent(FileSystemComponent... fileSystemComponents) {
        this(List.of(fileSystemComponents));
    }

    CompositeFileSystemComponent(List<FileSystemComponent> components) {
        this.components = components;
    }

    @Override
    public void accept(FileSystemHandler handler) {
        for (FileSystemComponent component : components) {
            component.accept(handler);
        }
    }
}
class FileHandler implements FileSystemHandler {
    @Override
    public void handle(FileSystemComponent component) {
        if (component instanceof File) {
            File file = (File) component;
            System.out.println("Handling file: " + file.getName());
        }
    }
}

class DirectoryHandler implements FileSystemHandler {
    @Override
    public void handle(FileSystemComponent component) {
        if (component instanceof Directory) {
            Directory directory = (Directory) component;
            System.out.println("Handling directory: " + directory.getName());
        }
    }
}

class CompositeFileSystemHandler implements FileSystemHandler {
    private List<FileSystemHandler> handlers = new ArrayList<>();

    public CompositeFileSystemHandler(FileSystemHandler... fileSystemHandlers) {
        this(List.of(fileSystemHandlers));
    }

    CompositeFileSystemHandler(List<FileSystemHandler> handlers) {
        this.handlers = handlers;
    }

    @Override
    public void handle(FileSystemComponent component) {
        for (FileSystemHandler handler : handlers) {
            handler.handle(component);
        }
    }
}
public class CompositeHandlerDemo {
    public static void main(String[] args) {
        FileSystemComponent randomComponent = createRandomComponent();

        CompositeFileSystemHandler compositeHandler = new CompositeFileSystemHandler(new FileHandler(), new DirectoryHandler());
        randomComponent.accept(compositeHandler);
    }

    public static FileSystemComponent createRandomComponent() {
        return null;
    }
}

Analyze

Single Responsibility

good part: file, directory only do one thing bad part: fileHander and directoryHandler both handle sth and decide whether to handle. (InstanceOf check) thinking: a resolver/strategy like pattern can take the instanceOf check respnsibility.

Open for Extension, Closed for Modification

good part: Adding a new component (e.g. Symlink) or a new handler (e.g. SizeCalculator) requires no existing code changes. bad part: However, every handler is forced to contain explicit instanceof blocks.

Liskov Substitution

good part: File and Directory can safely be used wherever FileSystemComponent is expected; they never weaken pre-conditions or strengthen post-conditions. bad part: FileHandler has code smell

Interface Segregation

The instanceof block is a symptom of ISP violation. Generic should be used.

public interface FileSystemHandler<T extends FileSystemComponent> {
    void handle(T component);
}
class FileHandler implements FileSystemHandler<File> {
    @Override public void handle(File file) {
        System.out.println("file: " + file.getName());
    }
}

class CompositeHandler implements FileSystemHandler<FileSystemComponent> {
    private final Map<Class<?>, FileSystemHandler<?>> table = new HashMap<>();

    <T extends FileSystemComponent> void register(
            Class<T> type, FileSystemHandler<? super T> h) {
        table.put(type, h);
    }

    @Override @SuppressWarnings("unchecked")
    public void handle(FileSystemComponent c) {
        FileSystemHandler handler = table.get(c.getClass());
        if (handler != null) handler.handle(c);   // no instanceof cascade
    }
}

Dependency Inversion

ignore the system out.... everything is injected via constructor to assure it's fully unit-testable.

References