EyeCatcher

Imagine you could build big complex applications only from small, independent and testable components. Well, with flows you can!

Tip
If you don’t know whether this is interesting for you: There is a summary at the bottom of this page.

Ports as the glue between independent components

Lets take a very simple flow apart and analyze it:

Introduction
  • The first component 'Input' generates data (e.g. by reading a file) and doesn’t care where the data goes and what happens next to it.

  • The second component 'Output' consumes data (e.g. by writing it to an other file) and doesn’t know where its input data comes from.

  • The only connection between the two components is the arrow. The data flows from the first component through the arrow into the second component. Lets call the arrow a port.

  • The components can evolve independently as long as they agree on the data type. That is why the data type is annotated on top of the arrow.

  • The components can change their inner workings independently. They can even add further ports to other components fully independent of each other.

  • The subcomponents operate on the data that flows through them. So lets call them operations.

Such a flow and its subcomponents have many desirable features. Now lets see what they look like in Java code.

What does the code look like?

Port is just a single method interface.

package org.flowdev.base;


public interface Port<T> {
	void send(T data);
}

The sender of a data package is calling the interface and the receiver of the data package has to implement it. The interface is usually implemented with a Java 8 lambda that often calls a method in the enclosing class. This way it is easy to understand, trivial to debug and speed is optimal since its just a simple method call.

Lets create a simple greeter

In this example we create a very simple flow that contains only two components. One to say hello and one to tell the names to greet.

Building the first component

Lets first build a very simple component that is just able to greet.

    public static class SayHello {
        private Port<String> inPort = name -> {    (1)
            System.out.println("Hello, " + name + "!");
        };

        public Port<String> getInPort() {    (2)
            return inPort;
        }
    }
  1. Here we implement the Port interface as a simple Java 8 lambda.

  2. This is just a standard Java getter for the input port.

Building a second component

    public static class TellNames {
        private Port<String> outPort;    (1)

        public void tell() {
            outPort.send("Harry");
            outPort.send("Joanne");
            outPort.send("Ron");
            outPort.send("Lily");
        }

        public void setOutPort(Port<String> outPort) {    (2)
            this.outPort = outPort;
        }
    }
  1. We simply declare the Port interface and don’t care about its implementation.

  2. This is just a standard Java setter for the output port.

Connecting the components

Now lets see how to connect components.

    private TellNames tellNames;    (1)
    private SayHello sayHello;

    public Greeter() {
        tellNames = new TellNames();    (2)
        sayHello = new SayHello();

        createConnections();
    }

    private void createConnections() {
        tellNames.setOutPort(sayHello.getInPort());    (3)
    }
  1. The components are declared as normal private fields in the flow class.

  2. The components are constructed in the constructor as you would expect.

  3. The components are connected by simply setting the input port of the receiving component as the output port of the sending component.

Important
The flow class is only active at initialization time. At runtime you won’t step through it in the debugger. Then it only holds the references to its subcomponents so they don’t get garbage collected.

Putting it all together

Our little example flow looks like this:

Greeter

And here is the complete source code:

package org.flowdev.base;

public class Greeter {

    private TellNames tellNames;
    private SayHello sayHello;

    public Greeter() {
        tellNames = new TellNames();
        sayHello = new SayHello();

        createConnections();
    }

    private void createConnections() {
        tellNames.setOutPort(sayHello.getInPort());
    }


    public void greet() {
        tellNames.tell();
    }


    public static class TellNames {
        private Port<String> outPort;

        public void tell() {
            outPort.send("Harry");
            outPort.send("Joanne");
            outPort.send("Ron");
            outPort.send("Lily");
        }

        public void setOutPort(Port<String> outPort) {
            this.outPort = outPort;
        }
    }



    public static class SayHello {
        private Port<String> inPort = name -> {
            System.out.println("Hello, " + name + "!");
        };

        public Port<String> getInPort() {
            return inPort;
        }
    }


    public static void main(String[] args) {
        Greeter greeter = new Greeter();
        greeter.greet();
    }
}

When you run it you should see the following output:

Hello, Harry!
Hello, Joanne!
Hello, Ron!
Hello, Lily!

Lets build a subflow and a reusable component

That first example was nice but two important features are missing:

  1. It won’t scale if we start to build the promised big complex applications in a single flow with thousands of operations. So we want to use subflows in flows.

  2. The operations and flows so far aren’t reusable. To change this we have to abstract away the exact data type that flows through the operation.

Flows can have ports too and function as subflow

First lets change the 'SayHello' component to send the greeting to an output port instead of printing it directly.

SayHello2

And now lets take a look at its implementation.

    public static class SayHello {
        private Port<String> outPort;
        private Port<String> inPort = name -> outPort.send("Hello, " + name + "!");

        public Port<String> getInPort() {
            return inPort;
        }

        public void setOutPort(Port<String> outPort) {
            this.outPort = outPort;
        }
    }

The code should be as expected since it just adds an output port like the one we created for the 'TellNames' component.

Now we create the real 'GreetingsFlow' flow:

GreetingsFlow

And the quite boring code is:

    public static class GreetingsFlow {
        private TellNames tellNames;
        private SayHello sayHello;

        public GreetingsFlow() {
            tellNames = new TellNames();
            sayHello = new SayHello();

            createConnections();
        }

        private void createConnections() {
            tellNames.setOutPort(sayHello.getInPort());
        }

        public void setOutPort(Port<String> outPort) {
            sayHello.setOutPort(outPort);
        }

        public void greet() {
            tellNames.tell();
        }
    }

It is just like our first 'Greeter' flow with the sole exception that it has an output port. No, it doesn’t even have its own output port. Instead it is simply using the output port of the 'SayHello' operation as its output port. This way we don’t have any overhead at runtime! Input ports of flows can be build the same way.

Thus flows with ports are indistinguishable from operations without looking into their implementation and can be used as subcomponents in other flows.

Reusable components

Making a component reusable is a little bit more tricky. On the graphical view you can’t recognize a flow or operation as reusable:

OutputText

In contrast to that the code looks a bit more funny:

    public static class OutputText<T> {    (1)
        public static class Params<T> {    (2)
            public Getter<T, String> getText;
        }

        private final Params<T> params;    (3)
        private Port<T> inPort = this::outputText;

        public OutputText(Params<T> params) {
            this.params = params;
        }

        private void outputText(T data) {
            System.out.println(params.getText.get(data));    (4)
        }

        public Port<T> getInPort() {
            return inPort;
        }
    }
  1. First the class has to be made generic. This enables type checking by the compiler even though the component itself doesn’t know the type.

  2. Now comes the ugliest part of it all: We are using a simple static inner class called 'Params' to store 'Getter’s from and potentially 'Setter’s to the generic data type. This way we don’t have to remember the order of constructor arguments and the code for flows can be generated easier. This pays off when we have a lot of such parameters.

  3. Of course we have to declare the parameters as a field and set them in the constructor.

  4. Every operation is the owner of the data that flows through it from the moment it is called with the data until it sends the data to one of its output ports. So it has to call 'Getter’s only once during normal operation.

Additionally the same generic type has to be used all over the reusable class and its parameter class.

All users of the reusable component will have to change too:

public class Greeter2 {
    private GreetingsFlow greetingsFlow;
    private OutputText<String> outputText;    (1)

    public Greeter2() {
        greetingsFlow = new GreetingsFlow();
        OutputText.Params<String> outputTextParams = new OutputText.Params<>();    (2)
        outputTextParams.getText = data -> data;    (3)
        outputText = new OutputText<>(outputTextParams);    (4)
  1. The reusable component has to be declared with the correct concrete type unless the flow that is using the component is reusable too. Then the generic type of the parent flow has to be used.

  2. The parameter class has to be created.

  3. Finally 'Getter’s and 'Setter’s have to be created. These are simple Java8 lambdas. Using this for Android development is less fun. :-(

  4. The parameters have to be given to the constructor of the component.

Even though the code looks unfamiliar at first, a component can be made reusable as an afterthought with less than one hour of mechanical changes. This usually holds true even if you have quite a lot of users of the component since most of the changes can be copied.

Flows can be made reusable too by using an own parameter class that contains the 'Getter’s and 'Setter’s needed by its components. Parent flows are rarely made reusable more than one or two levels up. Somewhere you have to decide what data type to use.

The advantages of this approach are:

  • Components can be made reusable as an afterthought. So you can wait until you really want to reuse something (YAGNI: You Aint Gonna Need It).

  • No special generic data type needed.

    • Full compatibility with existing libraries.

    • Type safety is preserved.

    • Refactoring tools and usage search work as expected.

    • Memory usage is optimal.

Putting it all together again

Our second example flow looks like this:

Greeter2

With the 'GreetingsFlow' beeing:

GreetingsFlow

And here is the complete source code:

package org.flowdev.base;


public class Greeter2 {
    private GreetingsFlow greetingsFlow;
    private OutputText<String> outputText;

    public Greeter2() {
        greetingsFlow = new GreetingsFlow();
        OutputText.Params<String> outputTextParams = new OutputText.Params<>();
        outputTextParams.getText = data -> data;
        outputText = new OutputText<>(outputTextParams);


        createConnections();
    }

    private void createConnections() {
        greetingsFlow.setOutPort(outputText.getInPort());
    }

    public void greet() {
        greetingsFlow.greet();
    }


    public static class GreetingsFlow {
        private TellNames tellNames;
        private SayHello sayHello;

        public GreetingsFlow() {
            tellNames = new TellNames();
            sayHello = new SayHello();

            createConnections();
        }

        private void createConnections() {
            tellNames.setOutPort(sayHello.getInPort());
        }

        public void setOutPort(Port<String> outPort) {
            sayHello.setOutPort(outPort);
        }

        public void greet() {
            tellNames.tell();
        }
    }



    public static class OutputText<T> {
        public static class Params<T> {
            public Getter<T, String> getText;
        }

        private final Params<T> params;
        private Port<T> inPort = this::outputText;

        public OutputText(Params<T> params) {
            this.params = params;
        }

        private void outputText(T data) {
            System.out.println(params.getText.get(data));
        }

        public Port<T> getInPort() {
            return inPort;
        }
    }



    public static class TellNames {
        private Port<String> outPort;

        public void tell() {
            outPort.send("Harry");
            outPort.send("Joanne");
            outPort.send("Ron");
            outPort.send("Lily");
        }

        public void setOutPort(Port<String> outPort) {
            this.outPort = outPort;
        }
    }



    public static class SayHello {
        private Port<String> outPort;
        private Port<String> inPort = name -> outPort.send("Hello, " + name + "!");

        public Port<String> getInPort() {
            return inPort;
        }

        public void setOutPort(Port<String> outPort) {
            this.outPort = outPort;
        }
    }


    public static void main(String[] args) {
        Greeter2 greeter = new Greeter2();
        greeter.greet();
    }
}

When you run it you should see again the following output:

Hello, Harry!
Hello, Joanne!
Hello, Ron!
Hello, Lily!

Summary

These are the main points about flows I wanted to show:

  • The components that only orchestrate other components (and could potentially be replaced by a single generic component) are called flows.

  • The components doing the real work are called operations.

  • And the connection between components is called a port.

  • The components don’t know each other and are fully independent. They do just one thing and do it well.

  • The performance and memory overhead is negligible because the step from one operation to the next is just a simple method call.

  • Flows are very simple since only the simple 'Port' interface is mandatory.

  • Flows are quite flexible since many input and output ports are possible (including arrays of ports).

  • Flows are easy to debug since you don’t have to step through any framework.

  • Flows can be used as components in other flows. So big and complex flows can be build easily.

  • No matter how high the stack of flows grows: At runtime only operations are active.

  • Operations and flows can be made reusable by using 'Getter’s and 'Setter’s in their creation. This makes the component not only independent of the other components but independent of the exact data type flowing through it too.

  • Operations are quite small (usually half a monitor page, up to one monitor page for special components).

  • Flows can be nicely documented in graphs.

If you want to try out the flowdev way, please take a look at the code. Especially the 'jbase' project since it contains some useful base classes and helpers.