In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class.
In this article I am going to talk about the Decorator Design Pattern.
More specifically, I will present some combinations of the Decorator pattern with other Design Patterns in order to achieve better readability and maintainability of our code. I am going to use Java in the code segments of this article but all the examples should be applicable to any object oriented language.
Before examining the Design Pattern synergies let’s make sure that we have a common understanding of the Decorator pattern.
In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class.
Another definition provided in the book Head First Design Patterns is the following:
The Decorator Pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
The Decorator pattern has some very powerful features.
The Decorator Pattern attaches additional responsibilities to an object dynamically.
This means that one may decorate an object at runtime and add additional functionality to that object.
Imagine this example. We have a program that writes a file to the file system.
This could be expressed in code like that:
File file = new File("hello.txt"); try(OutputStream os = new FileOutputStream(file)) { os.write("Hello, World!".getBytes()); }
At some point we decide that the file should be compressed! Then by following the Decorator pattern we may add this additional functionality without changing any of the involved classes!
File file = new File("hello.gz"); try(OutputStream os = new GZIPOutputStream(new FileOutputStream(file))) { os.write("Hello, World!".getBytes()); }
Have in mind that the try-with-resources may
fail to close the resources
in case an error is thrown during the construction of one of the wrappers. |
To make the change at runtime part more obvious, let’s say that some of our users want to select if the file will be compressed or not.
// compressed is a boolean - true or false File file = compressed ? new File("hello.gz") : new File("hello.txt"); try (OutputStream fos = new FileOutputStream(file); OutputStream os = compressed ? new GZIPOutputStream(fos) : fos) { os.write("Hello, World!".getBytes()); }
See, in the above example the behavior may be decided at runtime.
The key point in this case is that the decorators all implement the OutputStream interface.
Every Decorator adds an extra bit of functionality around the methods
defined by the interface. For example the GZIPOutputStream will wrap
the os.write("Hello, World!".getBytes())
and compress
the data. Then the write()
method of FileOutputStream will be called which
will write the compressed data to the specified file.
I guess this is the most common and probably most powerful form of the Decorator pattern. You may have an OutputStream object which has been wrapped around dozen of times, each time with a decorator which adds a tiny bit more functionality.
Except the case described above there are times where we may need to add additional functionality to an object. Functionality which may not follow a common interface.
We still can do that. Consider the following example where we want additionally to produce a hash of the created file and print it to the user.
File file = compressed ? new File("hello.gz") : new File("hello.txt"); try (OutputStream fos = new FileOutputStream(file); CheckedOutputStream cos = new CheckedOutputStream(fos, new Adler32()); OutputStream os = compressed ? new GZIPOutputStream(cos) : cos) { os.write("Hello, World!".getBytes()); System.out.println(cos.getChecksum().getValue()); }
See, we have added some extra functionality for the calculation of the checksum. This has the implication that now we rely on the existence of the cos variable in our code which has the explicit type of CheckedOutputStream.
This is not so bad actually. On the other hand, it would complicate things if we wanted to write the try-with-resources a bit differently, by not using separate variables for each decorator.
File file = new File("hello.gz"); try (OutputStream os = new GZIPOutputStream(new CheckedOutputStream( new FileOutputStream(file), new Adler32()))) { os.write("Hello, World!".getBytes()); // How we will get checksum? os is not of type CheckedOutputStream! // System.out.println(cos.getChecksum().getValue()); }
Of course, in that specific case there is a good way to get the checksum
by keeping a reference of the Adler32
object.
Checksum checksum = new Adler32(); File file = new File("hello.gz"); try (OutputStream os = new GZIPOutputStream(new CheckedOutputStream( new FileOutputStream(file), checksum))) { os.write("Hello, World!".getBytes()); System.out.println(checksum.getValue()); }
This is not always the case as we may need to access fields of a specific wrapper. In case the specific decorator we need to access is the last one on the stack then it is easy to access it. Otherwise keeping a reference of the specific decorator object is the only option.
Most of the Decorators we have seen in the previous examples are part of the java.io package. The basic interface the Decorators use is the OutputStream.
There are numerous implementations of this Decorator. One may find them by examining the Javadoc of the OutputStream interface.
The Direct Known Subclasses of OutputSteam are:
In addition to that the Direct Known Subclasses of FilterOutputStream are:
The reason FilterOutputStream has so many subclasses is that this class is the superclass of all classes that filter output streams. These streams sit on top of an already existing output stream (the underlying output stream) which it uses as its basic sink of data, but possibly transforming the data along the way or providing additional functionality.
Wait! There is more! The DeflaterOutputStream has the following Direct Known Subclasses:
Then ZipOutputStream has as Direct Known Subclasses the JarOutputStream.
PrintStream has as Direct Known Subclasses the LogStream, which however is now deprecated.
If we count all that we have in total 15 concrete OutputStream implementations.
For sure one has to be pretty familiar with the java.io
API in order to
use it in its full potential.
The Decorator pattern is also used in other parts of the API, for example the InputStream.
By now the problems of the Decorator pattern should be apparent.
Too many implementations of a Decorator interface may exist. It may be difficult for one to find all the implementations or know how to use them. Note that the OutputStream classes are not even all included in a package exclusive to this type of decorator!
It’s difficult to access methods that are not defined by the base interface, especially when the specific decorator which adds the extra functionality is wrapped by many other instances of decorators.
Below we are going to try to address these two issues by adding more design patterns in the mix!
Before going on let’s set some common code that we are going to try to improve later on:
byte[] data = {'H', 'e', 'l', 'l', 'o', '\n', '\0'}; File file = new File("hello.gz"); OutputStream os = new FileOutputStream(file); os = new BufferedOutputStream(os); CheckedOutputStream cos = new CheckedOutputStream(os, new Adler32()); os = cos; os = new GZIPOutputStream(os); PrintStream pos = new PrintStream(os); os = pos; os.write(data); pos.println("World!"); os.flush(); os.close(); System.out.println(cos.getChecksum().getValue());
In this example we are keeping an OutputStream object for making all the basic actions on it and we keep references to the CheckedOutputStream and PrintStream decorators so we may call some methods provided exclusively by these 2 types.
Let’s omit the try-with-resources for now for the sake of clarity.
The first design pattern which comes in mind when initializing objects that share a common interface is probably the Factory method pattern.
Normally a factory method accepts one or a few more arguments. A factory for our case could seem like that:
public class OutputStreamFactory { public static OutputStream outputStream( boolean buffered, boolean checked, boolean gzipped, boolean printStreamed) throws FileNotFoundException, IOException { OutputStream os = new ByteArrayOutputStream(); return outputStream(os, buffered, checked, gzipped, printStreamed); } public static OutputStream outputStream(File file, boolean buffered, boolean checked, boolean gzipped, boolean printStreamed) throws FileNotFoundException, IOException { OutputStream os = new FileOutputStream(file); return outputStream(os, buffered, checked, gzipped, printStreamed); } private static OutputStream outputStream(OutputStream os, boolean buffered, boolean checked, boolean gzipped, boolean printStreamed) throws FileNotFoundException, IOException { if (buffered) { os = new BufferedOutputStream(os); } if (checked) { os = new CheckedOutputStream(os, new Adler32()); } if (gzipped) { os = new GZIPOutputStream(os); } if (printStreamed) { os = new PrintStream(os); } return os; } }
In code this could be used like that:
byte[] data = {'H', 'e', 'l', 'l', 'o', '\n', '\0'}; File file = new File("hello.gz"); OutputStream os = OutputStreamFactory.outputStream(file, true, true, true, true); os.write(data); os.write("World!".getBytes()); os.flush(); os.close();
So, what have we achieved?
PROS
Less code to initialize our decorated object.
All the possible decorators we want to support may be initialized by the factory method. It is easy to inspect a single method, its arguments and documentation and know what kind of decorators are available.
The use of the concrete decorator classes is encapsulated inside the factory method and this permits future changes with little refactoring effort.
CONS
There is no way to get access to the internal decorators.
The factory method may get too crowded with many arguments.
Exception handling and object initialization may be a bit more complex.
In general I don’t find personally any great benefit to the above solution.
Maybe a bit different implementation of multiple factory methods, one for each case could be more beneficial.
Consider the following class:
public class OutputStreamFactory { /** * Creates a new ByteArrayOutputStream. * @return An empty ByteArrayOutputStream. */ public static ByteArrayOutputStream byteArrayOutputStream() { return new ByteArrayOutputStream(); } public static FileOutputStream fileOutputStream(File file) throws FileNotFoundException { return new FileOutputStream(file); } public static BufferedOutputStream bufferedOutputStream(OutputStream os) { return new BufferedOutputStream(os); } public static CheckedOutputStream checkedOutputStream(OutputStream os, Checksum checksum) { return new CheckedOutputStream(os, checksum); } public static GZIPOutputStream gzipOutputStream(OutputStream os) throws IOException { return new GZIPOutputStream(os); } public static PrintStream printStream(OutputStream os) { return new PrintStream(os); } }
Hmm, this way we have actually wrapped the initialization of each concrete OutputStream to a separate method. This would be used in action like that:
byte[] data = {'H', 'e', 'l', 'l', 'o', '\n', '\0'}; File file = new File("hello.gz"); OutputStream os = OutputStreamFactory.fileOutputStream(file); os = OutputStreamFactory.bufferedOutputStream(os); CheckedOutputStream cos = OutputStreamFactory.checkedOutputStream(os, new Adler32()); os = cos; os = OutputStreamFactory.gzipOutputStream(os); PrintStream pos = OutputStreamFactory.printStream(os); os = pos; os.write(data); pos.println("World!"); os.flush(); os.close(); System.out.println(cos.getChecksum().getValue());
If you consider this for a moment this is exactly like calling the concrete constructors but with some additional benefits!
Furthermore we could return the interface type but I didn’t implemented it like that in this example to avoid extra type casting.
Extra PROS
Easily find all the available Decorators by browsing the (preferably documented) methods of the OutputStreamFactory class. All modern IDEs should support auto-completion.
Encapsulated classes can be altered with minimal refactoring (especially if we would return OutputStream objects instead of the concrete classes).
In general I see benefit from having a class like this only in the case there are too many decorators around. The programmer would be free to spend less time hunting external documentation and more time relying to the every day tools of the trade in order to find easily the required information.
CONS
Every other drawback we had before still remains.
The API is too verbose. Statically importing the factory class makes it less verbose but maybe a bit less readable because all the factory methods could be mistaken for local ones.
Another popular pattern for object creation is the Builder pattern.
We are going to use Josh Bloch’s variation of the pattern as described in Effective Java.
This should look like this:
public class OutputStreamBuilder { enum BaseType { ByteArrayOutputStream, FileOutputStream; } private boolean buffered; private boolean checked; private boolean gzipped; private boolean printStreamed; private Checksum checksum; private File file; private final BaseType baseType; public OutputStreamBuilder() { this.baseType = BaseType.ByteArrayOutputStream; } public OutputStreamBuilder(File file) throws FileNotFoundException { this.file = file; this.baseType = BaseType.FileOutputStream; } public OutputStreamBuilder bufferedOutputStream(boolean buffered) { this.buffered = buffered; return this; } public OutputStreamBuilder checkedOutputStream(boolean checked, Checksum checksum) { this.checked = checked; this.checksum = checksum; return this; } public OutputStreamBuilder gzipOutputStream(boolean gzipped) throws IOException { this.gzipped = gzipped; return this; } public OutputStreamBuilder printStream(boolean printStreamed) { this.printStreamed = printStreamed; return this; } public OutputStream build() throws FileNotFoundException, IOException { OutputStream os = null; switch(baseType) { case ByteArrayOutputStream: os = new ByteArrayOutputStream(); break; case FileOutputStream: os = new FileOutputStream(file); break; default: throw new IllegalStateException("Base OutputStream not specified."); } if (buffered) { os = new BufferedOutputStream(os); } if (checked) { os = new CheckedOutputStream(os, checksum); } if (gzipped) { os = new GZIPOutputStream(os); } if (printStreamed) { os = new PrintStream(os); } return os; } }
This could be used like that:
byte[] data = {'H', 'e', 'l', 'l', 'o', '\n', '\0'}; File file = new File("hello.gz"); Checksum cksum = new Adler32(); OutputStream os = new OutputStreamBuilder(file) .bufferedOutputStream(true) .checkedOutputStream(true, cksum) .gzipOutputStream(true) .printStream(true) .build(); PrintStream pos = (PrintStream) os; os.write(data); pos.println("World!"); os.flush(); os.close(); System.out.println(cksum.getValue());
Let’s rate this pattern!
PROS
The decorators can be easily found under the builder class.
The syntax is short enough.
We can use the created builder to create many similar objects.
CONS
We cannot access intermediate decorators. For example in the code above we cast the OutputStream to PrintStream which is the last one used. We cannot access for example the CheckedOutputStream directly.
The order of the decoration is fixed inside the build()
method. We cannot
decorate multiple times with the same decorator or alter the order easily without
making the implementation too complex. However, this structure is sufficient
for most of the common cases.
A couple of notes.
We could make the builder class implement Autocloseable in order to free correctly the resources if used in try-with-resources.
We can omit calling the builder methods if we want to pass false to them because this is the default value. Additionally we could achieve a more fluent API. if we didn’t accept arguments at all. For example calling a more fluent API for the previous example would be like that:
OutputStream os = new OutputStreamBuilder(file) .bufferedOutputStream() .checkedOutputStream(cksum) .gzipOutputStream() .printStream() .build();
Of course having arguments makes a bit easier to use the API in some cases. Consider the example where the compressed functionality was added at runtime based on some condition.
I think I personally like this way most of all so far. It has some rough edges but it provides a beautiful and readable interface.
The builder pattern as described by Bloch has some important benefits.
In contrast to Javabean initialization of objects by using the setters methods this way we can be sure that the created object will be fully initialized and has the expected state before we use it.
A factory may be re-used and create other instances of similar objects.
In the case of the decorators builder we may disregard these benefits in favor of other. The builder could be written as such:
public class OutputStreamBuilder { private OutputStream os; public OutputStreamBuilder() { os = new ByteArrayOutputStream(); } public OutputStreamBuilder(File file) throws FileNotFoundException { os = new FileOutputStream(file); } public OutputStreamBuilder(OutputStream os) { this.os = os; } public OutputStreamBuilder bufferedOutputStream() { os = new BufferedOutputStream(os); return this; } public OutputStreamBuilder checkedOutputStream(Checksum checksum) { os = new CheckedOutputStream(os, checksum); return this; } public OutputStreamBuilder gzipOutputStream() throws IOException { os = new GZIPOutputStream(os); return this; } public OutputStreamBuilder printStream() { os = new PrintStream(os); return this; } public OutputStream build() { return os; } }
Now, we have lost the 2nd previously mentioned benefit. However, we gained something else. We can access this way the intermediate decorators:
OutputStreamBuilder builder = new OutputStreamBuilder(file) .bufferedOutputStream() .checkedOutputStream(cksum); CheckedOutputStream cos = (CheckedOutputStream) builder.build(); OutputStream os = builder .gzipOutputStream() .printStream() .build(); PrintStream pos = (PrintStream) os; os.write(data); pos.println("World!"); os.flush(); os.close(); System.out.println(cos.getChecksum().getValue());
If wanted we could wrap a decorator multiple times that way. Also, the order the decorators are applied isn’t any more fixed.
As we can see this variation of building objects may often be more suitable for building decorators in comparison to the original.
I think with this we have more or less exhausted the topic of initializing instances of decorators. At the process we got familiar with each approach and with the benefits that offers and the problems that introduces.
Let’s see if we can address the second issue. When we decorate an object with a decorator which adds additional functionality not defined in the decorator interface we have to keep references to the concrete class instead.
In software engineering, the adapter pattern is a software design pattern (also known as wrapper, an alternative naming shared with the decorator pattern) that allows the interface of an existing class to be used as another interface.[1] It is often used to make existing classes work with others without modifying their source code.
In order to solve the problem of having access to the intermediate concrete classes we can instead create an adapter that exposes all the intermediate interfaces.
The adapter in our example could look like this:
public class OutputStreamAdapter extends OutputStream implements Appendable, Closeable { private OutputStream os; private CheckedOutputStream cos; private DeflaterOutputStream dos; private Appendable appendable; private PrintStream pos; public OutputStreamAdapter(OutputStream os) { setOutputStream(os); } public void addOutputStream(OutputStream o) { setOutputStream(o); } private void setOutputStream(OutputStream o) { os = o; if (o instanceof CheckedOutputStream) { cos = (CheckedOutputStream) o; } if (o instanceof DeflaterOutputStream) { dos = (DeflaterOutputStream) o; } if (o instanceof Appendable) { appendable = (Appendable) o; } if (o instanceof PrintStream) { pos = (PrintStream) o; } } @Override public void write(int i) throws IOException { os.write(i); } public Checksum getChecksum() { if (cos != null) { return cos.getChecksum(); } else if (os instanceof OutputStreamAdapter) { return ((OutputStreamAdapter) os).getChecksum(); } throw new UnsupportedOperationException("Decorator method not implemented."); } public void finish() throws IOException { if (dos != null) { dos.finish(); } else if (os instanceof OutputStreamAdapter) { ((OutputStreamAdapter) os).finish(); } throw new UnsupportedOperationException("Decorator method not implemented."); } @Override public void flush() throws IOException { os.flush(); } @Override public void write(byte[] b, int off, int len) throws IOException { os.write(b, off, len); } @Override public void write(byte[] b) throws IOException { System.out.println("WRITE!"); os.write(b); } @Override public void close() throws IOException { os.close(); } @Override public Appendable append(CharSequence cs) throws IOException { if (appendable != null) { appendable.append(cs); } throw new UnsupportedOperationException("Decorator method not implemented."); } @Override public Appendable append(CharSequence cs, int i, int i1) throws IOException { if (appendable != null) { appendable.append(cs, i, i1); } throw new UnsupportedOperationException("Decorator method not implemented."); } @Override public Appendable append(char c) throws IOException { if (appendable != null) { appendable.append(c); } throw new UnsupportedOperationException("Decorator method not implemented."); } public void println(String x) { if (pos != null) { pos.println(x); } else { throw new UnsupportedOperationException("Decorator method not implemented."); } } }
I have implemented only some of the methods in this example but the point is to implement all of the decorators that add additional functionality.
With an adapter like this we may access each one directly even if it is not implemented by all the decorators. An exception will be thrown if the implementation does not exist.
This could be used like that:
OutputStreamAdapter os = new OutputStreamBuilderWithAdapter(file) .bufferedOutputStream(true) .checkedOutputStream(true, cksum) .gzipOutputStream(true) .printStream(true) .build(); os.write(data); os.println("World!"); os.flush(); os.close(); System.out.println(os.getChecksum().getValue());
This uses the Bloch-based Builder class from before. However, some modification is required.
The build()
method should look like that:
public OutputStreamAdapter build() throws FileNotFoundException, IOException { OutputStreamAdapter osa; OutputStream os; switch(baseType) { case ByteArrayOutputStream: os = new OutputStreamAdapter(new ByteArrayOutputStream()); break; case FileOutputStream: os = new OutputStreamAdapter(new FileOutputStream(file)); break; default: throw new IllegalStateException("Base OutputStream not specified."); } osa = new OutputStreamAdapter(os); if (buffered) { os = new BufferedOutputStream(os); osa.addOutputStream(os); } if (checked) { os = new CheckedOutputStream(os, checksum); osa.addOutputStream(os); } if (gzipped) { os = new GZIPOutputStream(os); osa.addOutputStream(os); } if (printStreamed) { os = new PrintStream(os); osa.addOutputStream(os); } return osa; }
PROS
We can access all the intermediate decorator functionality without leaking references to the intermediate concrete classes.
We have control if the implementation is not there to throw an exception and delegate the control to the client, fail silently or graciously somehow.
CONS
Many more lines of code to implement the adapter.
Increased maintenance cost. For each new decorator which deviates from the main interface we have to add the new methods to the adapter. For any change we may have to update the adapter accordingly.
Builder build()
method is kind of more complicated.
In general however I feel for certain occasions it offers some useful functionality.
In this article we explored various combinations of the Decorator pattern with the Factory method, Builder and Adapter patterns. Hopefully, we all have a better understanding of what each combination offers.
By all means do not go out and start using every bit of design pattern combination demonstrated here.
Using the Decorator pattern alone is fine for most of the cases, especially if the total number of decorators is low. Just don’t forget to provide the necessary documentation and maybe group them all together under the same package/directory.
Don’t rush to add things that you are not going to need soon or ever. As you have seen many of the above combinations introduce additional complexity and require more maintenance effort. Keep it simple but have in mind how to solve a problem when it arise.
The only exception to the above rule may be the case where you are building an API that will be distributed for external consumption. Maybe then providing an API what won’t introduce breaking changes in the future is important.
Finally, some patterns such as the Adapter, Decorator or Proxy share similar implementations. Don’t get confused, sometimes the differences are subtle. The name of the pattern first of all shows the intention. The specific implementation is usually well-defined but comes second.
I hope you have found any of the above just a wee bit useful. Until the next time keep coding smart :-)