At my work, we have surveys, and one survey involves multiple steps. I work in automation, so I design tests around the page-objects we create for these surveys. We call this particular survey a "flow" survey because it has multiple steps. So you can skip step1 (survey A), then complete or skip step 2 (survey B), then complete or skip step 3 (survey C). Naively, we could write a test that just has methods that look like this:
public void completeSurveyA() {
//...
}
public void skipSurveyB() {
//...
}
public void completeSurveyB() {
//...
}
public void skipSurveyC() {
//...
}
public void completeSurveyC() {
//...
}
You would use it like this
completeSurveyA();
skipSurveyB();
completeSurveyC();
However, that could be a problem because we might call completeSurveyB()
before we call completeSurveyA()
, call completeSurveyA
twice, etc. and the test would break. To avoid this, I introduced a different approach where calling a method on surveyA would return a surveyB object, which would return a surveyC object.
public class SurveyFlow() {
public SurveyB completeSurveyA() {
//...
return new SurveyB();
}
private class SurveyB() {
public SurveyC skipSurveyB() {
//...
return new SurveyC();
}
public SurveyC completeSurveyB() {
//...
return new SurveyC();
}
private class SurveyC() {
public void skipSurveyC() {
//...
}
public void completeSurveyC() {
//...
}
}
}
}
You would use it like this
new SurveyFlow().completeSurveyA().skipSurveryB().completeSurveyC();
The pattern reminds me of a state machine because only certain methods are available to you in different states, but I'm wondering if there is a more specific name for this pattern.
According to the classes of your example, it's a FluentInterface:
Probably the most important thing to notice about this style is that the intent is to do something along the lines of an internal DomainSpecificLanguage. (...) The API is primarily designed to be readable and to flow.
It's not the builder pattern, because you're not building anything (i.e. you don't have a final build()
method where data gathered in previous steps is used to create an instance).
It's not the state pattern either, because operations (skip()
and complete()
in this case) do not depend on the state of an object (actually steps don't have a state).
It would have been the state pattern if the whole survey had been modeled as an object with one method whose implementation depended on different states (in this case, the states would be the steps plus the action taken, i.e. surveyACompleted
, surveyASkipped
, surveyBCompleted
, surveyBSkipped
, etc, while the method would be something like nextStep()
):
public class SurveyFlow {
private SurveyState state; // this represents the current step
public SurveyFlow(boolean skipFirst) {
this.state = skipFirst ? new SurveyASkipped() : new SurveyACompleted();
}
void setState(SurveyState state) {
this.state = state;
}
public void takeStep(boolean skipNext) { // takeStep operation delegated
// to the state (current step)
this.state.takeStep(skipNext, this); // "this" passed to the step so
// that it can switch to the
// next step if needed
}
}
The state would be polymorphically represented by each step of the SurveyFlow
:
abstract class SurveyState {
protected abstract void takeStep(boolean skipNext, SurveyFlow survey);
}
Survey A states would be as follows:
class SurveyACompleted extends SurveyState {
protected void takeStep(boolean skipNext, SurveyFlow survey) {
// ...
survey.setState(skipNext ? new SurveyBSkipped() : new SurveyBCompleted());
}
}
class SurveyASkipped extends SurveyState {
protected void takeStep(boolean skipNext, SurveyFlow survey) {
// ...
survey.setState(skipNext ? new SurveyBSkipped() : new SurveyBCompleted());
}
}
Survey B states would be as follows:
class SurveyBCompleted extends SurveyState {
protected void takeStep(boolean skipNext, SurveyFlow survey) {
// ...
survey.setState(skipNext ? new SurveyCSkipped() : new SurveyCCompleted());
}
}
class SurveyBSkipped extends SurveyState {
protected void takeStep(boolean skipNext, SurveyFlow survey) {
// ...
survey.setState(skipNext ? new SurveyCSkipped() : new SurveyCCompleted());
}
}
For your example:
You could do:
SurveyFlow survey = new SurveyFlow(false); // will complete survey A
survey.takeStep(true); // completed survey A and will skip survey B
survey.takeStep(false); // skipped survey A and will complete survey C
survey.takeStep(true); // completed survey C
If survey C is the last step, then it can ignore the boolean
argument and shouldn't set further steps.
This is in a way the State pattern, but does not completely adhere to the State pattern described by GoF, because you are not changing the state of a single object, but rather creating and returning a new object of different class which you use afterwards.
Actually, this resembles much more the Builder pattern, where the completeSurveyC()
acts as a build
or getResult
method to build a Surway
from multiple consisting pieces specified earlier.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With