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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
//... | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
} |
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.
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.