In the first two articles of this series we re-implemented, from a more functional point of view, 4 very commonly used GoF patterns. In this third part of the series, we will review 2 more: the Decorator and Chain of Responsibility patterns. These 2 patterns, despite their unquestionable usefulness, are probably employed less widely than the ones we discussed in the former parts, but only because their traditional GoF implementations are excessively verbose and cumbersome, discouraging their usage.

The Decorator pattern

The Decorator pattern allows to dynamically extend the functionality of an existing object by wrapping it with multiple nested layers. All these layers have to implement the same interface in order to allow their composition.

Let’s demonstrate this with a practical example: we’re required to develop a salary calculator starting from a gross annual salary and calculating the net monthly one after having divided it by 12 and applied a series of taxes to it. For convenience, the business logic regarding these taxes has been grouped in a set of static method each of them implementing the application of a given tax.

public class Taxes {
    public static double generalTax(double salary) {
        return salary * 0.8;
    }
    
    public static double regionalTax(double salary) {
        return salary * 0.95;
    }
    
    public static double healthInsurance(double salary) {
        return salary - 200.0;
    }
}

The implementation has to be flexible enough to allow to dynamically add and remove taxes from the salary calculation algorithm. This means that the specific calculation performed by each layer can be modelled as a simple transformation of a double into another double. Each stage of our computation has to then implement the following interface:

interface SalaryCalculator {
    double calculate(double grossAnnual);
}

As anticipated the first stage of this salary calculation process is a division by 12 to obtain the monthly salary starting from the annual one.


public class DefaultSalaryCalculator implements SalaryCalculator {
    @Override
    public double calculate(double grossAnnual) {
        return grossAnnual / 12;
    }
}

Now it is necessary to develop a mechanism to combine this first SalaryCalculator implementation with the others that will apply the afore mentioned taxes. For this we can use the Decorator pattern, encapsulating its essence into an abstract class.

public abstract class AbstractTaxDecorator implements SalaryCalculator {
    private final SalaryCalculator salaryCalculator;

    public AbstractTaxDecorator( SalaryCalculator salaryCalculator ) {
        this.salaryCalculator = salaryCalculator;
    }

    protected abstract double applyTax(double salary);

    @Override
    public final double calculate(double gross) {
        double salary = salaryCalculator.calculate( gross );
        return applyTax( salary );
    }
}

This class decorates (wraps) a SalaryCalculator, but it's also a SalaryCalculator by itself. It has an abstract method where each concrete implementation of this class will have to provide its specific business logic. In this way, this abstract class can directly implement the calculate() method by passing the gross salary to the SalaryCalculator it wraps and then applying its own calculation function to the result. After having developed this abstraction, it is now possible to have a different implementation of the former abstract class for each of the taxes that we may want to eventually apply to the gross salary. In particular, we will have an implementation applying the general tax:

public class GeneralTaxDecorator extends AbstractTaxDecorator {
    public GeneralTaxDecorator( SalaryCalculator salaryCalculator ) {
        super( salaryCalculator );
    }

    @Override
    protected double applyTax(double salary) {
        return Taxes.generalTax( salary );
    }
}

another for the regional one:

public class RegionalTaxDecorator extends AbstractTaxDecorator {
    public RegionalTaxDecorator( SalaryCalculator salaryCalculator ) {
        super( salaryCalculator );
    }

    @Override
    protected double applyTax(double salary) {
        return Taxes.regionalTax( salary );
    }
}

and a third one to cover the health insurance:

public class HealthInsuranceDecorator extends AbstractTaxDecorator {
    public HealthInsuranceDecorator( SalaryCalculator salaryCalculator ) {
        super( salaryCalculator );
    }

    @Override
    protected double applyTax(double salary) {
        return Taxes.healthInsurance( salary );
    }
}

Finally, we are now ready to compose the first DefaultSalaryCalculator with all or a subset of the decorators formerly implemented and to pass to the resulting SalarayCalculator the gross annual salary.

double netSalary = new HealthInsuranceDecorator(
    new RegionalTaxDecorator(
        new GeneralTaxDecorator(
            new DefaultSalaryCalculator()
        )
    )
).calculate( 30000.00 ));

The advantage of this pattern is evidently in the fact that it allows to easily add and remove different decorators and then choose the taxes that should or shouldn't be applied for a certain salary calculation. The drawback, not considering the verbose implementation, is that the computation proceeds from the most internal calculator to the external ones and then the decorators have to be written in reverse order.

As usual let's now review what we have done so far under a more functional perspective. Under this point of view it is obvious that the original DefaultSalaryCalculator is just a function transforming a double into another double. To avoid any unnecessary boxing and unboxing of the primitive double we can make the DefaultSalaryCalculator to implement a DoubleUnaryOperator instead of a plain Function.

public class DefaultSalaryCalculator implements DoubleUnaryOperator {

    @Override
    public double applyAsDouble(double grossAnnual) {
        return grossAnnual / 12;
    }
}

Also all other static methods grouped into the Taxes class have the same signature and then can be seen as other implementations of the DoubleUnaryOperator interface. It's now time to wonder what's the essence of the Decorator pattern. What it actually does it is offering a way to compose different computations, but functions composition is of course something that is far more natural in functional programming. It's almost surprising that we can achieve exactly the same result that costed us so many efforts using the Decorator pattern in a trivial way as it follows:

double netSalary = new DefaultSalaryCalculator()
        .andThen( Taxes::generalTax )
        .andThen( Taxes::regionalTax ) 
        .andThen( Taxes::healthInsurance )
        .applyAsDouble( 30000.00 );

Note that this idiom allows to add and remove functions on demand in the same way that it was possible with the Decorator based implementation. Moreover, this time the computation proceeds in the same order with which the functions are written. It is also possible to provide a nicer API to our user implementing a method that accepts the gross salary and a varargs of the functions to be applied to it.

public static double calculate(double gross, DoubleUnaryOperator... fs) {
    return Stream.of( fs )
                 .reduce( DoubleUnaryOperator.identity(), 
                          DoubleUnaryOperator::andThen )
                 .applyAsDouble( gross );
}

Here the functions composition is achieved by putting all the functions in the varargs array into a Stream and reducing the Stream of functions into a single one. Now it is possible to calculate the net salary by invoking this new static method in this way.

double netSalary = calculate( 30000.00, 
                                  new DefaultSalaryCalculator(), 
                                  Taxes::generalTax, 
                                  Taxes::regionalTax, 
                                  Taxes::healthInsurance );

The Chain of Responsibility pattern

This pattern consists in creating a sequence of objects that are designed to process an input. Each object in the chain could, or could not, be able to process a specific input. If a processing object has been designed for the input at hand it will do so, otherwise it will pass the input down to the next object of the chain. If the last object of the chain also isn't able to process a given input, the chain will fail silently, or more commonly, will notify the user of the failure with an Exception. As usual, let's try to put this general idea to work with a practical example.

Let's suppose that we have a file to parse and the file can be of 3 different types: text, audio and video.

public class File {
     
    enum Type { TEXT, AUDIO, VIDEO }
    
    private final Type type;
    private final String content;
    
    public File( Type type, String content ) {
        this.type = type;
        this.content = content;
    }
    
    public Type getType() {
        return type;
    }
    
    public String getContent() {
        return content;
    }
    
    @Override
    public String toString() {
        return type + ": " + content;
    }
}

Let's then create an interface defining the behaviours of each item in the chain: parsing the File and setting the next item of the chain.

interface FileParser {
    String parse(File file);
    void setNextParser(FileParser next);
}

The possibility of setting the next parser will have exactly the same implementation for all processing objects so we can put it into an abstract class.

public abstract class AbstractFileParser implements FileParser {
    protected FileParser next;

    @Override
    public void setNextParser( FileParser next ) {
        this.next = next;
    }
}

Now we're ready to implement the first concrete File parser that will be able to manage files of type text.

public class TextFileParser extends AbstractFileParser {
    @Override
    public String parse( File file ) {
        if ( file.getType() == File.Type.TEXT ) {
            return "Text file: " + file.getContent();
        } else if (next != null) {
            return next.parse( file );
        } else {
           throw new RuntimeException( "Unknown file: " + file );
        }
    }
}

If the file is textual, this parser returns the result of the parsing, otherwise it delegates the parsing process to the next parser in the chain. If this parser is the last one it means that the whole chain haven't been able to parse the file and then it will report this problem by throwing an Exception. The audio and video parser will do exactly the same thing for the respective types of file of their competence.

public class AudioFileParser extends AbstractFileParser {
    @Override
    public String parse( File file ) {
        if ( file.getType() == File.Type.AUDIO ) {
            return "Audio file: " + file.getContent();
        } else if (next != null) {
            return next.parse( file );
        } else {
            throw new RuntimeException( "Unknown file: " + file );
        }
    }
}

public class VideoFileParser extends AbstractFileParser {
    @Override
    public String parse( File file ) {
        if ( file.getType() == File.Type.VIDEO ) {
            return "Video file: " + file.getContent();
        } else if (next != null) {
            return next.parse( file );
        } else {
            throw new RuntimeException( "Unknown file: " + file );
        }
    }
}

At this point have all the pieces to put our parser based on the chain of responsibility pattern at work. First of all, we need to create an instance for each item of the chain.

FileParser textParser = new TextFileParser();
FileParser audioParser = new AudioFileParser();
FileParser videoParser = new VideoFileParser();

Then it is necessary to set the chain up by wiring all the single parsers together.

textParser.setNextParser( audioParser );
audioParser.setNextParser( videoParser );

Finally it is now possible to try to parse a file passing it to the first item in the chain. Fantastic! It is the last amazing Dream Theater's album.

File file = new File( File.Type.AUDIO, "Dream Theater  - The Astonishing" );
String result = textParser.parse( file );

The first parser of the chain, the one designed to parse text file won't be able to parse this file so it will pass the file to the next parser, the one for audio files. This second parser can parse a Dream Theater album so it will return the result of the parsing. Since this second item of the chain has been able to perform the required parsing task, the file won't reach the third parser, the one for the video files.

As we have done with all other patterns we have analyzed, so far let's try to extract the business logic artificially wrapped in classes into pure functions. For instance, we can then have a function that can parse text file, but what should it do when it is passed with a different type of file? At this stage we don't have another parser function to which delegate the parsing process and we don't want to report the problem by throwing an Exception even because if we will do this we won't be able to compose this function with others.

In this case, both throwing an Exception or returning a null pointer won't be the functional idiomatic solution. Java 8 introduced the new Optional class explicitly to deal with these cases. An Optional value models a result that may or may not be present, so we can make our parsing function to return an Optional wrapping the result in case of a successful parsing or an empty Optional if the function is passed with a non-textual file.

public static Optional<String> parseText(File file) {
    return file.getType() == File.Type.TEXT ?
           Optional.of("Text file: " + file.getContent()) :
           Optional.empty();
}

In the same way, we can develop two more functions to parse audio and video files.

public static Optional<String> parseAudio(File file) {
    return file.getType() == File.Type.AUDIO ?
           Optional.of("Audio file: " + file.getContent()) :
           Optional.empty();
}

public static Optional<String> parseVideo(File file) {
    return file.getType() == File.Type.VIDEO ?
           Optional.of("Video file: " + file.getContent()) :
           Optional.empty();
}

It is now possible to wire these functions in a virtual chain by putting them in a Stream and passing the file to be parsed to all functions in the Stream.

File file = new File( File.Type.AUDIO, "Dream Theater  - The Astonishing" );

String result = Stream.<Function<File, Optional<String>>>of( // [1]
        ChainOfRespLambda::parseText,
        ChainOfRespLambda::parseAudio,
        ChainOfRespLambda::parseVideo )
        .map(f -> f.apply( file )) // [2]
        .filter( Optional::isPresent ) // [3]
        .findFirst() // [4]
        .flatMap( Function.identity() ) // [5]
        .orElseThrow( () -> new RuntimeException( "Unknown file: " + file ) ) ); [6]

To figure out what's going on here, let's analyze this single fluent statement in a bit more detail. First of all we put all our parsing function in a Stream [1]. Here the Java compiler requires a small help: it is not able to figure what type of objects we're putting in the Stream, so we have to explicitly state this. Then we transform a Stream into a Stream<Optional> by calling apply on each function of the original Stream passing the file to be parsed [2].

Now we need to find the first Optional in the resulting Stream that is not empty so we filter only the Optionals that are present [3] and take the first of them [4]. Calling findFirst() on a Stream of Optional has a minor drawback: since it wraps the result in an Optional (because the Stream could be empty and in that case it will return an Optional.empty()) this time the result will be a Optional<Optional>. We need to flatten this doubly nested Optional in a single level one and to achieve this is enough to call flatMap passing a Function.identity() [5]. Finally, we have an Optional that wraps a value if at least one of our parsers was able to parse it or it is empty otherwise. We then unwrap the value contained in the Optional or we throw an Exception if the Optional was empty [6] exactly as we did in the original object-oriented chain of responsibility.

There is also another analogy between the object-oriented and the functional versions: in both cases the parser are invoked until the first one capable of performing the file parsing is found. In our example, the video parser is never invoked because the audio parser, coming before it in the sequence of parsers, can parse the Dream Theater's album and returns a non-empty result. In the functional implementation, this feature is ensured by Stream's laziness that keeps invoking the transformation function defined in the lambda passed to the map() method only until it finds the first non-empty Optional.

In the next (and last) part of this series of article we will investigate and refactor the Interpreter and the Visitor patterns.

Gang of Four Patterns in a Functional Light: Part 3

About The Author
-

2 Comments

Leave a Reply to Ajay Iyengar Cancel reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>