The source code of design patterns can be viewed and downloaded here :
https://github.com/ebundy/java-examples/tree/master/design-patterns
What is it ?
The decorator design pattern is a structural pattern.
It allows to design a class to decorate another class in a dynamic way.
Decorator is not an intrusive pattern. Indeed, the decorator class conforms to the class that it decorates. The decorator class can do, according its needs, a processing before and or after the operation of the class that it decorates but it is also responsible to call the original operation of the decorated class. Somehow, it adds a value for the method it decorates in a transparent way for the decorated class and clients that use the decorated class.
When using it ?
– You want to augment or diminish a behavior of an existing class without being invasive for this class : no needed modification.
– You want to add or remove dynamically a behavior to a class and more precisely to an operation of it.
– Your domain allows a mix of many independent responsibilities and you don’t want to create subclasses to cover all possible cases.
Our use case for decorators : an utility to do treatments on documents
We will build a utility to do some treatments on a document.
We want to be able to create a document in memory (relying on a simple String representation) on which we could apply compression and encryption treatments.
Reversely, from the transformed string document in memory, we want to be able to do the reverse operations in order to retrieve the original String document.
But we want a flexible solution which allows clients to choose the compression methods (zip, tar, etc…) and the encryption methods (simple, aes, etc…).
Without decorator usage, the number of subclasses to cover all cases would explode and would be hard to maintain. With decorators, things are simpler.
For the sake of simplicity, we cover a weak encryption method (base 64) and two compression methods (zip and tar).
In our example, we will have two families of decorators. Indeed, base contract and decorators for creating the string document in memory is not the same that base contract and decorators for retrieving the original string document.
I propose to do an full example which covers doing and undoing treatments on our string document because it allows to validate naturally that our decorators work as expected. Besides, it makes things more interesting and funny to go further in the example of the decorator pattern.
In real applications, not having unit tests is not really acceptable because it would be unsafe to update and refactor our implementation. So, we will validate our pattern implementation with unit-testing.
Before presenting the implementation of the pattern, we present a conceptual view of that.
It allows to understand the conceptual intent of the pattern. Implementation considerations are handled just after.
Conceptual view of our decorator pattern for writing document
Conceptual view of our decorator pattern for reading document
We will see that the implementation of the pattern is very near of the conceptual view.
We provide only a few additional methods.
Document Writer components
Base contract for writing documents must be the simplest possible.
More simple is the contract, more easy is the integration of decorators.
Decorators which refine document writing must have a way to get last data handled by the writing chain. The decorator doesn’t need to know and should not know the whole decorating chain. It just needs to know the element that it decorates and more precisely its content. That’s why, we introduce the getBytes() methods in the base interface IDocumentOutput.
As explained previously, a decorator must conform to the domain element that it decorates. So, writeInMemory(), the core method of DocumentOuput must be implemented by decorators too.
IDocumentOutput
package davidhxxx.teach.designpattern.decorator.output; public interface IDocumentOutput { void writeInMemory(); byte[] getBytes(); } |
Our domain class,DocumentOutput, is simple. It has a constructor to store the string representing the document to create. The writeInMemory() method converts the String into bytes by using the UTF-8 encoding. We could have moved the encoding process in decorators in order to allow clients to choose their encoding. Nevertheless, it’s an example and not a real application. So, we must limit our scope.
DocumentOutput
package davidhxxx.teach.designpattern.decorator.output; import java.nio.charset.StandardCharsets; public class DocumentOutput implements IDocumentOutput { private String content; private byte[] bytes; public DocumentOutput(String content) { this.content = content; } public void writeInMemory() { bytes = content.getBytes(StandardCharsets.UTF_8); } public byte[] getBytes() { return bytes; } } |
AbstractDocumentOutputDecorator is the base class for concrete document writer decorators.
It’s not essential because it doesn’t contain behaviors. Nevertheless, it contains common data to subclasses and the super constructor. Which allows to ease the development of our concrete subclasses.
AbstractDocumentOutputDecorator
package davidhxxx.teach.designpattern.decorator.output; public abstract class AbstractDocumentOutputDecorator implements IDocumentOutput{ protected IDocumentOutput document; protected byte[] bytes; public AbstractDocumentOutputDecorator(IDocumentOutput document) { this.document=document; } public byte[] getBytes() { return bytes; } } |
In concrete decorators, the way that writeInMemory() is implemented is important.
First, it must delegate to the writeInMemory() of the decorated instance.
It’s the case for all our decorators. Our decorators are indeed post-processing decorators.
Each decorator needs the result of its decorated document to do its processing which itself needs the result of its decorated document, etc…
It means that the most inner decorator does the first processing on the document writing and the most outer decorator does the last processing on the document writing.
SecuredDocumentOutputDecorator illustrates the execution order of the decorator and the decorated classes.
First, it delegates to the writeInMemory() of the decorated instance.
So, the decorated instance has finished its processing when the private method processSecuring() of SecuredDocumentOutputDecorator is called. Finally, processSecuring() applies securing decoration on the content provided by its decorated object (document.getBytes()).
SecuredDocumentOutputDecorator
package davidhxxx.teach.designpattern.decorator.output; import org.apache.commons.codec.binary.Base64; public class SecuredDocumentOutputDecorator extends AbstractDocumentOutputDecorator { public SecuredDocumentOutputDecorator(IDocumentOutput document) { super(document); } public void writeInMemory() { document.writeInMemory(); processSecurizing(); } private void processSecurizing() { byte[] originalBytes = document.getBytes(); bytes = Base64.encodeBase64(originalBytes); } } |
The following decorators show the same decorator mechanism as which one just explained.
TarCompressedDocumentOutputDecorator
package davidhxxx.teach.designpattern.decorator.output; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.OutputStream; import org.apache.commons.compress.archivers.ArchiveOutputStream; import org.apache.commons.compress.archivers.ArchiveStreamFactory; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.utils.IOUtils; public class TarCompressedDocumentOutputDecorator extends AbstractDocumentOutputDecorator { public TarCompressedDocumentOutputDecorator(IDocumentOutput document) { super(document); } public void writeInMemory() { document.writeInMemory(); processCompressing(); } private void processCompressing() { OutputStream outputFile; try { byte[] bytesOriginal = document.getBytes(); final File tempFileForArchive = File.createTempFile("output", "tar"); outputFile = new FileOutputStream(tempFileForArchive); ArchiveOutputStream archive = new ArchiveStreamFactory().createArchiveOutputStream(ArchiveStreamFactory.TAR, outputFile); final TarArchiveEntry entry = new TarArchiveEntry("doc"); entry.setSize(bytesOriginal.length); archive.putArchiveEntry(entry); ByteArrayInputStream inputStream = new ByteArrayInputStream(bytesOriginal); IOUtils.copy(inputStream, archive); archive.closeArchiveEntry(); archive.finish(); outputFile.close(); FileInputStream fileInputStream = new FileInputStream(tempFileForArchive); bytes= IOUtils.toByteArray(fileInputStream); } catch (Exception e) { throw new RuntimeException(e); } } } |
ZipCompressedDocumentOutputDecorator
package davidhxxx.teach.designpattern.decorator.output; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.OutputStream; import org.apache.commons.compress.archivers.ArchiveOutputStream; import org.apache.commons.compress.archivers.ArchiveStreamFactory; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.utils.IOUtils; public class ZipCompressedDocumentOutputDecorator extends AbstractDocumentOutputDecorator { public ZipCompressedDocumentOutputDecorator(IDocumentOutput document) { super(document); } public void writeInMemory() { document.writeInMemory(); processCompressing(); } private void processCompressing() { OutputStream outputFile; try { final File tempFileForArchive = File.createTempFile("output", "zip"); outputFile = new FileOutputStream(tempFileForArchive); ArchiveOutputStream archive = new ArchiveStreamFactory().createArchiveOutputStream(ArchiveStreamFactory.ZIP, outputFile); archive.putArchiveEntry(new ZipArchiveEntry("doc")); byte[] bytesOriginal = document.getBytes(); ByteArrayInputStream inputStream = new ByteArrayInputStream(bytesOriginal); IOUtils.copy(inputStream, archive); archive.closeArchiveEntry(); archive.finish(); outputFile.close(); FileInputStream fileInputStream = new FileInputStream(tempFileForArchive); bytes= IOUtils.toByteArray(fileInputStream); } catch (Exception e) { throw new RuntimeException(e); } } } |
Document reader components
Document reader components are symmetric to document writer components. They present very few variations between them.
As for writing documents, base contract for reading documents must be the simplest possible. More simple is the contract, more easy is the integration of decorators.
As for writing documents, decorators which refine documents reading must have a way to get last data handled by the reading chain. The decorator should not know the whole decorating chain. It just needs to know the element that it decorates and more precisely its content. That’s why, as for writing documents, we introduce the getBytes() methods in the base interface IDocumentInput.
As explained previously, a decorator must conform to the domain element that it decorates. So, read(), the core method of DocumentInput must be implemented by decorators too.
getStringContent() is an instance helper method which allows to get the bytes document in a String. It’s not essential for our example but it seems more practical to have a method to retrieve the original String from the inputDocument. Doing this process from client-side or from another class would give the feeling we miss something.
IDocumentInput
package davidhxxx.teach.designpattern.decorator.input; public interface IDocumentInput { void read(); byte[] getBytes(); String getStringContent(); } |
Our domain class, DocumentInput, is simple. It has a constructor to store the bytes representing a written document .
The read() method does nothing because DocumentInput is not able to do further.
It’s not symmetric with its homologue writeDocument() of DocumentOutput but it’s wished. DocumentOutput.writeDocument() must convert the string provided by the DocumentOutput’s constructor, to a byte array. In DocumentInput‘s constructor, it is helpless since we have already a byte array.
DocumentInput
package davidhxxx.teach.designpattern.decorator.input; package davidhxxx.teach.designpattern.decorator.input; import java.nio.charset.StandardCharsets; public class DocumentInput implements IDocumentInput { private String content; private byte[] bytes; public DocumentInput(byte[] bytes) { this.bytes = bytes; } public byte[] getBytes() { return bytes; } public void read() { } public final String getStringContent() { if (content == null) { content = new String(getBytes(), StandardCharsets.UTF_8); } return content; } } |
AbstractDocumentInputDecorator is the base class for concrete Document reader decorators.
It’s not essential because it doesn’t contain behaviors. Nevertheless, it contains common data and processing to subclasses. Which allows to ease the development of our concrete subclasses.
We can note that getStringContent() uses the same implementation that DocumentInput.
AbstractDocumentInputDecorator
package davidhxxx.teach.designpattern.decorator.input; package davidhxxx.teach.designpattern.decorator.input; import java.nio.charset.StandardCharsets; public abstract class AbstractDocumentInputDecorator implements IDocumentInput { protected IDocumentInput document; protected byte[] bytes; private String content; public AbstractDocumentInputDecorator(IDocumentInput document) { this.document = document; } public byte[] getBytes() { return bytes; } public final String getStringContent() { if (content == null) { content = new String(getBytes(), StandardCharsets.UTF_8); } return content; } } |
In concrete decorators, the way that read() is implemented is important.
We use the same logic as for writer decorators.
First, it must delegate to read() of the decorated instance.
Each decorator needs the result of its decorated document to do its processing which itself needs the result of its decorated document, etc…
As for writer decorators, the most inner decorator does the first processing on the document reading and the most outer decorator does the last processing on the document reading .
SecuredDocumentInputDecorator illustrates the execution order of the decorator and the decorated classes.
First, it delegates to the read() method of the decorated instance. The decorated instance has finished its processing when the private method processUnsecuring() of SecuredDocumentInputDecorator is called.
Finally, processUnsecuring() applies unsecuring decoration on the content provided by its decorated object (document.getBytes()).
SecuredDocumentInputDecorator
package davidhxxx.teach.designpattern.decorator.input; import org.apache.commons.codec.binary.Base64; public class SecuredDocumentInputDecorator extends AbstractDocumentInputDecorator { public SecuredDocumentInputDecorator(IDocumentInput document) { super(document); } @Override public void read() { document.read(); processUnsecuring(); } private void processUnsecuring() { byte[] originalBytes = document.getBytes(); bytes = Base64.decodeBase64(originalBytes); } } |
The following decorators show the same decorator mechanism as which one just explained.
TarCompressedDocumentInputDecorator
package davidhxxx.teach.designpattern.decorator.input; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import org.apache.commons.compress.archivers.ArchiveInputStream; import org.apache.commons.compress.archivers.ArchiveStreamFactory; import org.apache.commons.compress.utils.IOUtils; public class TarCompressedDocumentInputDecorator extends AbstractDocumentInputDecorator { public TarCompressedDocumentInputDecorator(IDocumentInput document) { super(document); } public void read() { document.read(); processUncompressing(); } private void processUncompressing() { try { final ByteArrayInputStream is = new ByteArrayInputStream(document.getBytes()); ArchiveInputStream archiveInput = new ArchiveStreamFactory().createArchiveInputStream(ArchiveStreamFactory.TAR, is); archiveInput.getNextEntry(); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); IOUtils.copy(archiveInput, byteArrayOutputStream); byteArrayOutputStream.close(); archiveInput.close(); is.close(); this.bytes = byteArrayOutputStream.toByteArray(); } catch (Exception e) { throw new RuntimeException(e); } } } |
ZipCompressedDocumentInputDecorator
package davidhxxx.teach.designpattern.decorator.input; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.ArchiveInputStream; import org.apache.commons.compress.archivers.ArchiveStreamFactory; import org.apache.commons.compress.utils.IOUtils; public class ZipCompressedDocumentInputDecorator extends AbstractDocumentInputDecorator { public ZipCompressedDocumentInputDecorator(IDocumentInput document) { super(document); } public void read() { document.read(); processUncompressing(); } private void processUncompressing() { try { final ByteArrayInputStream is = new ByteArrayInputStream(document.getBytes()); ArchiveInputStream archiveInput = new ArchiveStreamFactory().createArchiveInputStream(ArchiveStreamFactory.ZIP, is); ArchiveEntry nextEntry = archiveInput.getNextEntry(); if (nextEntry==null){ throw new IllegalArgumentException("not entry in the document input provided"); } ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); IOUtils.copy(archiveInput, byteArrayOutputStream); byteArrayOutputStream.close(); archiveInput.close(); is.close(); this.bytes = byteArrayOutputStream.toByteArray(); } catch (IllegalArgumentException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } } |
Unit tests
Now, we will create and run unit test to valid our developments.
We will check 5 things :
– assert that a document with no decorator can be written in memory and restored as the original.
– assert that a document with one couple of decorator (pkzip) can be written in memory and restored as the original.
– assert that a document with two couples of decorator (pkzip and securing) can be written in memory and restored as the original.
– assert that a document with three couples of decorators (pkzip, tar and securing) can be written in memory and restored as the original.
– assert that a decorator chaining gives the same results as each decorator applied separately one after the other one.
package davidhxxx.teach.designpattern.decorator; import org.junit.Test; import davidhxxx.teach.designpattern.decorator.input.DocumentInput; import davidhxxx.teach.designpattern.decorator.input.IDocumentInput; import davidhxxx.teach.designpattern.decorator.input.SecuredDocumentInputDecorator; import davidhxxx.teach.designpattern.decorator.input.TarCompressedDocumentInputDecorator; import davidhxxx.teach.designpattern.decorator.input.ZipCompressedDocumentInputDecorator; import davidhxxx.teach.designpattern.decorator.output.DocumentOutput; import davidhxxx.teach.designpattern.decorator.output.IDocumentOutput; import davidhxxx.teach.designpattern.decorator.output.SecuredDocumentOutputDecorator; import davidhxxx.teach.designpattern.decorator.output.TarCompressedDocumentOutputDecorator; import davidhxxx.teach.designpattern.decorator.output.ZipCompressedDocumentOutputDecorator; import junit.framework.Assert; public class DocumentDecoratorTest { private static final String MAGIC_HEADER_PKZIP = "PK"; @Test public void documentWithNoDecorator() throws Exception { final String contentToWrite = "Hello, who is here ?"; DocumentOutput document = new DocumentOutput(contentToWrite); // action document.writeInMemory(); // assertion, we can retrieve the original doc final byte[] bytesDocumentTransformed = document.getBytes(); DocumentInput documentInput = new DocumentInput(bytesDocumentTransformed); documentInput.read(); String actualContent = documentInput.getStringContent(); Assert.assertEquals(contentToWrite, actualContent); } @Test public void documentWithPkZipDecorator() throws Exception { final String contentToWrite = "Hello, who is here ?"; DocumentOutput document = new DocumentOutput(contentToWrite); IDocumentOutput documentDecorator = new ZipCompressedDocumentOutputDecorator(document); // action documentDecorator.writeInMemory(); // assertion final byte[] bytesDocumentTransformed = documentDecorator.getBytes(); // assertion, we have zip assertIsPkZipped(bytesDocumentTransformed); // assertion, we can retrieve the original doc DocumentInput documentInput = new DocumentInput(bytesDocumentTransformed); IDocumentInput documentInputDecorator = new ZipCompressedDocumentInputDecorator(documentInput); documentInputDecorator.read(); String actualContent = documentInputDecorator.getStringContent(); Assert.assertEquals(contentToWrite, actualContent); } @Test public void documentWithPkZipPlusSecuredDecorators() throws Exception { final String contentToWrite = "Hello, who is here ?"; DocumentOutput document = new DocumentOutput(contentToWrite); IDocumentOutput documentDecorator = new SecuredDocumentOutputDecorator(new ZipCompressedDocumentOutputDecorator(document)); // action documentDecorator.writeInMemory(); // assertion final byte[] bytesDocTransformed = documentDecorator.getBytes(); DocumentInput documentInputTransformed = new DocumentInput(bytesDocTransformed); // assert file not seen as a zip and cannot be uncompressed assertIsNotSeenAsPkzipedAndcannotBeUncompressed(bytesDocTransformed, documentInputTransformed); // assert that we retrievethe original file by unsecuring, then unzipping // assert ok by decorating one after the oher final SecuredDocumentInputDecorator securedDocumentDecorator = new SecuredDocumentInputDecorator(documentInputTransformed); securedDocumentDecorator.read(); byte[] bytesAfterUnsecurig = securedDocumentDecorator.getBytes(); assertIsPkZipped(bytesAfterUnsecurig); IDocumentInput unzippedDocumentDecorator = new ZipCompressedDocumentInputDecorator(new DocumentInput(bytesAfterUnsecurig)); unzippedDocumentDecorator.read(); String actualContent = unzippedDocumentDecorator.getStringContent(); Assert.assertEquals(contentToWrite, actualContent); // assert ok by doing a decorator chaining IDocumentInput chainingDecorators = new ZipCompressedDocumentInputDecorator(new SecuredDocumentInputDecorator(documentInputTransformed)); chainingDecorators.read(); actualContent = chainingDecorators.getStringContent(); Assert.assertEquals(contentToWrite, actualContent); } @Test public void documentWithPkZipPlusTarPlusSecuredDecorators() throws Exception { final String contentToWrite = "Hello, who is here ?"; DocumentOutput document = new DocumentOutput(contentToWrite); IDocumentOutput documentDecorator = new SecuredDocumentOutputDecorator(new ZipCompressedDocumentOutputDecorator(new TarCompressedDocumentOutputDecorator(document))); // action documentDecorator.writeInMemory(); // assertion DocumentInput documentInput = new DocumentInput(documentDecorator.getBytes()); IDocumentInput documentInputDecorator = new TarCompressedDocumentInputDecorator(new ZipCompressedDocumentInputDecorator(new SecuredDocumentInputDecorator(documentInput))); documentInputDecorator.read(); String actualContent = documentInputDecorator.getStringContent(); Assert.assertEquals(contentToWrite, actualContent); } private String readTwoFirstBytes(final byte[] bytesDocumentTransformed) { byte[] twoFirstBytes = new byte[2]; twoFirstBytes[0] = bytesDocumentTransformed[0]; twoFirstBytes[1] = bytesDocumentTransformed[1]; String twoFirstChar = new String(twoFirstBytes); return twoFirstChar; } private void assertIsPkZipped(final byte[] bytesDocumentTransformed) { final String twoFirstBytes = readTwoFirstBytes(bytesDocumentTransformed); Assert.assertEquals(MAGIC_HEADER_PKZIP, twoFirstBytes); } private void assertIsNotSeenAsPkzipedAndcannotBeUncompressed(final byte[] bytesDocTransformed, DocumentInput documentInputTransformed) { String twoFirstBytes = readTwoFirstBytes(bytesDocTransformed); Assert.assertTrue("must not seen as a zip since secured", !twoFirstBytes.equals(MAGIC_HEADER_PKZIP)); boolean isErrorWhenUnZip = false; IDocumentInput decoratorUncompressingButSecuringAgainPresents = new ZipCompressedDocumentInputDecorator(documentInputTransformed); try { decoratorUncompressingButSecuringAgainPresents.read(); } catch (IllegalArgumentException e) { isErrorWhenUnZip = true; } Assert.assertTrue("error is expected when unziping", isErrorWhenUnZip); } } |