All topics
library
intermediate

Decorator Pattern & I/O Streams

Understand how Java I/O streams use the Decorator pattern and how to apply it in your own designs.

A decorator implements the same interface as the thing it wraps, and adds behaviour around it.

Decorator = gift wrapping. The gift (core object) stays the same. Each wrapping layer (decorator) adds something: tissue paper (buffering), a bow (compression), a card (logging). You can combine any layers in any order.

Key Concepts

1
    InputStream in = new BufferedInputStream(
                       new GZIPInputStream(
                         new FileInputStream("data.gz")));
2
Read that from the inside out.
    FileInputStream      reads raw bytes from the file
    GZIPInputStream      decompresses them
    BufferedInputStream  reads them in 8 KB chunks
3
Each layer is an InputStream, so each can wrap any other, and the caller sees one InputStream.
InputStream
4
What it replaces: a class per combination.
    BufferedFileInputStream
    GzipFileInputStream
    BufferedGzipFileInputStream
    BufferedEncryptedGzipFileInputStream ...
5
Three features need seven classes. Four need fifteen. Decoration needs four.
6
The structure.
    component            InputStream          the interface
    concrete component   FileInputStream      the real thing
    decorator            FilterInputStream    holds one, delegates
    concrete decorators  Buffered..., GZIP..., Cipher...
7
Writing your own is this shape:
    class LoggingRepo implements Repo {
      private final Repo inner;
      LoggingRepo(Repo inner) { this.inner = inner; }
8
      public Order find(String id) {
        log.info("find {}", id);
        return inner.find(id);         // delegate
      }
    }
9
    Repo repo = new LoggingRepo(new CachingRepo(new JdbcRepo(ds)));
10
Where it earns its place. Logging, caching, retrying, rate limiting, metrics and validation — each a concern you want to add without touching the class that does the work, and in whatever combination.
11
The complaint about Java I/O is fair. Three nested constructors to read a compressed file is verbose, and the order matters in ways that are not obvious — buffering outside compression is right, inside is nearly useless.
12
Which is why the convenience methods exist. Files.readString and Files.newBufferedReader assemble the usual stack for you. Build it by hand when the combination is unusual.
Files.readStringFiles.newBufferedReader
13
What the interviewer is probing.1. "Where do you see the Decorator pattern in the JDK?" Probing: recognition in a familiar API. Stalls: "Nowhere obvious." Moves up: java.io — new BufferedInputStream(new GZIPInputStream(new FileInputStream(f))), each layer an InputStream adding one behaviour.
14
2. "Why does order matter when nesting streams?" Probing: whether you understand what each layer does. Stalls: "It does not." Moves up: buffering outside decompression batches the compressed reads usefully; inside it buffers already-decompressed bytes and achieves little.
15
3. "What does Decorator avoid?" Probing: the combinatorial problem. Stalls: "Extra classes." Moves up: a class per combination — three features would need seven subclasses, four would need fifteen.
16
4. "How would you apply it outside I/O?" Probing: transfer to your own code. Stalls: "It is an I/O thing." Moves up: wrapping a repository or client to add logging, caching, retries or metrics, in any combination, without touching the class doing the work.