Sunday, December 27, 2009

Replacing conditional logic

A conditional dispatcher is a conditional statement (such as a switch) that performs request routing and handling. For example:

public void doAction(String action) {
        if (action.equals(UPLOAD_FILE)) {
            System.out.println("uploading file");
        } else if (action.equals(SAVE_FILE)) {
            System.out.println("saving file");
        } else if (action.equals(DEL_FILE)) {
            System.out.println("deleting file");
        }
        //etc
 }



Another variation is to use "instanceof" and then do something depending on the type of the object.

The two most common reasons to refactor from a conditional dispatcher are the following:

  1. Not enough runtime flexibility: Clients that rely on the conditional dispatcher develop a need to dynamically configure it with new requests or handler logic. Yet the conditional dispatcher doesn't allow for such dynamic configurations because all of its routing and handling logic is hard-coded into a single conditional statement.
  2. A bloated body of code: Some conditional dispatchers become enormous as they evolve to handle new requests or as their handler logic becomes ever more complex with new responsibilities. Extracting the handler logic into different methods doesn't help enough because the class that contains the dispatcher and extracted handler methods is still too large to work with.

The Command pattern provides an excellent solution to such problems. To implement it, you simply place each piece of request-handling logic in a separate "command" class that has a common method, like execute() or run(), for executing its encapsulated handler logic. Once you have a family of such commands, you can use a collection to store and retrieve instances of them; add, remove, or change instances; and execute instances by invoking their execution methods.

...
public enum FileOps {UPLOAD_FILE, SAVE_FILE, DEL_FILE}

private Map handlers = new HashMap();

public CommandDriver() {
        createHandlers();
}

private void createHandlers() {
        handlers.put(FileOps.UPLOAD_FILE, new UploadHandler());
        handlers.put(FileOps.SAVE_FILE, new SaveHandler());
        handlers.put(FileOps.DEL_FILE, new DeleteHandler());
}

public void doAction(FileOps action) {
        Handler handler = handlers.get(action);
        handler.execute(null);
}
....

And the command defines the operation to execute and accepts a context object with the necessary data to do so:

public class UploadHandler implements Handler{
    public void execute(Map params) throws IOException {
        System.out.println("uploading file");
    }
}


Another alternative to replacing conditional logic is to use the Strategy pattern. This pattern has a particularly good synergy with IoC containers: it's the responsability of the container to inject the strategy to use. It is also easier to test using mocks and we can change the strategy at runtime.

public class StrategyDriver {

    private FileStrategy fileStrategy;

    public void setFileStrategy(FileStrategy fileStrategy) {
        this.fileStrategy = fileStrategy;
    }


    public void doAction(Map params) throws IOException {
       fileStrategy.handleFile(params);
    }

    public static void main(String[] args) throws IOException {
        new StrategyDriver().doAction(null);
    }
}

No comments:

Post a Comment