Skip to content

bertilmuth/requirementsascode

Repository files navigation

Requirements as code

Gitter

requirements as code logo

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.

Getting started

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.

How to create a behavior and send messages to it

Let's look at the general steps first. After that, you'll see a concrete code example.

Step 1: Create a behavior model

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.

Step 2: Create a behavior based on the model

BehaviorModel myBehaviorModel = new MyBehaviorModel(...);
Behavior myBehavior = StatelessBehavior.of(myBehaviorModel);

Step 3: Send a message to the behavior

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.

Code example

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;
  }
}

Further documentation of requirements as code

Publications

Subprojects

Build from sources

Use Java >= 11 and the project's gradle wrapper to build from sources.

Related topics

  • 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".