Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pattern for sequence of steps with no common interface

In my java app I got a method that runs a long sequence of steps (synchronously) where one step result is the input for the next.

for example:

// Step 1
Map<String, SomeObject> objectsMap = someService.createObjectsMap();
if (!objectsMap.isEmpty()) {
    // Step 2
    AnotherObject anotherObject = anotherService.createAnotherObject(objectsMap);
    if (null != anotherObject) {
    // Step 3 that gets anotherObject as input and returns something else
    } else { // Step 2 failed
        // log and handle
    }
} else { // Step 1 failed
    // log and handle
}

And so I got this series of steps written in a series of if-else blocks. There is no common interface for the steps since each one has different signature. I've been loking on some different and tried to customize patterns such as chain-of-responsibility and command but could not get to a satisfied result.

I wonder if this ugly long if-else section is the way to go or is there a pattern out there that can help make this series of steps more clean and scalable.

like image 498
forhas Avatar asked Oct 08 '14 06:10

forhas


Video Answer


1 Answers

One question you must answer yourself is Why do I want to refactor my code?

Do you want it to be

  • more clean code?
  • more modularized?
  • Must the steps be configurable (replaceable) at runtime?

Refactor in order to make the code clean

If the steps do not need to be configured at runtime and you want to make your code more clean than you should take a look at the comments you made. Each comment is a hint.

Break the code block into methods and name them after the steps

/**
 * Explain what step1 does.
 */
private void step1() {
    // Step 1
    Map<String, SomeObject> objectsMap = someService.createObjectsMap();
    if (!objectsMap.isEmpty()) {
        step2(objectsMap);
    } else { // Step 1 failed
        // log and handle
    }
}

/**
 * Explain what step2 does.
 */
private void step2(Map<String, SomeObject> objectsMap) {
    // Step 2
    final AnotherObject anotherObject = anotherService
            .createAnotherObject(objectsMap);
    if (null != anotherObject) {
        step3(anotherObject);
    } else { // Step 2 failed
        // log and handle
    }
}

/**
 * Explain what step3 does.
 */
private void step3(AnotherObject anotherObject) {
    // Step 3 that gets anotherObject as input and returns something
    // else
}

This approach just breaks the method down to smaller methods. The advantage is that each smaller method is only responsible for one thing. And because it is a method you can add javadoc to it. So there is no need for inline comments anymore. Eventually you can give the method better names and omit the javadoc at all.

Refactor in order to make the steps replaceable at runtime

If you want to configure the steps that are executed at runtime (e.g. because of some user input) than you must encapsulate them in objects, because your application has references to objects that can be replaced.

Since you want all steps to have a common api you must make it more general.

Start thinking from the clients perspective. How should the steps be executed. E.g.

for (Step step : steps) {
    boolean executeNext = step.execute();
    if (!executeNext) {
        break;
    }
}

Design a Step interface

public interface Step {
    boolean execute();
}

How to pass the output of one step as the input to another?

Make an interface

public static interface StepInput<T> {
    public T getInput();
}

Implement your Steps. An abstract class will help you.

public abstract class InputOutputStep<T> implements Step,
        StepInput<T> {

    private T returnValue;

    protected void setReturnValue(T returnValue) {
        this.returnValue = returnValue;
    }

    public T getInput() {
        return returnValue;
    }
}

public class Step1 extends InputOutputStep<Map<String, SomeObject>> {

    private StepInput<Map<String, SomeObject>> stepInput;

    public Step1(StepInput<Map<String, SomeObject>> stepInput) {
        this.stepInput = stepInput;
    }

    public boolean execute() {
        boolean executeNext = false;

        Map<String, SomeObject> objectsMap = stepInput.getInput();
        if (!objectsMap.isEmpty()) {
            // Step 2
            setReturnValue(objectsMap);
            executeNext = true;
        } else { // Step 1 failed
            // log and handle
        }

        return executeNext;
    }
}

public class Step2 extends InputOutputStep<AnotherObject> {

    private StepInput<Map<String, SomeObject>> stepInput;
    private AnotherService anotherService;

    public Step2(AnotherService anotherService,
            StepInput<Map<String, SomeObject>> stepInput) {
        this.anotherService = anotherService;
        this.stepInput = stepInput;
    }

    public boolean execute() {
        boolean executeNext = false;

        Map<String, SomeObject> objectsMap = stepInput.getInput();
        AnotherObject anotherObject = anotherService
                .createAnotherObject(objectsMap);
        if (null != anotherObject) {
            setReturnValue(anotherObject);
            executeNext = true;
        } else { // Step 2 failed
            // log and handle
        }
        return executeNext;
    }
}

public class Step3 extends InputOutputStep<Void> {

    private StepInput<AnotherObject> stepInput;

    public Step3(StepInput<AnotherObject> stepInput) {
        this.stepInput = stepInput;
    }

    public boolean execute() {
        AnotherObject anotherObject = stepInput.getInput();
        setReturnValue(null);
        return false;
    }
}

Configure the steps at runtime and execute

Step1 step1 = new Step1(stepInput);
Step2 step2 = new Step2(anotherService, step1);
Step step3 = new Step3(step2);

Step[] steps = new Step[]{step1, step2, step3};

for (Step step : steps) {
    boolean executeNext = step.execute();
    if (!executeNext) {
        break;
    }
}
like image 125
René Link Avatar answered Nov 15 '22 20:11

René Link