This section explains elements used in activity diagrams. Activities are graphs consisting of different nodes connected by edges that can carry data. In the end, activities describe in which sequence operations within building blocks are executed. The different activity nodes are used to describe how the different flows depend on each other.
Initial nodes mark the start of a system and, hence, only exist in application blocks. Typically, each system contains one or more initial nodes. Before a system is started, initial nodes hold one token which moves downstream at startup.
You may need to initialize several blocks during startup. Instead of using one initial node and a fork with multiple outgoing flows, use several initial nodes. This results in better layout that is easier to understand as depicted in the figure above. At startup all initial nodes fire tokens at once.
Decision nodes introduce alternative flows, depending on the values of the guards attached to them.
To make a decision dependent on a boolean value provided by an operation or a variable, simply use a pair of outgoing flows from a decision node and add one of the following combination guards:
Edges that carry boolean values need to be object flows. This means the incoming edge of a decision node and outgoing edges with
false guards. The one with
else guard can be a control flow.
To decide between more than two flows, you can use string or integer guards as shown in the following figure. Integer values are compared with
== operator, while String values are compared with
String.equals() method. Hence, String guards are case-sensitive.
Fork nodes introduce concurrent flows that execute in parallel. Actions belonging to different flows may be executed in any order, depending on how a system is implemented. Usually this means that these flows should not write on the same variables to avoid interference.
Forks may be connected using both control and object flows. Some of the outgoing edges may be of type object flow while others are of type control flow. However, if object flows are involved, all outgoing object flows and the ingoing edge have to provide the same data type.
Fork nodes can also be considered as an assurance by the developer of the application regarding the behavior of their outgoing flows: A fork node allows a system that implements a specification to execute the operations of parallel branches in any order, without worrying about data consistency. If you are unsure, if two operations can be executed in parallel, do not use a fork node at all and simply execute these operations in a certain sequence. In the example below, prepareData may be executed after useData. Therefore, this value may not be initialized at all.
Fork and decision nodes result in very different behavior:
Merge nodes make that several flows are merged into one, by forwarding all incoming tokens to the outgoing edge. Both control and object flows are possible for merge nodes, however all attached flows have to provide the same data type. Merge nodes may have only one outgoing flow, but as many incoming as needed. For layout reasons, it is often better to have several merge node in a series rather than one single one, as shown with the two merge nodes (1) and (2) below.
A connector merge is basically a merge node with a label that you choose yourself. All connector merges with the same label refer to the same merge node. Connector merges are introduced to reduce the number of edges that crosscut other nodes so that your activity block is cleaner, easier to read and understand.
Join nodes synchronize several flows. Tokens are stored on the incoming edges, and as soon as the last missing token arrives, the join node fires through, with one token flowing along the outgoing edge.
A join node may have two or more incoming edges. At most one of them may be an object flow. In case all incoming edges are control flows, the outgoing edge has to be a control flow as well.
Join and merge nodes have a very different effect:
Flow final nodes terminate a flow. A token flowing into a flow final node is simply removed. Flow final nodes are only needed in two situations:
Note that operations may lead to a flow final node, but this node may be left our for simplicity.
Activity final nodes terminate a flow, but in comparison to flow final nodes they also terminate an entire application by removing all tokens from the included join nodes, event receptions and timers. Since activity final nodes terminate an application, they can only be used at the system level.
Both types of final nodes terminate a flow. The activity final node, however, also terminates the entire system.
To realize a simple delay, use the timer element. By default, the duration can be determined as a constant (at design-time). To change the duration at run-time, you can connect an object flow to a timer that provides an integer (int). The value will be used to set the duration of the timer in milliseconds.If a timer is activated again before it expired, the timer is restarted, which means that the time period begins again.
In some situations, we just want to interrupt the execution of a flow for technical reasons, so that other behavior of the system may be executed in between. This can happen for instance to comply with the block contract. In these cases, timers with a delay of 0 ms can be used. To obtain this type of timer, add a timer component and from its context menu, set the duration to 0 ms. This kind of timer looks like the following figure.
Both timers and flow breaks may pass data when used within an object flow. The example below shows a flow break within an object flow passing data of type JSONObject.
In many cases, you may need more control over the timer, for instance, turn it off again. For these cases exist dedicated building blocks in the standard library, from which we introduce some below.
Timers with Termination In some cases, you want the possibility to stop a timer. For this case, there are two building blocks in the Timers Library (com.bitreactive.library.timers), i.e., Timer (1) and Timer 2 (2). Both realize timers that can be aborted via a pin. The only difference is that the duration for Timer is determined by an instance parameter and Timer 2 accepts a duration at run-time.
Periodic Timers Periodic Timers can be realized easiest by the special building block Timer Periodic (3). This block comes in two variants. The first takes the duration as an instance parameter. The second variant accepts the duration of the period as input parameter.
Timeout Blocks To observe if a certain event happens within a certain time, i.e., an event arrives in time, use the Timeout block (4) or its variants from the library.
A building block can declare variables that store data, like the one for a String below (1). This data can be accessed by all operations within the building block, and there are specific set (2) and get (3) actions for variables.
Operation nodes refer to Java methods and work on data or APIs. If an operation has several parameters, it works similar to a join node. Each pin for the parameter can store a token, and the operation is executed once the last missing token arrives.
The content of operations is described by Java methods. The behavior of the building block determines when and in which sequence Java methods are never allowed to block or wait. This rule ensures that applications can be executed efficiently and stay responsive. If some code has to wait for an external response (for instance an HTTP request) there exist notification patterns to decouple the behavior, using threads and event receptions.
Exceptions must be caught within the methods. Java methods that represent operations must not throw any exceptions. This means you have to think of all exceptions that may happen and handle them appropriately.
In general, Java allows more than one method with the same name as long as they have a different signature, i.e., a distinct set of parameters. To keep the connection between the activity and the Java code simple, only one Java method with a certain name may exist.
Event receptions represent internal events needed for synchronization. They tell for instance when a certain operation or API delivers a result or an interrupt happened. As long as a token rests on them, they are ready to receive the specified event.
Events always originate from the code of a building block. This means, that some operation of the building block must contain code that dispatches this event, otherwise it will never be received. Three things are important for declaring an event, as shown below. (1) The reception in the behavior of a block, and (2) it’s dispatching from code. An event declaration can add parameter to an event. This parameter has to be fed into the system from code when the event is dispatched (2), and the behavior will automatically declare a reception pin for that parameter.
By default, an event reception is deactivated once the event happens, and it will not receive any more of them, until it is activated again, as in (1) below. To continuously listen to incoming events, a construction with a fork can be used, as shown in (2). Instead of the construction with the fork, the event reception can also be marked with an “*”, so show that it is continuously enabled (3).
Comments provide the possibility to add information to activity blocks. They can be used to give more detailed information about the behaviour of a block. In the following example a comment explains the purpose and the expected outcome of a test system:
Comments can also be used as reminder about unfinished parts of a block (TODOs) as in the following block Send Multiple Emails, which is missing operations, timers and flows:
Comments can also indicate known problems:
To add an activity node, do a right-click on the diagram to reveal the context menu. Then, select Add… and the type of node you want to add.