Monday, June 8, 2009

dependency injection in 100 lines (or less)

If you intend to develop software in a modular fashion, at some point you will need to swap out one implementation of an interface (or service) for another. Now, the naive approach, to simply find and replace will work for a while. However, at some point you will have too many instances or simply tire of switching back and forth (with all the recompiling that entails, I'm looking at you Java).

Dependency injection is a type of inversion of control (see Fowler), which means that instead of creating dependencies by having the code you write reference other code (either that you also wrote, or in libraries) directly at compile time, you switch things up and let someone else (the injector) determine how to satisfy your requirements at runtime.

For my object oriented java class we do a largish (for a class anyway) software project. For example, past projects have included peer to peer file sharing systems, web application server, and email gui's. The point is, something that is too big for a single person or group to complete in the time allotted and something that can be easily split into smaller pieces. We design the solution as a class, write the interfaces, and then each student (or sometimes group) is responsible for implementing one component of the complete system.

Of course, I supply bytecode solutions (but not the source) for all the components and mock classes for testing. And finally, I needed an easy way to sometimes use my implementations, sometimes use my mockups, and other times use my students solutions. So, given that background, you will find my solution below.

It is not an enterprise IoC container and it has only been tested in a classroom environment (it may very well cause your computer to burst into flames, you have been warned). However, it might serve as an instructive example precisely because, it is short and to the point.

import java.io.*;
import java.lang.reflect.*;
import java.util.*;

public class ObjectFactory
{
    private static String CONFIG = "ObjectFactory.properties";
    private static Properties config = new Properties();

    public static <T> T create(Class<T> cls)
    {
        Class rcls;
        Constructor<T> cn = null;
        Class<?>[] pt = null;
        Object[] params = null;
        T inst = null;
        String name, real;

        if (config.isEmpty()) {
            try {
                InputStream in = ClassLoader.getSystemResourceAsStream(CONFIG);
                if (in == null)
                    throw except("file not found");
                config.load(in);
            } catch (IOException e) {
                throw except(e.toString());
            }
        }

        name = cls.getName();

        if (cls.isInterface() && !config.containsKey(name))
            throw except("no configured implementation for %s", name);

        real = (String)config.get(name);
        try {
            rcls = real != null ? Class.forName(real) : cls;

            for (Constructor<T> c : rcls.getConstructors()) {
                if (cn == null || c.getParameterTypes().length < pt.length) {
                    cn = c;
                    pt = c.getParameterTypes();
                }
            }

            params = new Object[pt.length];
            for (int i = 0; i < params.length; i++)
                params[i] = create(pt[i]);

            inst = cls.cast(cn.newInstance(params));
        } catch (InvocationTargetException e) {
            throw except("invocation failed for %s is %s, " +
                "but %s not a subclass of %s", name, real, real, name);
        } catch (ClassCastException e) {
            throw except("the configured implementation of %s is %s, " +
                "but %s not a subclass of %s", name, real, real, name);
        } catch (ClassNotFoundException e) {
            throw except("the configured implementation of %s is %s, " +
                "but %s cannot be found", name, real, real);
        } catch (InstantiationException e) {
            throw except("the configured implementation of %s is %s, " +
                "but a new %s cannot be created", name, real, real);
        } catch (IllegalAccessException e) {
            throw except("the configured implementation of %s is %s, " +
                "but a new %s cannot be created", name, real, real);
        }

        return inst;
    }

    private static RuntimeException except(String msg, Object ... args)
    {
        return new RuntimeException(String.format(
            "configuration error in %s: " + msg, CONFIG, args));
    }
}

Configuration is simple (it's a standard Java properties file), just a file named ObjectFactory.properties, located via the classpath. You just map an interface or abstract class to its concrete implementation. See the example below.

Message=MockMessage
Folder=MockFolder

So there you have it, 76 lines, probably could use some comments (and still be under 100). On the other hand, I probably could have squeezed it more too, but this version has pretty reasonable errors.

No comments: