Chapter 3. Design Patterns

Functional Design Patterns

One of the other bastions of design we’re all familiar with is the idea of design patterns. Patterns document reusable templates that solve common problems in software architecture. If you spot a problem and you’re familiar with an appropriate pattern, then you can take the pattern and apply it to your situation. In a sense, patterns codify what people consider to be a best-practice approach to a given problem.

In this section, we’re instead going to look at how existing design patterns have become better, simpler, or in some cases, implementable in a different way. In all cases, the application of lambda expressions and a more functional approach are the driving factor behind the pattern changing.

The Command Pattern

A command object is an object that encapsulates all the information required to call another method later. The command pattern is a way of using this object in order to write generic code that sequences and executes methods based on runtime decisions. There are four classes that take part in the command pattern, as shown in Figure 3-1:

Receiver
Performs the actual work
Command
Encapsulates all the information required to call the receiver
Invoker
Controls the sequencing and execution of one or more commands
Client
Creates concrete command instances
Figure 3-1. The command pattern

Let’s look at a concrete example of the command pattern and see how it improves with lambda expressions. Suppose we have a GUI Editor component that has actions upon it that we’ll be calling, such as open or save, like in Example 3-1. We want to implement macro functionality—that is, a series of operations that can be recorded and then run later as a single operation. This is our receiver.

Example 3-1. Common functions a text editor may have
interface Editor {

    void save();

    void open();

    void close();

}
    

In this example, each of the operations, such as open and save, are commands. We need a generic command interface to fit these different operations into. I’ll call this interface Action, as it represents performing a single action within our domain. This is the interface that all our command objects implement (Example 3-2).

Example 3-2. All our actions implement the Action interface
interface Action {

    void perform();

}
    

We can now implement our Action interface for each of the operations. All these classes need to do is call a single method on our Editor and wrap this call into our Action interface. I’ll name the classes after the operations that they wrap, with the appropriate class naming convention—so, the save method corresponds to a class called Save. Example 3-3 and Example 3-4 are our command objects.

Example 3-3. Our save action delegates to the underlying method call on Editor
class Save implements Action {

    private final Editor editor;

    public Save(Editor editor) {
        this.editor = editor;
    }

    @Override
    public void perform() {
        editor.save();
    }
}
    
Example 3-4. Our open action also delegates to the underlying method call on Editor
class Open implements Action {

    private final Editor editor;

    public Open(Editor editor) {
        this.editor = editor;
    }

    @Override
    public void perform() {
        editor.open();
    }
}
    

Now we can implement our Macro class. This class can record actions and run them as a group. We use a List to store the sequence of actions and then call forEach in order to execute each Action in turn. Example 3-5 is our invoker.

Example 3-5. A macro consists of a sequence of actions that can be invoked in turn
class Macro {

    private final List<Action> actions;

    public Macro() {
        actions = new ArrayList<>();
    }

    public void record(Action action) {
        actions.add(action);
    }

    public void run() {
        actions.forEach(Action::perform);
    }

}
    

When we come to build up a macro programmatically, we add an instance of each command that has been recorded to the Macro object. We can then just run the macro, and it will call each of the commands in turn. As a lazy programmer, I love the ability to define common workflows as macros. Did I say “lazy?” I meant focused on improving my productivity. The Macro object is our client code and is shown in Example 3-6.

Example 3-6. Building up a macro with the command pattern
Macro macro = new Macro();
macro.record(new Open(editor));
macro.record(new Save(editor));
macro.record(new Close(editor));
macro.run();
    

How do lambda expressions help? Actually, all our command classes, such as Save and Open, are really just lambda expressions wanting to get out of their shells. They are blocks of behavior that we’re creating classes in order to pass around. This whole pattern becomes a lot simpler with lambda expressions because we can entirely dispense with these classes. Example 3-7 shows how to use our Macro class without these command classes and with lambda expressions instead.

Example 3-7. Using lambda expressions to build up a macro
Macro macro = new Macro();
macro.record(() -> editor.open());
macro.record(() -> editor.save());
macro.record(() -> editor.close());
macro.run();
    

In fact, we can do this even better by recognizing that each of these lambda expressions is performing a single method call. So, we can actually use method references in order to wire the editor’s commands to the macro object (see Example 3-8).

Example 3-8. Using method references to build up a macro
Macro macro = new Macro();
macro.record(editor::open);
macro.record(editor::save);
macro.record(editor::close);
macro.run();
    

The command pattern is really just a poor man’s lambda expression to begin with. By using actual lambda expressions or method references, we can clean up the code, reducing the amount of boilerplate required and making the intent of the code more obvious.

Macros are just one example of how we can use the command pattern. It’s frequently used in implementing component-based GUI systems, undo functions, thread pools, transactions, and wizards.

Note

There is already a functional interface with the same structure as our interface Action in core Java—Runnable. We could have chosen to use that in our macro class, but in this case, it seemed more appropriate to consider an Action to be part of the vocabulary of our domain and create our own interface.

Strategy Pattern

The strategy pattern is a way of changing the algorithmic behavior of software based upon a runtime decision. How you implement the strategy pattern depends upon your circumstances, but in all cases, the main idea is to be able to define a common problem that is solved by different algorithms and then encapsulate all the algorithms behind the same programming interface.

An example algorithm we might want to encapsulate is compressing files. We’ll give our users the choice of compressing our files using either the zip algorithm or the gzip algorithm and implement a generic Compressor class that can compress using either algorithm.

First we need to define the API for our strategy (see Figure 3-2), which I’ll call CompressionStrategy. Each of our compression algorithms will implement this interface. They have the compress method, which takes and returns an OutputStream. The returned OutputStream is a compressed version of the input (see Example 3-9).

Figure 3-2. The Strategy Pattern
Example 3-9. Defining a strategy interface for compressing data
interface CompressionStrategy {

    OutputStream compress(OutputStream data) throws IOException;

}
    

We have two concrete implementations of this interface, one for gzip and one for zip, which use the built-in Java classes to write gzip (Example 3-10) and zip (Example 3-11) files.

Example 3-10. Using the gzip algorithm to compress data
class GzipCompressionStrategy implements CompressionStrategy {

    @Override
    public OutputStream compress(OutputStream data) throws IOException {
        return new GZIPOutputStream(data);
    }

}
    
Example 3-11. Using the zip algorithm to compress data
class ZipCompressionStrategy implements CompressionStrategy {

    @Override
    public OutputStream compress(OutputStream data) throws IOException {
        return new ZipOutputStream(data);
    }

}
    

Now we can implement our Compressor class, which is the context in which we use our strategy. This has a compress method on it that takes input and output files and writes a compressed version of the input file to the output file. It takes the CompressionStrategy as a constructor parameter that its calling code can use to make a runtime choice as to which compression strategy to use—for example, getting user input that would make the decision (see Example 3-12).

Example 3-12. Our compressor is provided with a compression strategy at construction time
class Compressor {

    private final CompressionStrategy strategy;

    public Compressor(CompressionStrategy strategy) {
        this.strategy = strategy;
    }

    public void compress(Path inFile, File outFile) throws IOException {
        try (OutputStream outStream = new FileOutputStream(outFile)) {
            Files.copy(inFile, strategy.compress(outStream));
        }
    }
}
    

If we have a traditional implementation of the strategy pattern, then we can write client code that creates a new Compressor with whichever strategy we want (Example 3-13).

Example 3-13. Instantiating the Compressor using concrete strategy classes
Compressor gzipCompressor = 
  new Compressor(new GzipCompressionStrategy());
gzipCompressor.compress(inFile, outFile);

Compressor zipCompressor = new Compressor(new ZipCompressionStrategy());
zipCompressor.compress(inFile, outFile);
    

As with the command pattern discussed earlier, using either lambda expressions or method references allows us to remove a whole layer of boilerplate code from this pattern. In this case, we can remove each of the concrete strategy implementations and refer to a method that implements the algorithm. Here the algorithms are represented by the constructors of the relevant OutputStream implementation. We can totally dispense with the GzipCompressionStrategy and ZipCompressionStrategy classes when taking this approach. Example 3-14 is what the code would look like if we used method references.

Example 3-14. Instantiating the Compressor using method references
Compressor gzipCompressor = new Compressor(GZIPOutputStream::new);
gzipCompressor.compress(inFile, outFile);
        
Compressor zipCompressor = new Compressor(ZipOutputStream::new);
zipCompressor.compress(inFile, outFile);
    

Yet again thinking in a more functional way—modelling in terms of functions rather than classes and objects—has allowed us to reduce the boilerplate and simplify an existing design pattern. This is the great win about being able to combine the functional and object-oriented world view: you get to pick the right approach for the right situation.

Summary

In this section, we have evaluated a series of design patterns and talked about how they could all be used differently with lambda expressions. In some respect, a lot of these patterns are really object-oriented embodiments of functional ideas. Take the command pattern. It’s called a pattern and has some different interacting components, but the essence of the pattern is the passing around and invoking of behavior. The command pattern is all about first-class functions.

The same thing with the Strategy pattern. Its really all about putting together some behavior and passing it around; again it’s a design pattern that’s mimicking first-class functions. Programming languages that have a first-class representation of functions often don’t talk about the strategy or command patterns, but this is what developers are doing. This is an important theme in this report. Often times, both functional and object-oriented programming languages end up with similar patterns of code, but with different names associated with them.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset