This is not a common problem. The issue you need to address is the expectation of your interface. You're combining the behavior of a non-side effect interface with an interface that allows side effects.
Consider this:
public class CommandMonitor {
public static void main(String[] args) {
Command<?> sec = new SideEffectCommand();
reportResults(sec);
}
public static void reportResults(Command<?> cmd){
final Object results = cmd.execute("arg");
if (results != null ) {
System.out.println(results.getClass());
}
}
}
There is nothing wrong with using <Void>
as the template type but allowing it to mix with implementations for "Command<T>
" means some clients of the interface may not expect a void result. Without changing the interface, you've allowed an implementation to create an unexpected result.
When we pass around sets of data using the Collection classes, my team agreed to never return null even though it's fine syntactically. The problem was that classes that used the return values would constantly have to check for a void to prevent a NPE. Using the code above, you would see this everywhere:
if (results != null ){
Because there is now way to know whether the implementation actually has an object or is null. For a specific case, sure you'd know because your familiar with the implementation. But as soon as you start to aggregate them or they pass beyond your coding horizon (used as a library, future maintenance, etc.) the null problem will present itself.
Next, I tried this:
public class SideEffectCommand implements Command<String> {
@Override
public String execute(String... args) {
return "Side Effect";
}
}
public class NoSideEffectCommand implements Command<Void>{
@Override
public Void execute(String... args) {
return null;
}
}
public class CommandMonitor {
public static void main(String[] args) {
Command<?> sec = new SideEffectCommand();
Command<?> nsec = new NoSideEffectCommand();
reportResults(sec.execute("args"));
reportResults(nsec.execute("args")); //Problem Child
}
public static void reportResults(Object results){
System.out.println(results.getClass());
}
public static void reportResults(Void results){
System.out.println("Got nothing for you.");
}
}
Overloading didn't work because the second call to reportResults still called the version expecting an Object (of course). I contemplated changing to
public static void reportResults(String results){
But this illustrated the root of the problem, your client code starts having to know implementation details. Interfaces are supposed to help isolate code dependencies when possible. To add them at this point seems like bad design.
The bottom line is you need to use a design which makes clear when you expect a command to have a side effect and to think through how you would handle a set of commands, i.e. an array of unknown commands.
This may be a case of leaky abstractions.