bertilmuth / Requirementsascode
Programming Languages
Projects that are alternatives of or similar to Requirementsascode
Requirements as code
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.
The call to reactTo()
is synchronous.
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 final 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 final 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 final 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. For testability, pass in all collaborators via constructor parameters.
class SayHello implements Consumer<RequestHello> {
private final OutputAdapter outputAdapter;
public SayHello(OutputAdapter outputAdapter) {
this.outputAdapter = 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) {
final OutputAdapter outputAdapter = new OutputAdapter();
AbstractActor greetingService = new GreetingService(new SayHello(outputAdapter));
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;
}
}
/**
* Message sender class
*/
class MessageSender {
private final AbstractActor greetingService;
public MessageSender(AbstractActor greetingService) {
this.greetingService = greetingService;
}
/**
* Send message 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 final String userName;
public RequestHello(String userName) {
this.userName = userName;
}
public String getUserName() {
return userName;
}
}
/**
* Message handler
*/
class SayHello implements Consumer<RequestHello> {
private final OutputAdapter outputAdapter;
public SayHello(OutputAdapter outputAdapter) {
this.outputAdapter = outputAdapter;
}
public void accept(RequestHello requestHello) {
String greeting = Greeting.forUser(requestHello.getUserName());
outputAdapter.showMessage(greeting);
}
}
/**
* Infrastructure class
*/
class OutputAdapter {
public void showMessage(String message) {
System.out.println(message);
}
}
/**
* Domain class
*/
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
- Examples for building/running use case models with flows
- Cross-cutting concerns example
- How to generate documentation from models
Publications
Subprojects
- 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.
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".