Skip to content
master
Go to file
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.

README.md

Requirements as code

Build Status Gitter

requirements as code logo

Requirements as code enables you to translate use cases to code to build maintainable applications.

This page describes simple ways to get started.

You will see how to create actors with use case models that react to individual messages.

For sequences of interactions, create a model with flows instead.

An actor running such a model with flows can serve as an easy to understand alternative to state machines.

A model with flows is useful to build user journeys, sagas and process managers.

See this wiki page for an explanation.

You can find code examples for models with flows here.

Getting started

Requirements as code is available on Maven Central.

The size of the core jar file is less than 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>1.9.2</version>
  </dependency>

If you are using Gradle, include the following in your build.gradle, to use the core:

implementation 'org.requirementsascode:requirementsascodecore:1.9.2'

At least Java 8 is required to use requirements as code, download and install it if necessary.

How to create an actor 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 an actor with a model

class MyActor extends AbstractActor{
  @Override
  public Model behavior() {
    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.

Note that the Actor class is not thread-safe, and it's not an active class that runs in its own thread.

Step 2: Send a message to the actor

MyActor actor = new MyActor();
Optional<T> queryResultOrEvent = actor.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.

Code example

There's an actor with a single use case 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.AbstractActor;
import org.requirementsascode.Model;

public class HelloUser {
  public static void main(String[] args) {
    GreetingService greeter = new GreetingService(HelloUser::saysHello);
    greeter.reactTo(new RequestHello("Joe"));
  }
  
  private static void saysHello(RequestHello requestsHello) {
    System.out.println("Hello, " + requestsHello.getUserName() + ".");
  }
}

class GreetingService extends AbstractActor {
  private static final Class<RequestHello> requestsHello = RequestHello.class;
  private final Consumer<RequestHello> saysHello;

  public GreetingService(Consumer<RequestHello> saysHello) {
    this.saysHello = saysHello;
  }

  @Override
  public Model behavior() {
    Model model = Model.builder()
      .user(requestsHello).system(saysHello)
    .build();
    return model;
  }
}

class RequestHello {
  private String userName;

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

  public String getUserName() {
    return userName;
  }
}

Applying the requirements as code design principles

The example above has shown how to create an actor, and send messages to it. In practice, that already gives you the benefit of recording the interaction in the code for long term maintenance. To apply the requirements as code design principles, to clearly separate requirements from realization and get to a pure domain model, the above example needs to be expanded as follows.

Actor

Create a subclass of AbstractActor, and override its behavior() method to provide the model.

Pass the message handlers as constructor parameters.

Use interfaces, not concrete classes, as constructor parameters.

That let's you change the concrete message handler from the outside.

Message senders

There needs to be someone who's sending messages to the actor. In practice, this could be a Spring Controller, or a desktop GUI, for example. Pass the actor to the message sender as a constructor parameter. After that, the sender can send messages to the actor.

class MessageSender {
  private AbstractActor greetingService;

  public MessageSender(AbstractActor greetingService) {
    this.greetingService = greetingService;
  }

  public void sendMessages() {
    greetingService.reactTo(new RequestHello("Joe"));
  }
}

Messages

Messages should be simple and immutable POJOs. They just carry the information needed to be processed by the message handler. No domain logic is allowed here. In the example, the RequestHello class represents a command that carries the user name.

class RequestHello {
  private String userName;

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

  public String getUserName() {
    return userName;
  }
}

Message handlers

Message handlers orchestrate the calls to the infrastructure and domain code. They are 'dumb' in the sense that they don't contain business logic themselves.

class SayHello implements Consumer<RequestHello> {
  private OutputAdapter outputAdapter;

  public SayHello() {
    this.outputAdapter = new OutputAdapter();
  }
  
  public void accept(RequestHello requestHello) {
    String greeting = Greeting.forUser(requestHello.getUserName());
    outputAdapter.showMessage(greeting);
  }
}

Infrastructure classes

These are classes that connect to external services or the infrastructure. In the example, this is the class that prints the message to the console.

class OutputAdapter{
  public void showMessage(String message) {
    System.out.println(message);
  }
}

Pure domain code

These are the domain classes. They don't communicate with the technical infrastructure, since all communication with the infrastructure happens in the message handler.

In the example, there is only a single domain function: for creating a greeting, based on the user name.

class Greeting{
  public static String forUser(String userName) {
    return "Hello, " + userName + ".";
  }
}

Complete example code for applying the design priciples

Here's the complete example as a single file for convenience.

package actor;

import java.util.function.Consumer;

import org.requirementsascode.AbstractActor;
import org.requirementsascode.Model;

public class ActorExample {
  public static void main(String[] args) {
    AbstractActor greetingService = new GreetingService(new SayHello());
    new MessageSender(greetingService).sendMessages();
  }
}

/**
 * Actor that owns and runs the use case model, and reacts to messages by
 * dispatching them to message handlers.
 */
class GreetingService extends AbstractActor {
  private static final Class<RequestHello> requestsHello = RequestHello.class;
  private final Consumer<RequestHello> saysHello;

  public GreetingService(Consumer<RequestHello> saysHello) {
    this.saysHello = saysHello;
  }

  @Override
  public Model behavior() {
    Model model = Model.builder()
      .user(requestsHello).system(saysHello)
    .build();
    return model;
  }
}

/**
 * Sender of the message, external to the boundary
 */
class MessageSender {
  private AbstractActor greetingService;

  public MessageSender(AbstractActor greetingService) {
    this.greetingService = greetingService;
  }

  /**
   * Send messages to the service actor. In this example, we don't care 
   * about the return value of the call, because we don't send a query
   * or publish events.
   */
  public void sendMessages() {
    greetingService.reactTo(new RequestHello("Joe"));
  }
}

/**
 * Command class
 */
class RequestHello {
  private String userName;

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

  public String getUserName() {
    return userName;
  }
}

/**
 * Message handlers
 */
class SayHello implements Consumer<RequestHello> {
  private OutputAdapter outputAdapter;

  public SayHello() {
    this.outputAdapter = new OutputAdapter();
  }
  
  public void accept(RequestHello requestHello) {
    String greeting = Greeting.forUser(requestHello.getUserName());
    outputAdapter.showMessage(greeting);
  }
}

/**
 * Infrastructure classes
 */
class OutputAdapter{
  public void showMessage(String message) {
    System.out.println(message);
  }
}

/**
 * Domain classes
 */
class Greeting{
  public static String forUser(String userName) {
    return "Hello, " + userName + ".";
  }
}

Publishing events

When an actor's behavior only uses the system() method, it's restricted to just consuming messages. But an actor can also publish events with systemPublish(), as shown in this file:

class PublishingActor extends AbstractActor {
  @Override
  public Model behavior() {
    Model model = Model.builder()
      .user(EnterName.class).systemPublish(this::publishNameAsString)
      .on(String.class).system(this::displayNameString)
    .build();
    return model;
  }

  private String publishNameAsString(EnterName enterName) {
    return enterName.getUserName();
  }

  public void displayNameString(String nameString) {
    System.out.println("Welcome, " + nameString + ".");
  }
}

As you can see, publishNameAsString() takes a command object as input parameter, and returns an event to be published. In this case, a String.

By default, the actor takes the returned event and publishes it to the same model, as shown above. But you can also publish events to a different actor. That receiving actor will react to the event.

The syntax is:

.user(/* command class */).systemPublish(/* event producing function*/).to(/* receiving actor */)

or

.on(/* event class */).systemPublish(/* event producing function*/).to(/* receiving actor */)

Here is an example of two actors. The MessageProducer receives an EnterName command and sends a NameEntered event to the MessageConsumer. The consumer receives the event, and prints the name.

class MessageProducer extends AbstractActor {
  private AbstractActor messageConsumer;

  public MessageProducer(AbstractActor messageConsumer) {
    this.messageConsumer = messageConsumer;
  }
  
  @Override
  public Model behavior() {
    Model model = Model.builder()
      .user(EnterName.class).systemPublish(this::nameEntered).to(messageConsumer)
    .build();
    return model;
  }

  private NameEntered nameEntered(EnterName enterName) {
    return new NameEntered(enterName.getUserName());
  }
}

class MessageConsumer extends AbstractActor {
  @Override
  public Model behavior() {
    Model model = Model.builder()
      .on(NameEntered.class).system(this::displayName)
    .build();
    return model;
  }

  public void displayName(NameEntered nameEntered) {
    System.out.println("Welcome, " + nameEntered.getUserName() + ".");
  }
}

To access the model runner inside of an actor, call super.getModelRunner().

Note that in any case, an actor returns the event that was published last to the caller of actor.reactTo().

Influences and special features

Requirements as code is influenced by the ideas of clean architecture and hexagonal architecture. It can be used to implement them.

You can use this library to publish DDD Domain Events without littering your code with calls to a domain event publisher. Instead, your command handler returns the event. Your event publisher will pick it up automatically.

The use case model at the boundary represents the single source of truth for interactions started by the user. That's why you can generate living documentation from the use case model. The generated use case documents represent always up to date information about how the system works from a user's perspective.

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".
You can’t perform that action at this time.