In my previous post I summarized the notation for Flow-Design (FD) diagrams. Now is the time to show you how to translate those diagrams into code. Hopefully you feel how different this is from UML. UML leaves you alone with your sequence diagram or component diagram or activity diagram. They leave it to you how to translate your elaborate design into code. Or maybe UML thinks it´s so easy no further explanations are needed? I don´t know. I just know that, as soon as people stop designing with UML and start coding, things end up to be very different from the design. And that´s bad. That degrades graphical designs to just time waste on paper (or some designer). I even believe that´s the reason why most programmers view textual source code as the only and single source of truth. Design and code usually do not match.
FD is trying to change that. It wants to make true design a first class method in every developers toolchest. For that the first prerequisite is to be able to easily translate any design into code. Mechanically, without thinking. Even a compiler could do it :-) (More of that in some other article.)
Translating to Methods
The first translation I want to show you is for small designs. When you start using FD you should translate your diagrams like this.
Functional units become methods. That´s it. An input-pin becomes a method parameter, an output-pin becomes a return value:
The above is a part. But a board can be translated likewise and calls the nested FUs in order:
In any case be sure to keep the board method clear of any and all business logic. It should not contain any control structures like if, switch, or a loop. Boards do just one thing: calling nested functional units in proper sequence.
What about multiple input-pins? Try to avoid them. Replace them with a join returning a tuple:
What about multiple output-pins? Try to avoid them. Or return a tuple. Or use out-parameters:
But as I said, this simple translation is for simple designs only.
Splits and joins are easily done with method translation:
All pretty straightforward, isn´t it.
But what about wires, named pins, entry points, explicit dependencies? I suggest you don´t use this kind of translation when your designs need these features.
Translating to methods is for small scale designs like you might do once you´re working on the implementation of a part of a larger design. Or maybe for a code kata you´re doing in your local coding dojo. Instead of doing TDD try doing FD and translate your design into methods. You´ll see that way it´s much easier to work collaboratively on designs, remember them more easily, keep them clean, and lessen the need for refactoring.
Translating to Events
Translating FD diagrams to methods does not scale well. To reap all the benefits of FD you should therefore translate designs to Event-Based Components (EBC) as they were called originally. However this term is now deprecated, since the translation does not produce components in the sense of binary units of code. Nevertheless events enter the stage to hook together functional units.
The favored translation results in a class for every functional unit with a method for every input-pin, and an event for every output-pin:
The default names for input-pins and output-pins are Process() and Result. You might find that strange since you´re thinking of classes as “things” with many responsibilities. In FD, though, classes can be very small with just one responsibility. And this responsibility is an action.
It´s not unusual to have a functional unit called “Read lines from text file” which then is translated into a class; why then should the class have a methode called ReadLinesFromTextFile()? Process() is sufficient.
If a functional unit has more than a single input-/output-pin the methods/events surely must have different names which are denoted by pin names:
Boards and parts do not differ in their basic translation. Both become classes (or interfaces, if you like). However, parts you implement yourself in some creative way. They are the workhorses. They contain the domain logic. Boards on the other hand could be generated – or are implemented by you without much thinking. Leave aside any creativity when implementing boards.
Boards do not contain (much) code in the input-pin methods. What they are doing happens in the constructor. Their sole purpose is to connect the functional units nested within them. Thats why input-pins are just delegating the work to be done. And that´s why all nested FUs are injected into a board using ctor injection:
With FD dependencies are (primarily) used to express nesting, i.e. different levels of abstraction. So a board is dependent on its nested functional units.
Within a flow, however, functional units do not (!) depend on one another. FU A does not depend on FU B or vice versa. Also X is independent of any other FUs preceding or following it. That makes testing extremely easy (see below).
Wires in their simplest form are just event handler assignments:
This also makes it easy to translate split/fork:
Board input-/output-pins are treated a bit differently, though. That´s because they are connected to the same kind of pins, input to input, output to output:
A join unfortunately is not that simple. You should create a small standard part to accomplish the task – or you use the join class from the ebclang project at CodePlex. ebclang is an effort to provide tools to help with Flow-Design. And ebcpatterns is a sub-project collecting implementations of parts to solve recurring problems.
Here is how you´d translate a join using the Join<T0, T1> class from ebcpatterns:
Sets of data items
Events can of course fire any number of times. So there is no need to distinguish between one output packet for an input packet and several output packets. Take as an example a functional unit splitting text lines into words:
One word or many… that does not make a difference for the translation of the output-pin to event Action<string>.
But there is a difficulty for any receiver of the output packets. Which word is the last word in a line? If several lines are processed and if it makes a difference to which line a word belongs, then a receiver has no way of associating a word with a line. The only way would be to send a special End-of-Line word as the last word of every line.
Fortunately there is another way of designing this. Just make it clear that output packets consist of several entries:
The star after the typename signifies the packet to be a list of data items. And a list is most simply represented by an IEmumerable<>.
Please note: This also works if the list contains millions of entries. Just don´t create an array and pass it along, but use an iterator (yield return) instead.
Explicit dependencies are translated in a very explicit way. Instead of injecting them into the ctor an interface is used:
This has to advantages:
- Injection does not interfere with any other part of the implementation. It does not force a ctor or an additional parameter to the ctor, it does not require a base class.
- Injection is independent of object creation; injection can take place at any time during start-up of a Flow-Oriented application.
The entry point attribute is likewise translated to the implementation of an interface:
Also making a part configurable is translated to the implementation of an interface:
Testing of functional units is easy:
With board you just do integration tests. Check only if the wiring is correct. Every path through a flow needs to be tested only once. Once you start using tools to generate boards these integration tests are not needed anymore.
With parts do unit tests as usual. Note that you don´t need a mock framework for that anymore because there are no dependencies between parts. Just pay attention to how to check the output:
You need to assign any relevant output event handlers before you call an input-pin method of the part. And don´t do the assert in the event handler because the test would go green even if the handler is not called.
Hosting – Putting it all together
Hosting the code created by this translation usually follows a pattern. It runs through a couple of phases:
- Build: Create all instances of functional units
- Bind: Wire-up the FUs by connecting output- to input-pins
- Inject: Inject explicit dependencies on all FUs implementing IDependsOn<T>
- Configure: Pass the command line args to all parts implementing IConfigurable
- Run: Call the Run() method with the command line args on the sole part implementing IEntryPoint
Build and Bind are put in a sequence to distinguish them; in reality, though, they are intertwined since binding happends also in ctors of boards upon creation.
| posted on Sunday, March 20, 2011 6:02 PM