[JAVA] Refactor the Decorator pattern implementation with a functional interface

Overview

Let's refactor the implementation of the Decorator pattern using the functional interface and function synthesis introduced in Java 8.

Decorator pattern

The Decorator pattern allows you to dynamically add new features and behaviors to existing objects. Classes such as InputStream / OutputStream and Reader / Writer are often referred to in Java. For example, if you write the following code when reading a file

        InputStream is0 = Files.newInputStream(pathIn);
        InputStreamReader reader1 = new InputStreamReader(is0, csIn);
        BufferedReader reader = new BufferedReader(reader1);

        //This processing using reader ...

You can add a function to handle the contents of a file in byte units to InputStream in character units (InputStreamReader), and add a buffering function (BufferedReader).

In the above example, file data processing using reader continues, but pre-processing such as data shaping and correction may be required before data processing. This kind of pre-processing can also be implemented with the Decorator pattern. Here's an example implementation (the full text of PreprocedReaderOld.java can be found on GitHub).

PreprocedReaderOld.java


/**
 *Implementation example of pre-processing (Decorator pattern version)
 */
public class PreprocedReaderOld extends PipedReader {
    protected Reader in;

    public PreprocedReaderOld(Reader in) throws IOException {
        this.in = in;
        doPreproc();
    }

    protected void doPreproc() throws IOException {
        PipedWriter pipedOut = new PipedWriter();
        this.connect(pipedOut);

        BufferedReader reader = new BufferedReader(in);
        BufferedWriter writer = new BufferedWriter(pipedOut);
        try (reader; writer;) {
            String line;
            int lineNo = 1;
            while ((line = reader.readLine()) != null) {
                pipedOut.write(preprocess(lineNo, line));
                pipedOut.write('\n');
                lineNo++;
            }
        }
    }

    protected String preprocess(int lineNo, String line) {
        return "!Pre-processing! " + line;
    }

    @Override
    public void close() throws IOException {
        in.close();
    }
}

This pre-processing execution class, like other decorators, applies additional constructor calls as shown below.

        InputStream is0 = Files.newInputStream(pathIn);
        InputStreamReader reader1 = new InputStreamReader(is0, csIn);
        Reader reader2 = new PreprocedReaderOld(reader1); //Added pre-processing
        BufferedReader reader = new BufferedReader(reader2);

        //This processing using reader ...

If another pre-processing is needed, this example defines a new pre-processing execution class that inherits the pre-processing execution class PreprocedReaderOld and overrides thepreprocess (int lineNo, String line)method. We will be adding constructor calls.

        InputStream is0 = Files.newInputStream(pathIn);
        InputStreamReader reader1 = new InputStreamReader(is0, csIn);
        Reader reader2 = new PreprocedReaderOld(reader1); //Added pre-processing
        Reader reader3 = new PreprocedReaderOld2(reader2); //Pre-processing part 2 added
        Reader reader4 = new PreprocedReaderOld3(reader3); //Added pre-processing part 3
        BufferedReader reader = new BufferedReader(reader4);

        //This processing using reader ...

The above is an implementation example using the Decorator pattern for preprocessing for files.

Rewrite using a functional interface

From now on, let's rewrite the above implementation example using the functional interface.

First, prepare the following functional interface Preprocess.

    /**
     *Preprocessing interface
     */
    @FunctionalInterface
    public static interface Preprocess {

        /**
         *Pre-processing
         *
         * @param lineNo Target line number
         * @param line Target line string
         * @return Pre-processing result
         */
        public String apply(int lineNo, String line);

        // ----- ----- //

        /**
         *Pre-processing synthesis
         *
         * @param next Pre-processing to synthesize
         * @return Pre-processing after synthesis
         */
        default Preprocess compose(Preprocess next) {
            return (int n, String v) -> next.apply(n, this.apply(n, v));
        }

        /**
         *Identity element
         *
         * @return
         */
        public static Preprocess identity() {
            return (lineNo, line) -> line;
        }

        /**
         *Utility function that synthesizes multiple preprocessing
         *
         * @param preprocs Preprocessing for synthesis
         * @return
         */
        static Preprocess compose(final Preprocess... preprocs) {
            return Stream.of(preprocs).reduce((preproc, next) -> preproc.compose(next)).orElse(identity());
        }
    }

Pre-processing execution class is implemented as follows. The only difference from the Decorator pattern version of PreprocedReader Old illustrated in the first half is the handling of pre-processing.

PreprocedReader.java


public class PreprocedReader extends PipedReader {
    private final Reader in;

    /**
     *Pre-processing (synthesized).
     *The initial value is the identity element.
     */
    private Preprocess preprocs = Preprocess.identity();

    public PreprocedReader(Reader in, Preprocess...preprocs) throws IOException {
        this.in = in;
        this.preprocs = Preprocess.compose(preprocs); //Preserve the specified pre-processing after compositing
        doPreproc();
    }

    private void doPreproc() throws IOException {
        PipedWriter pipedOut = new PipedWriter();
        this.connect(pipedOut);

        BufferedReader reader = new BufferedReader(in);
        BufferedWriter writer = new BufferedWriter(pipedOut);
        try (reader; writer;) {
            String line;
            int lineNo = 1;
            while ((line = reader.readLine()) != null) {
                pipedOut.write(preprocs.apply(lineNo, line)); //Application of pre-processing
                pipedOut.write('\n');
            }
        }
    }

    @Override
    public void close() throws IOException {
        in.close();
    }
}

In the Decorator pattern version, the preprocessing implemented as a method (preprocess (int lineNo, String line)) was called, but in the rewritten PreprocedReader, the preprocessing implemented as a function (Preprocess) is called. It is given as a constructor argument, and if there are multiple pre-processes, they are combined into one pre-process and then applied.

Each preprocess is implemented by implementing Preprocess. The following example implements three types of preprocessing.

    /**
     *<< Pre-processing >> Escape processing is performed
     */
    private static class PreprocEscape implements Preprocess {
        @Override
        public String apply(int lineNo, String line) {
            return org.apache.commons.text.StringEscapeUtils.escapeJava(line);
        }
    }
    public static final Preprocess ESCAPE = new PreprocEscape(); //Syntactic sugar

    /**
     *<< Pre-processing >> Trim the file at the specified column position
     */
    private static class PreprocTrimCol implements Preprocess {

        private final int from;
        private final int to;

        public PreprocTrimCol(int from, int to) {
            this.from = from;
            this.to = to;
        }

        @Override
        public String apply(int lineNo, String line) {
            final int len = line.length();
            if (len < to) {
                return line.substring(from);
            } else if (to <= from) {
                return "";
            } else {
                return line.substring(from, to);
            }
        }
    }
    public static final Preprocess TRIM_COL(int from, int to) {  //Syntactic sugar
        return new PreprocTrimCol(from, to);
    }

    /**
     *<< Pre-processing >> Outputs the line contents to standard output without performing line operations.
     */
    private static class PreprocDumpStdout implements Preprocess {
        @Override
        public String apply(int lineNo, String line) {
            System.out.println("[DUMP]"+line);

            return line;
        }
    }
    public static final Preprocess DUMP_STDOUT = new PreprocDumpStdout();  //Syntactic sugar

The pre-processing prepared in this way is applied as follows.

App.java


        InputStream is0 = Files.newInputStream(pathIn);
        InputStreamReader reader1 = new InputStreamReader(is0, csIn);
        Reader reader2 = new PreprocedReader(reader1, TRIM_COL(0, 5), DUMP_STDOUT, ESCAPE); //Added pre-processing
        BufferedReader reader = new BufferedReader(reader2);

        //This processing using reader ...

In the Decorator pattern, multiple constructors had to be nested, but in the rewritten function version, preprocessing can be listed as constructor arguments. The preprocessing given as an argument is executed in order from left to right.

In cases where you want to add multiple features dynamically like this, it is recommended to use the functional interface above instead of the Decorator pattern, as it often simplifies the code. I often use it when defining internal DSLs.

Quiz The pre-processing given as an argument is executed from left to right, but where in Preprocess should be modified to execute from right to left?

** Source code: ** Located on GitHub


reference:

Recommended Posts

Refactor the Decorator pattern implementation with a functional interface
Explain the benefits of the State pattern with a movie rating
Until the interface implementation becomes lambda
Conditional branching with a flowing interface
Exception handling with a fluid interface
Create exceptions with a fluid interface
Create a jar file with the command
Run a DMN with the Camunda DMN Engine
Decorator pattern
Decorator Pattern
Come out with a suffix on the method
Come out with a suffix on the method 2
Matches annotations on the interface with Spring AOP
Create a multi-key map with the standard library
I made a lock pattern using the volume key with the Android app. Fragment edition