Why Software Should Be Like Onions
I have now been writing full applications for well over 18 months now and I can honestly say that my development has changed (and improved!) significantly in this time.
When I look at my software now I see two major areas of improvement. It is far more readable and decoupled than in the past which has been because I have made my software like onions, it has layers.
I will start by saying these are not rules, these are not fixed but these are common.
- “Process” or “Application” level – This is essentially your application framework. This handles connecting the dots, moving data between asynchronous processes and is really the scaffolding of the application.
- “Problem” level – Also often termed business logic. This is the part that is very application specific and really defines the function and process of the application. This level should all be described in the terms of the problem you are trying to solve
- The IO layer – This is the layer that connects your problem to the outside world. The important distinction here is that the API/Interface is defined by the problem level
- Reuse code and drivers
Essentially each layer is composed of the elements below and the IO layer is composed of libraries, interfaces and reuse code.
Why It Works – Readability
If I gave you a blank VI, could you rewrite your application as is? But you’ve done it once? We never write a whole application though, we focus on areas at a time. The layers hide the complexity below them from view, allowing focus on the task at hand.
The process layer should only show how the problem level components talk to each other.
The problem layer should only show how data is taken from a input, processed and sent to the relevant outputs.
The IO layer should show the specific combinations of driver calls to achieve the desired input or output.
This works because we can only hold so much of the program in our heads at once. By having driver calls and decision logic on one diagram our mental model of what is happening has to be much harder making us work slower and more prone to mistakes.
Why It Works – Dependendency Management
Similarly when there are different parts of the program coupled together it is hard for us to comprehend the knock on effects of changes.
By having these layers contained we have well defined interfaces and can track changes. Change the driver? We only need to look at the IO layer. Changing how processes communicate? You only need to worry about the process layer.
We can also identify uses of dependency inversion more easily. This means that rather than the problem domain being coupled to a driver directly, we define an interface (my implementation uses classes and dynamic dispatch) which is owned and designed from the perspective of the problem level. This is far less likely to change than the driver calls themselves and protects the problem logic from the IO logic.
The final benefit is more practical. I don’t let dependencies cross multiple boundaries, classic cases are type defs that end up spreading throughout an application. This reduces the dependencies which allows for faster load, editing and less “random” recompiles in LabVIEW. Sometimes this can be hard, it feels inefficient to convert between very similar type defs, but generally the computer cycles are cheap and the benefits in the software are worth it.
For me, increased awareness of these have been the single biggest gains in terms of the way I design my applications, I hope it might help you as well. I suspect these ideas need a bit of refining and distilling and would love your feedback or thoughts.