Skip to content
master
Switch branches/tags
Code

Files

Permalink
Failed to load latest commit information.

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 all 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 hexagonal architecture or clean architecture.

This page describes a simple way 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.

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

Clean architecture outline

The following example shows how to implement a clean architecure with requirements as code, in principle:

public class CleanArchitectureOutline {
  public static void main(String[] args) {
    ConsolePrinter consolePrinter = new ConsolePrinter(); 
    GreetingServiceModel greetingServiceModel = new GreetingServiceModel(consolePrinter);
    Behavior greetingService = StatelessBehavior.of(greetingServiceModel);
    
    greetingService.reactTo(new SayHelloRequest("Joe"));
  }
}

/**
 * The behavior model defines that a consumer reacts to SayHelloRequest.
 * 
 * @author b_muth
 *
 */
class GreetingServiceModel implements BehaviorModel {
  private final Consumer<String> outputPort;

  public GreetingServiceModel(Consumer<String> outputPort) {
    this.outputPort = outputPort;
  }
  
  @Override
  public Model model() {
    Model model = Model.builder()
      .user(SayHelloRequest.class).system(sayHello())
    .build();
    return model;
  }
  
  private SayHello sayHello() {
    return new SayHello(outputPort);
  }
}

/**
 * Command class
 */
class SayHelloRequest {
  private final String userName;

  public SayHelloRequest(String userName) {
    this.userName = userName;
  }

  public String getUserName() {
    return userName;
  }
}

/**
 * Message handler
 */
class SayHello implements Consumer<SayHelloRequest> {
  private final Consumer<String> outputPort;

  public SayHello(Consumer<String> outputPort) {
    this.outputPort = outputPort;
  }

  public void accept(SayHelloRequest requestHello) {
    String greeting = Greeting.forUser(requestHello.getUserName());
    outputPort.accept(greeting);
  }
}

/**
 * Infrastructure class
 */
class ConsolePrinter implements Consumer<String>{
  public void accept(String message) {
    System.out.println(message);
  }
}

/**
 * Domain class
 */
class Greeting {
  public static String forUser(String userName) {
    return "Hello, " + 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".