A behavior is configured by a behavior model.
A behavior model maps message types to message handlers.
A message handler is a function, consumer or supplier of messages.
Your calling code sends messages to the behavior. The behavior finds the right handler. The handler processes the message, and potentially produces a result.
So the calling code doesn't need to know anything about the internals of your service. It sends all messages to a single behavior instance, and gets a result back. Black box behavior.
Since the behavior is the central point of control for all functions, you can inject and configure the dependencies of all functions through it. That makes it easy to implement a ports & adapters architecture. The Behavior interface acts as the single driver port.
Having a central place also enables you to deal with cross-cutting concerns, like database transactions and logging. You can keep those concerns separate from your business functions, and change them without affecting those.
This page describes how to get started. Learn how to create a stateless behavior that handles each message individually.
For sequences of interactions, create an actor instead. An actor runs a use case model with flows. It remembers the current position in the flow, and accepts or rejects messages depending on that position. Thus, an actor can serve as an easy to understand alternative to state machines.
See this wiki page for an explanation of actors, use cases and flows. You can find more code examples for actors here.
Requirements as code is available on Maven Central.
The size of the core jar file is around 100 kBytes. It has no further dependencies.
If you are using Maven, include the following in your POM, to use the core:
<dependency>
<groupId>org.requirementsascode</groupId>
<artifactId>requirementsascodecore</artifactId>
<version>2.0</version>
</dependency>
If you are using Gradle, include the following in your build.gradle, to use the core:
implementation 'org.requirementsascode:requirementsascodecore:2.0'
At least Java 8 is required to use requirements as code, download and install it if necessary.
Let's look at the general steps first. After that, you'll see a concrete code example.
class MyBehaviorModel implements BehaviorModel{
@Override
public Model model() {
Model model = Model.builder()
.user(/* command class */).system(/* command handler*/)
.user(..).system(...)
...
.build();
return model;
}
}
...
For handling commands, the message handler has a Consumer<T>
or Runnable
type, where T is the message class.
For handling queries, use .systemPublish
instead of .system
, and the message handler has a Function<T, U>
type.
For handling events, use .on()
instead of .user()
.
For handling exceptions, use the specific exception's class or Throwable.class
as parameter of .on()
.
Use .condition()
before .user()
/.on()
to define an additional precondition that must be fulfilled.
You can also use condition(...)
without .user()
/.on()
, meaning: execute at the beginning of the run, or after an interaction, if the condition is fulfilled.
Use .step(...)
before .user()
/.on()
to explicitly name the step - otherwise the steps are named S1, S2, S3...
The order of user(..).system(...)
statements has no significance here.
BehaviorModel myBehaviorModel = new MyBehaviorModel(...);
Behavior myBehavior = StatelessBehavior.of(myBehaviorModel);
Optional<T> queryResultOrEvent = myBehavior.reactTo(<Message POJO Object>);
Instead of T, use the type you expect to be published. Note that reactTo()
casts to that type, so if you don't know it, use Object
for T.
If an unchecked exception is thrown in one of the handler methods, reactTo()
will rethrow it.
The call to reactTo()
is synchronous.
Here's a behavior with a single interaction.
The user sends a request with the user name ("Joe"). The system says hello ("Hello, Joe.")
package helloworld;
import java.util.function.Consumer;
import org.requirementsascode.Behavior;
import org.requirementsascode.BehaviorModel;
import org.requirementsascode.Model;
import org.requirementsascode.StatelessBehavior;
public class HelloUser {
public static void main(String[] args) {
GreeterModel greeterModel = new GreeterModel(HelloUser::sayHello);
Behavior greeter = StatelessBehavior.of(greeterModel);
greeter.reactTo(new SayHelloRequest("Joe"));
}
private static void sayHello(SayHelloRequest requestsHello) {
System.out.println("Hello, " + requestsHello.getUserName() + ".");
}
}
class GreeterModel implements BehaviorModel {
private final Consumer<SayHelloRequest> sayHello;
public GreeterModel(Consumer<SayHelloRequest> sayHello) {
this.sayHello = sayHello;
}
@Override
public Model model() {
Model model = Model.builder()
.user(SayHelloRequest.class).system(sayHello)
.build();
return model;
}
}
class SayHelloRequest {
private final String userName;
public SayHelloRequest(String userName) {
this.userName = userName;
}
public String getUserName() {
return userName;
}
}
- Examples for building/running use case models with flows
- Cross-cutting concerns example
- How to generate documentation from models
- Implementing a ports & adapters architecture
- Kissing the state machine goodbye
- The truth is in the code
- requirements as code core: create and run models.
- requirements as code extract: generate documentation from the models (or any other textual artifact).
- requirements as code examples: example projects illustrating the use of requirements as code.
Use Java >= 11 and the project's gradle wrapper to build from sources.
- The work of Ivar Jacobson on Use Cases. As an example, have a look at Use Case 2.0.
- The work of Alistair Cockburn on Use Cases, specifically the different goal levels. Look here to get started, or read the book "Writing Effective Use Cases".