Prototype design pattern in java

In this article I’ll give you an introduction (with Java examples) into the prototype design pattern.

What is the Prototype design pattern about?

This pattern is a creational pattern (just as the already introduced Factory Pattern) and it comes to play where performance matters. This pattern is used when creating a new object is costly: you use a prototype and extend it with the particular implementations of the needed object.

The best use-case for this pattern if an object is created after a costly database operation: you get all the data with the query (or queries) and you can use this information later to have new objects populated. With this approach you can reduce the number of database operations and save some I/O time for your application.

And to have one word in comparison with the introduced Factory Patter: the Factory Pattern is creation through inheritance, the Prototype Pattern is creation through delegation.

How to apply the pattern?

Well, if you use Java since some time and apply the Object-Oriented patterns you already know this pattern: it comes with abstraction and polymorphism.

OK, actually this is only the base of the pattern, you need some more things to do. Here I’ll will give you an insight on my best practices how to apply this pattern.

Have a clone method

For this define a base class (I suggest it to be abstract but this may vary depending on your needs) and it should implement the java.lang.Cloneable interface — which only indicates that the Object.clone() method is a valid operation on this type of object (this means that the Cloneable interface does not have any methods defined).

/**
 * Base abstract class for the Prototype Pattern.
 *
 * @author GHajba
 *
 */
public abstract class App implements Cloneable {

    private AppType type;

    public abstract void develop();

    public void test() {
    }

    public void debug() {
    }

    public void deliver() {
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public AppType getType() {
        return this.type;
    }

    protected void setType(AppType type) {
        this.type = type;
    }
}

After the abstract class is ready, let's create some implementations.

/**
 * @author GHajba
 *
 */
public class IOSApp extends App {

    public IOSApp() {
        System.out.println("Creating an iOS App");
        setType(AppType.IOS);
    }

    @Override
    public void develop() {
        System.out.println("Developing an iOS App");
    }

}

/**
 * @author GHajba
 *
 */
public class WatchApp extends App {

    public WatchApp() {
        System.out.println("Creating a watch app");
        setType(AppType.WATCH);
    }

    @Override
    public void develop() {
        System.out.println("Developing a Watch App");
    }

}

Have a cache of prototypes

Because creating an App costs time (because we load it from the database) let’s create a cache to store those Apps.

The best choice for the cache in my opinion is a java.util.HashMap because it has a very fast (constant) time value for the basic operations: put and get.

/**
 * This is the cache where we store the Apps which were loaded from the "database".
 *
 * @author GHajba
 *
 */
public class BasicAppCache {
    Map<AppType, App> appCache = new HashMap<>();

    /**
     * Loads all available types of the application and puts them into the cache.
     */
    public void load() {
        System.out.println("Loading App of type " + AppType.IOS.name());
        this.appCache.put(AppType.IOS, new IOSApp());
        System.out.println("Loading App of type " + AppType.WATCH.name());
        this.appCache.put(AppType.WATCH, new WatchApp());
    }

    /**
     * Gets the app from the cache based on its type
     */
    public App get(AppType type) {
        System.out.println("Getting App of type " + type.name());
        final App app = this.appCache.get(type);
        if (app != null) {
            try {
                return (App) app.clone();
            } catch (final CloneNotSupportedException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

This cache cam have a load method which can initially load all the known objects from the database and add it to the cache. For such a small example this is a good approach however in real-life scenarios it should handle each request and decide on the contents of the cache if it should be loaded or just replicated from the cache.

/**
 * This is the cache where we store the Apps which were loaded from the "database".
 *
 * @author GHajba
 *
 */
public class AdvancedAppCache {
    Map<AppType, App> appCache = new HashMap<>();

    private App load(AppType type) {
        System.out.println("Loading App of type " + type.name());
        switch (type) {
        case IOS:
            final App app = new IOSApp();
            this.appCache.put(type, app);
            return app;
        case WATCH:
            final App watchApp = new WatchApp();
            this.appCache.put(type, watchApp);
            return watchApp;
        default:
            return null;
        }

    }

    public App get(AppType type) {
        System.out.println("Getting App of type " + type.name());
        App app = this.appCache.get(type);
        // the type is not in the cache, let's load it
        if (app == null) {
            app = load(type);
        }
        try {
            // we have to cast the result of clone() to App because the default return type is Object
            return (App) app.clone();
        } catch (final CloneNotSupportedException e) {
            // a nasty exception, let's just ignore it
            e.printStackTrace();
        }
        // if everything fails, return simply null
        return null;
    }
}

As you can see, the cache determines if the object is already loaded or not and based on this information it loads and adds it to the cache or just returns a new clone from the already loaded object.

Have a factory

We already learned about the Factory Patterns which defers the instantiation of the classes. This means that you do not need to create every object you need with new and you do not need to know the concrete implementation of a given interface or abstract class you want to use.

/**
 * This factory is used to create a new Application.
 *
 * It utilizes the BasicAppCache.
 *
 * @author GHajba
 *
 */
public class AppFactory {
    BasicAppCache appCache;

    public AppFactory() {
        this.appCache = new BasicAppCache();
        this.appCache.load();
    }

    public App createApp(AppType type) {
        return this.appCache.get(type);
    }
}

In this example the Factory calls the cache and does not want to know about how the cache deals with object creation.

Use the factory!

Now it is time to use the factory in our simple application. Here we will create some apps and display what is happening. And instead of creating ever instance with new we use the previously defined factory.

/**
 * This is the main entry point of the application.
 *
 * @author GHajba
 *
 */
public class AppStore {

    public void orderApp(AppType type) {
        final App app = new AppFactory().createApp(type);
        app.develop();
        app.test();
        app.debug();
        app.deliver();
    }

    public static void main(String... args) {
        new AppStore().orderApp(AppType.WATCH);
    }
}

And here is an output of the previous application running:

Loading App of type IOS
Creating an iOS App
Loading App of type WATCH
Creating a watch app
Getting App of type WATCH
Developing a Watch App

We could use the cache itself instead of the factory in the example above but in this case you have to know that there is a cache behind the scenes. If you later decide that you do not need a cache and won’t use the prototype pattern anymore then you have to rework all the places where you create new apps.

However I’ll include the use case where we can use the caches directly. An alternative would be to have a CacheFactory and hide the implementations of the cache but this article is not about the Factory Pattern.

public static void main(String... args) {
    final BasicAppCache basicCache = new BasicAppCache();
    basicCache.load();
    for (final AppType t : AppType.values()) {
        basicCache.get(t);
    }
    for (final AppType t : AppType.values()) {
        basicCache.get(t);
    }

    System.out.println("------");

    final AdvancedAppCache advancedCache = new AdvancedAppCache();
    for (final AppType t : AppType.values()) {
        advancedCache.get(t);
    }
    for (final AppType t : AppType.values()) {
        advancedCache.get(t);
    }
}

As you can see we use both caches two times to see how they work and here is the output:

Loading App of type IOS
Creating an iOS App
Loading App of type WATCH
Creating a watch app
Getting App of type IOS
Getting App of type WATCH
Getting App of type IOS
Getting App of type WATCH
------
Getting App of type IOS
Loading App of type IOS
Creating an iOS App
Getting App of type WATCH
Loading App of type WATCH
Creating a watch app
Getting App of type IOS
Getting App of type WATCH

As you can see, the BasicAppCache loads (creates) the objects when the load() method is called, the AdvancedAppCache loads the entries on demand. This means if you forget to call load() on the BasicAppCache you’ll end up with no Apps.

Conclusion

As mentioned in the introduction, the Prototype Pattern is best used with creation through delegation. It can be used with the Factory Pattern (using a Factory) to hide the implementation details from the user of our service but it is not a must, you can access the class caching the prototypes too.

For an application in production I suggest to use the AdvancedAppCache solution where entries are loaded on-demand. It is nicer, more performant at the beginning (giving a bit of slow performance if a prototype is not in the cache) but you can start faster and make your users happier — not to mention the call of load() when using the BasicAppCache which can lead to unexpected errors.

<< Adapter design pattern in java Facade Design Pattern >>

Leave A Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.