Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
187 lines (143 sloc) 7.8 KB

requirements as code

Build Status

This project simplifies the development of message-driven applications.

It provides a builder API to create handlers for many types of messages at once.

You can customize message handling in a simple way, for example for measuring performance, or for logging purposes.

For more advanced cases that depend on the application's state, like Process Managers and Sagas, you can create a model with flows. It's a simple alternative to state machines, understandable by developers and business people alike.

For the long term maintenance of your application, you can generate documentation from the models inside the code without the need to add comments to it.

getting started

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

Requirements as code is available on Maven Central.

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.2.1</version>
  </dependency>

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

compile 'org.requirementsascode:requirementsascodecore:1.2.1'

how to use requirements as code

Here's what you need to do as a developer.

Step 1: Build a model defining the message types to handle, and the methods that react to a message:

Model model = Model.builder()
	.user(<command class>).system(<command handler, i.e. lambda, method reference, consumer or runnable)>)
	.user(..).system(...)
	...
.build();

The order of the statements has no significance. For handling events instead of commands, 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 a step has been run, if the condition is fulfilled.

Step 2: Create a runner and run the model:

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

Step 3: Send messages to the runner, and enjoy watching it react:

runner.reactTo(<Message POJO Object> [, <Message POJO Object>,...]);

To customize the behavior when the runner reacts to a message, use modelRunner.handleWith(). By default, if a message's class is not declared in the model, the runner consumes it 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.

hello world

Here's a complete Hello World example:

package hello;

import org.requirementsascode.Model;
import org.requirementsascode.ModelRunner;

public class HelloUser {
	public static void main(String[] args) {
		new HelloUser().buildAndRunModel();
	}
	
	private void buildAndRunModel() {
		Model model = Model.builder()
			.user(RequestHello.class).system(this::displayHello)
			.user(EnterName.class).system(this::displayName)
		.build();

		new ModelRunner().run(model)
			.reactTo(new RequestHello(), new EnterName("Joe"));		
	}

	public void displayHello(RequestHello requestHello) {
		System.out.println("Hello!");
	}

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

	class RequestHello {}
	
	class EnterName {
		private String userName;

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

		public String getUserName() {
			return userName;
		}
	}
}

event queue for non-blocking handling

The default mode for the ModelRunner is to handle messages in a blocking way. Instead, you can use a simple event queue that processes events one by one in its own thread:

Model model = ...;
ModelRunner modelRunner = new ModelRunner();
modelRunner.run(model);

EventQueue queue = new EventQueue(modelRunner::reactTo);
queue.put(new String("I'm an event, react to me!"));

The constructor argument of EventQueue specifies that each event that's put() will be placed in the queue, and then forwarded to ModelRunner.reactTo(). Note that you can forward events to any other consumer of an object as well.

publishing events

When you use the system() method, you are restricted to just consuming messages. But you can also publish events with systemPublish(), like so:

	private void buildAndRunModel() {
		Model model = Model.builder()
			.on(EnterName.class).systemPublish(this::publishNameAsString) 
			.on(String.class).system(this::displayNameString) 
		.build();		
		
		Optional<Object> userName = new ModelRunner().run(model)
			.reactTo(new EnterName("Joe"));	
	}
	
	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 model runner takes the returned event and publishes it to the model. In the example, this will print "Welcome, Joe."

This behavior can be overriden by specifying a custom event handler on the ModelRunner with publishWith(). For example, you can use modelRunner.publishWith(queue::put) to publish events to an event queue.

documentation

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.