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.
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:
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.
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).
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.
class Save implements Action { private final Editor editor; public Save(Editor editor) { this.editor = editor; } @Override public void perform() { editor.save(); } }
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.
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.
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.
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).
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.
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.
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).
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.
class GzipCompressionStrategy implements CompressionStrategy { @Override public OutputStream compress(OutputStream data) throws IOException { return new GZIPOutputStream(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).
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).
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.
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.
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.