Adding functionalities without changing existing code in Java: practical example

The decorator design pattern allows to add behaviour to an object without changing it, by implementing the behaviour in a new object that “wraps” the original one without extending it. Clear advantages are keeping the logic in separate classes, being able to compose them and being able to unit-test them in details.

Let’s take for example a basic URL download functionality. I just want to download an URL from a server and get the content, but I also want to cache the result, and also have a functionality to try again if the network was temporary unavailable.

Placing all this logic into a single class looks like the quickest thing to do, but it’s not recommended, you’ll have a big class doing too much, difficult to change. I’ll show you how to develop it in steps using this pattern. I’ll code in Java, but it’ll basically be the same concept in any other OOP language like PHP, Python, Typescript.

Implementation

package io.github.elvisciotti.Downloader;

import java.io.IOException;
import java.net.URL;

public interface DownloaderInterface {
String getContent(URL url) throws IOException;
}

and provide an implementation using native libraries:


package io.github.elvisciotti.Downloader;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;

public class NativeDownloader implements DownloaderInterface {

@Override
public String getContent(URL url) throws IOException {
InputStreamReader isr = new InputStreamReader(url.openStream());
BufferedReader br = new BufferedReader(isr);

StringBuffer content = new StringBuffer();
String inputLine;
while ((inputLine = br.readLine()) != null) {
content.append(inputLine);
}
br.close();

return content.toString();
}
}

Simple. This class does one thing, pure native downloading, nothing else. It’ll always stay the same and it’ll be easy to reuse in the future, and also to unit-test.

Adding “cached downloading” functionality

We are not going to add the caching functionality by wrapping a DownloaderInterface implementation and add the caching behaviour.

Let’s define an interface for caching generic content:

package io.github.elvisciotti.Cache;

public interface CacheInterface {
boolean exists(String key);
String get(String key);
void store(String key, String data);
}

and one implementation (e.g. in memory) using HashMap:

package io.github.elvisciotti.Cache;

import java.util.HashMap;

public class MemoryCache implements CacheInterface {
private static HashMap<String, String> cache = new HashMap<>();

@Override
public boolean exists(String key) {
return cache.get(key) != null;
}

@Override
public String get(String key) {
return cache.get(key);
}

@Override
public void store(String key, String data) {
cache.put(key, data);
}
}

Our cached downloader can now be implemented decorating any downloader, and using the cache implementation:

package io.github.elvisciotti.Downloader;

import io.github.elvisciotti.Cache.CacheInterface;

import java.io.*;
import java.net.URL;
import java.security.*;

public class DownloaderCached implements DownloaderInterface {
private DownloaderInterface innerDownloader;
private CacheInterface cache;

private static MessageDigest messageDigestSha1 = null;
static {
try {
messageDigestSha1 = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}

public DownloaderCached(DownloaderInterface innerDownloader, CacheInterface cache) {
this.innerDownloader = innerDownloader;
this.cache = cache;
}

public String getContent(URL url) throws IOException {
String cacheId = new String(messageDigestSha1.digest(url.toString().getBytes()));
if (this.cache.exists(cacheId)) {
String cachedContent = this.cache.get(cacheId);
if (cachedContent.length() > 0) {
return cachedContent;
}
}

String freshContent = innerDownloader.getContent(url);
this.cache.store(cacheId, freshContent);

return freshContent;
}
}

Adding “try again” functionality

I now want to have a functionality to re-attempt the download in case of temporary network failures. I’m then creating another a decorator:

package io.github.elvisciotti.Downloader;

import java.io.IOException;
import java.net.URL;

public class DownloaderTryAgain implements DownloaderInterface {
private DownloaderInterface innerDownloader;
private int maxAttempts;

public DownloaderTryAgain(DownloaderInterface innerDownloader, int maxAttempts) {
this.innerDownloader = innerDownloader;
this.maxAttempts = maxAttempts;
}

public DownloaderTryAgain(DownloaderInterface innerDownloader) {
this(innerDownloader, 3);
}

public String getContent(URL url) throws IOException {
StringBuilder errorMessage = new StringBuilder();
for (int i = 1; i <= maxAttempts; i++) {
try {
return this.innerDownloader.getContent(url);
} catch (IOException e) {
errorMessage.append("Failed to read URL at attempt #" + i + " attempts. " + e.getMessage() + ";");
}
}

throw new IOException(errorMessage.toString());
}
}

Use and combine all the functionality

To summarize, we now have 3 downloaders:

  • NativeDownloader: to read an URL content from the network;
  • DownloaderCached (decorator): to store successful content into a storage by URL;
  • DownloaderTryAgain (decorator): to re-attemp the download in case of failure;

Without changing any of those classes, I can now combine those functionalities, creating a downloader that simply download, or that caches on disk, or caches first in memory and then in the disk (for subsequent launches) and re-attempts downloads in case of failure as in the following example:

CacheInterface cacheMemory = new MemoryCache();
// DiskCache is another CacheInterface implementation using the disk
CacheInterface cacheFile = new DiskCache("/tmp/downloader-");

DownloaderInterface downloader = new NativeDownloader();
downloader = new DownloaderTryAgain(downloader);
downloader = new DownloaderCached(downloader, cacheFile);
downloader = new DownloaderCached(downloader, cacheMemory);

Note that changing the behaviour is as simple as changing one line (or config in your DI) without changing nor recompiling anything else. I can also replace the caching implementation (e.g. distributed Redis) by implementing the interface and without touching any other class. This is good design also following the SOLID principles that applies on this context:

  • Single responsability: no class implements two things, download and caching are declared in interfaces and implemented in different places;
  • Open/Close: enforced by the decorator pattern that enforces to add behaviour by adding code;
  • Interface segregation: note how caching is done in one interface, and not as part of the DownloaderCached. That allowed to add DiskCached downloader without making changes on that class;
  • Dependency inversion: note how methods argument accept interfaces and call their functionalities (internal wrapped downloader definition or cache definition) without knowing what’s their actual implementation.

Thanks for reading, clap if useful.

Software Engineer @ London

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store