Variable Tutorial

This tutorial demonstrates passing values downstream between functions using variables within a Spring Boot application.

Unlike direct method parameters, variables do not need to be passed explicitly by the caller. A variable set in one function is available for injection into any downstream function, allowing pipelines to be composed from independent classes that know nothing of each other.

Tutorial Source

Maven dependency

		<dependency>
			<groupId>net.officefloor.springboot</groupId>
			<artifactId>officefloor-rest-spring-boot-4-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

Application class

@SpringBootApplication
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

Out<T> / Var<T>

The following method demonstrates setting values for variables.

public class OutLogic {

	public static void setValues(Out<Person> person, @Description Out<String> description) {
		person.set(new Person("Daniel", "Sagenschneider"));
		description.set("Need to watch his code!");
	}
}

Variables are created by reflectively interrogating the method parameters. Because many variables may share the same type, qualifiers distinguish them — much like naming a variable.

The @Qualifier meta-annotation is the recommended approach. The qualifier used in this tutorial is:

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Description {
}

Prefer Out<T> to set a variable. This allows the compiler to reason about out/in combinations and catch uninitialised variables.

When a function must both read and write the same variable, use Var<T>. Use this sparingly as it reduces the ability to statically reason about flow composition.

public class VarLogic {

	public static void setValues(Var<Person> person, @Description Var<String> description) {
		person.set(new Person("Daniel", "Sagenschneider"));
		description.set("Need to watch his code!");
	}
}

@Val / In<T>

The following method demonstrates reading variable values downstream.

public class ValLogic {

	public static void useValues(@Val Person person, @Description @Val String description,
			ObjectResponse<ServerResponse> response) {
		response.send(new ServerResponse(person, description));
	}
}

@Val annotates a parameter to receive the current value of a variable. It distinguishes the variable from other injected objects such as ObjectResponse.

The preferred combination is Out<T> upstream with @Val downstream — readable and avoids importing extra framework types at the read site.

For symmetry there is also In<T>:

public class InLogic {

	public static void useValues(In<Person> person, @Description In<String> description,
			ObjectResponse<ServerResponse> response) {
		response.send(new ServerResponse(person.get(), description.get()));
	}
}

A shared scope for functions

Variables give a set of functions the same convenience that a single method gives its statements.

A single method has one local scope. Each statement can use any variable declared above it. Set a value on one line and every later line can use it. The state accumulates in scope as the method runs.

Composing the same work from separate functions would normally lose that scope. A function sees only its own parameters. With return values a function hands one value to the next. To carry several accumulated values it has to bundle them into a type. As the flow grows, so does the number of these carrier types, one for each combination of state in flight. The types model nothing. They exist only to move state between functions.

Variables restore the shared scope. A function publishes a value with Out. Any later function reads it with @Val. The set of live variables is the shared scope, so each function reads any earlier value it needs. Each function is like a block of code in the single method, with its variables still available to the functions that follow. There are no carrier types.

It also improves on the single method. In a method a block's use of the surrounding variables is implicit, and the block cannot be reused on its own. A function instead declares the variables it reads and the variables it writes. Its inputs and outputs are explicit, so the function is reusable wherever those variables are in scope.

So variables give the flexibility of a shared local scope with the discipline of an explicit, reusable contract on each function.

Composing flows — YAML endpoint files

The YAML files wire together the upstream setter and downstream reader as a two-function pipeline. Each function is independent — neither class references the other.

OutSet:
  class: net.officefloor.tutorial.variablehttpserver.OutLogic
  method: setValues
  next: InUse
InUse:
  class: net.officefloor.tutorial.variablehttpserver.InLogic
  method: useValues
VarSet:
  class: net.officefloor.tutorial.variablehttpserver.VarLogic
  method: setValues
  next: ValUse
ValUse:
  class: net.officefloor.tutorial.variablehttpserver.ValLogic
  method: useValues
OutToValSet:
  class: net.officefloor.tutorial.variablehttpserver.OutLogic
  method: setValues
  next: OutToValUse
OutToValUse:
  class: net.officefloor.tutorial.variablehttpserver.ValLogic
  method: useValues

Variables in a REST pipeline

Two rules make variables the natural way to pass request state between functions.

First, the HTTP request body can be read only once. Only one function may bind @RequestBody. When a later function also needs the body, the first function binds it once and publishes it with Out. The later function reads the variable, not the body.

Second, @Val returns the same object the producer stored. It is not a copy. So one function can load an entity into a variable, a second function can mutate that entity in place, and a third function sees the change.

The REST CRUD Orchestration tutorial uses both rules to build a full resource. The Orchestration Patterns and Naming reference states them in full.

Testing

Tests use MockMvc to exercise the full composed pipeline through Spring MVC:

@SpringBootTest
@AutoConfigureMockMvc
public class VariableHttpServerTest {

	@Autowired
	private MockMvc mvc;

	@Autowired
	private ObjectMapper mapper;

	private static final ServerResponse EXPECTED =
			new ServerResponse(new Person("Daniel", "Sagenschneider"), "Need to watch his code!");

	@Test
	public void outIn() throws Exception {
		mvc.perform(MockMvcRequestBuilders.get("/outIn")
				.accept(MediaType.APPLICATION_JSON))
				.andExpect(status().isOk())
				.andExpect(content().json(mapper.writeValueAsString(EXPECTED)));
	}

	@Test
	public void varVal() throws Exception {
		mvc.perform(MockMvcRequestBuilders.get("/varVal")
				.accept(MediaType.APPLICATION_JSON))
				.andExpect(status().isOk())
				.andExpect(content().json(mapper.writeValueAsString(EXPECTED)));
	}

	@Test
	public void outVal() throws Exception {
		mvc.perform(MockMvcRequestBuilders.get("/outVal")
				.accept(MediaType.APPLICATION_JSON))
				.andExpect(status().isOk())
				.andExpect(content().json(mapper.writeValueAsString(EXPECTED)));
	}
}

Next

The Governance tutorial wraps function execution with cross-cutting concerns such as transactions, declared in YAML.