Saturday, October 17, 2015

Delivering Highly Configurable Libraries Using Dependency Injection



In the last post, I described usage of a factory pattern to provide highly configurable functionality. An alternate approach would be the usage of dependency injection(DI). At the time I designed these libraries, dependency injection platforms on Android were not as mature as they are today. Guice was starting to gain some popularity, but uses reflection and can impact runtime performance. With the advent of Dagger2, we have access to a static dependency injection solution that does not rely on run-time binding. 

Using Dagger2, I could replace the previous abstract factory pattern. I decided to explore this solution as an exercise. Note that I did not actually implement and compile this solution and humans are notoriously lousy compilers, so it's possible (likely) there are errors. However, it should illustrate the general approach.  This also assumes that you are generally familiar with usage of Dagger2.



package com.nuance.androidcore.base.unpublished.util;
@Module
public final class ApplicationDispatchHandlerDataModule {
@Provides @Singleton
AppCommandHandlerBase provideAlarmAppHandler() {
return new AlarmAppHandler();
}
@Provides @Singleton
AppCommandHandlerBase provideCalendarAppHandlerHandler() {
return new CalendarAppHandler();
}
@Provides @Singleton
AppCommandHandlerBase provideVoiceDialHandlerAppHandler() {
return new VoiceDialHandler();
}
//...
}
import com.nuance.androidcore.base.unpublished.util.AppCommandType;
import java.util.EnumSet;
public class ApplicationDispatchHandlerDaggerSample extends CommandHandlerBase {
//CommandHandlerBase is the base class of the commands we want to execute
//default handlers could now simply be renamed "handlers", but maintaining naming
//for comparison to previous approach
private final static Map<AppCommandType, CommandHandlerBase> defaultHandlers =
new HashMap<AppCommandType, CommandHandlerBase>();
//override handlers no longer necessary
// private final static Map<AppCommandType, Class<? extends CommandHandlerBase>> overrideHandlers =
// new HashMap<AppCommandType, Class<? extends CommandHandlerBase>>();
@Inject private AlarmAppHandler;
@Inject private CalendarAppHandler;
@Inject private VoiceDialHandler;
static {
Graph.Initializer.init();
registerHandlers();
}
// TO override default behavior, register your own graph and module, then
// call this method
public static void registerHandlers() {
registerDefaultHandler(AppCommandType.ALARM, AlarmAppHandler);
registerDefaultHandler(AppCommandType.CALENDAR, CalendarAppHandler);
registerDefaultHandler(AppCommandType.CALLING, VoiceDialHandler);
//...
}
private static void registerDefaultHandler(AppCommandType key, CommandHandlerBase handler) {
defaultHandlers.put(key, handler);
}
public static void overrideHandlers()
// public static void registerHandler(AppCommandType key, Class<? extends CommandHandlerBase> handler) {
// overrideHandlers.put(key, handler);
// }
//Can no longer unregister overrides!
// public static void unregisterHandler(AppCommandType key) {
// overrideHandlers.remove(key);
// }
//
// public static void unregisterAll() {
// overrideHandlers.clear();
// }
@Override
public boolean execute(String appTypeString) {
//...
AppCommandType appType = getAppType(appTypeString);
if (appType == null) {
log.error("No AppType found for action: " + appTypeString);
return false;
}
CommandHandlerBase handler = getHandler(appType);
if (handler != null) {
try {
handler.setListener(getListener());
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
// operate on the handler
//...
}
private AppCommandType getAppType(String appTypeString) {
AppCommandType appType = null;
try {
appType = new AppCommandType(appTypeString);
} catch (IllegalArgumentException e) {
log.error("App name '" + action.getApp() + "' is not supported");
return null;
}
return appType;
}
private static CommandHandlerBase getHandler(AppCommandType appType) {
//CommandHandlerBase not shown here
return defaultHandlers.get(appType);
}
}
package com.nuance.androidcore.base.unpublished.util;
@Singleton
@Component(modules = {ApplicationDispatchHandlerDataModule.class})
public interface Graph {
void inject(ApplicationDispatchHandlerDaggerSample dispatcher);
public final static class Initializer {
public static Graph init() {
return DaggerGraph.builder()
.dataModule(new ApplicationDispatchHandlerDataModule())
.build();
}
}
}
view raw Graph.java hosted with ❤ by GitHub
As commented in the code. To override this behavior, the user could provide their own graph and data module and call ApplicationDispatchHandlerDaggerSample.registerHandlers() at runtime.

While some might argue that the DI solution with Dagger2 is cleaner, it would impose an additional constraint on each user of the NAC API to download and learn its usage. This is a relatively minor cost, but you could make the argument that in the interest of keeping NAC as easy to use as possible, the Dagger2 approach would add complexity with no real end-user benefit. A bigger issue with this approach is that, since we don't know which modules the user will want to  inject a-priori, we inject them all. This means that a user would have to specify handlers from within the NAC library, which  the user should not even need to know about, and which we have been keeping private and obfuscated. There are probably ways we could fix this by exposing methods to get the default implementation for a class, but this seems to just add more unintentional complexity. My conclusion is that, while DI is an important and highly useful pattern, for  this  specific use-case, the pattern we chose was an overall better approach.