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.

A use case model defines interactions. An interaction consists of a message class and a message handler. A message handler orchestrates the calls to the domain code, and to the infrastructure. By switching message handlers, or by injecting different dependencies into them, you can switch your application's infrastructure.

After calling the domain/infrastructure code, the message handler either:

  • doesn't return anything,
  • returns a query result, or
  • returns an event to be published.

Optionally, you can specify a precondition.

For sequences of interactions, create a use case model with flows instead. It's a simple alternative to state machines.

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.

Getting started

Requirements as code is available on Maven Central.

The size of the core jar file is around 64 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.7.7</version>
  </dependency>

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

implementation 'org.requirementsascode:requirementsascodecore:1.7.7'

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

How to build and run a use case model

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

Step 1: Build a use case model

Model model = Model.builder()
  .user(/* command class */).system(/* command handler*/)
  .user(..).system(...)
  ...
.build();

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 runner, and run the model

ModelRunner runner = new ModelRunner().run(model);

Step 3: Send a message to the runner

Optional<T> queryResultOrEvent = runner.reactTo(<Message POJO Object>);

Instead of T, use the type you expect to be published. Note that the runner casts to that type, so if you don't know it, use Object for T. To customize the behavior when the runner reacts to a message, use modelRunner.handleWith() (example here).

By default, if a message's class (or superclass) is not declared in the model, the runner consumes the message silently. To customize that behavior, use modelRunner.handleUnhandledWith(). If an unchecked exception is thrown in one of the handler methods and it is not handled by any other handler method, the runner will rethrow it.

Example for building and running a use case model

There's 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.Model;
import org.requirementsascode.ModelRunner;

public class HelloUser {
  public static void main(String[] args) {
    Model model = new ModelBuilder().build(HelloUser::sayHello);
    ModelRunner modelRunner = new ModelRunner().run(model);
    modelRunner.reactTo(new RequestHello("Joe"));
  }
  
  public static void sayHello(RequestHello requestHello) {
    System.out.println("Hello, " + requestHello.getUserName() + ".");
  }
}

class ModelBuilder {
  private static final Class<RequestHello> requestsHello = RequestHello.class;

  public Model build(Consumer<RequestHello> saysHello) {
    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;
  }
}

Example for applying the design principles

The examples above have shown how to build and run use case models. 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 change as follows.

Actor that represents system/service

Instead of directly creating a runner for a model as shown above, for larger scale applications, you should create an actor. An actor encapsulates a ModelRunner and runs it for you. All you need to provide is the model of the actor's behavior, as shown below.

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

class GreetingService extends AbstractActor {
  private static final Class<RequestHello> requestsHello = RequestHello.class;
  private 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;
  }
}

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.

To send a message to the actor, call actor.reactTo(<message>). Same syntax you already know.

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 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(). If you want full control of the way events are published, call modelRunner.publishWith().

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

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.