Skip to content
On this page

Template Method

Defines the skeleton of an algorithm in an operation, deferring some steps to subclasses so that subclasses can redefine certain specific steps of an algorithm without changing its structure.

The Template Method pattern is a relatively simple design pattern. Its main idea is to define a series of steps for an operation, leaving certain steps that are not yet determined for the subclasses to implement. This way, different subclasses can define different steps.

Thus, the core of the Template Method is to define a "skeleton." Let’s illustrate this with an example.

Suppose we have developed a class for reading settings from a database:

java
public class Setting {
    public final String getSetting(String key) {
        String value = readFromDatabase(key);
        return value;
    }

    private String readFromDatabase(String key) {
        // TODO: Read from database
    }
}

Since reading data from the database can be slow, we might consider caching the settings, so that the next time we read the same key, we don’t have to access the database again. However, we haven't yet figured out how to implement the caching. Nonetheless, we can still write the caching code:

java
public class Setting {
    public final String getSetting(String key) {
        // First read from cache:
        String value = lookupCache(key);
        if (value == null) {
            // Not found in cache, read from the database:
            value = readFromDatabase(key);
            System.out.println("[DEBUG] load from db: " + key + " = " + value);
            // Put into cache:
            putIntoCache(key, value);
        } else {
            System.out.println("[DEBUG] load from cache: " + key + " = " + value);
        }
        return value;
    }
}

The overall process seems fine, but the methods lookupCache(key) and putIntoCache(key, value) are not implemented. How can we compile this? No worries, we can declare these as abstract methods:

java
public abstract class AbstractSetting {
    public final String getSetting(String key) {
        String value = lookupCache(key);
        if (value == null) {
            value = readFromDatabase(key);
            putIntoCache(key, value);
        }
        return value;
    }

    protected abstract String lookupCache(String key);

    protected abstract void putIntoCache(String key, String value);
}

Since we declared the abstract methods, the class must also be declared abstract. The implementation of lookupCache(key) and putIntoCache(key, value) is left to subclasses. The subclasses don’t need to worry about the core logic of getSetting(key); they only need to focus on completing those two smaller sub-tasks.

Suppose we want to use a Map for caching, we can create a LocalSetting class:

java
public class LocalSetting extends AbstractSetting {
    private Map<String, String> cache = new HashMap<>();

    protected String lookupCache(String key) {
        return cache.get(key);
    }

    protected void putIntoCache(String key, String value) {
        cache.put(key, value);
    }
}

If we want to use Redis for caching, we can create a RedisSetting class:

java
public class RedisSetting extends AbstractSetting {
    private RedisClient client = RedisClient.create("redis://localhost:6379");

    protected String lookupCache(String key) {
        try (StatefulRedisConnection<String, String> connection = client.connect()) {
            RedisCommands<String, String> commands = connection.sync();
            return commands.get(key);
        }
    }

    protected void putIntoCache(String key, String value) {
        try (StatefulRedisConnection<String, String> connection = client.connect()) {
            RedisCommands<String, String> commands = connection.sync();
            commands.set(key, value);
        }
    }
}

The client code using local caching would look like this:

java
AbstractSetting setting1 = new LocalSetting();
System.out.println("test = " + setting1.getSetting("test"));
System.out.println("test = " + setting1.getSetting("test"));

To switch to Redis caching, you only need to replace LocalSetting with RedisSetting:

java
AbstractSetting setting2 = new RedisSetting();
System.out.println("autosave = " + setting2.getSetting("autosave"));
System.out.println("autosave = " + setting2.getSetting("autosave"));

This demonstrates that the core idea of the Template Method pattern is: the parent class defines the skeleton, while subclasses implement certain details.

To prevent subclasses from overriding the skeleton method in the parent class, you can declare the skeleton method as final in the parent class. For abstract methods that require implementation by subclasses, it is common to declare them as protected to make these methods invisible to external clients.

The Java standard library also has many applications of the Template Method pattern. In collection classes, both AbstractList and AbstractQueuedSynchronizer define many common operations, and subclasses only need to implement certain necessary methods.

Practice

Use the Template Method pattern to create a subclass that uses Guava Cache.

Considerations

  1. Is it possible to make readFromDatabase() a template method, allowing subclasses to choose between reading from a database or a file?
  2. If we can extend both the caching mechanism and the underlying storage, could this lead to an explosion of subclass numbers? How would you resolve this?

Summary

The Template Method pattern is a design pattern that defines a high-level structure while allowing for lower-level implementation details to vary. It is suitable for scenarios where the overall process is fixed, but certain steps are uncertain or interchangeable.

Template Method has loaded