In the field of software development, there are many terms that need to be defined and categorized. We also need a defined process to get from the requirements to the code. The software universe is intended to help define terms and clarify a process.
Table of contents
Clean Code Developer Values
As part of the Clean Code Developer initiative, we have defined four values:
- Correctness
- Changeability
- Production efficiency
- Continuous improvement
The 45 building blocks, divided into principles and practices, each contribute more or less to the individual values. But the master question remains: through which process do we achieve the values? Repeatedly insisting that the principles must be adhered to is one thing. Wouldn't it be smart to follow a process that leads to clean code more or less "by itself"? We haven't invented a miracle cure. But a few steps are enough to go through an orderly development process. The software universe can help here by showing which aspects need to be taken into account.
The four axes
The software universe consists of four axes that all meet at a center point. On the one hand, the axes serve to define terms and categorize them in a hierarchy. On the other hand, the axes can define a process if you go through them in a certain order. This ensures that the various aspects are all considered.
The illustration shows the software universe with its four axes. The idea is that we start with the domain decomposition from the top until we reach the intersection point in the middle. For a small section of the requirements identified during the domain decomposition, we then go through the other three axes: make a design (bottom), assign the functional units to modules (right) and think through the runtime aspects (left).
Let's start at the top with the domain decomposition.
The domain decomposition
Goal: Agility
This axis is used for Agility. We use the following definition for agility:
Agility means: regular and short-term feedback
The definition is very simple and makes it clear that three things are required to achieve an agile approach.
- The iterative development ensures that the process regularly is run through again and again. In Scrum, the iteration is called Sprint.
- The agile process is based on many small experiments. To ensure that the direction is right and leads to the desired result, it is necessary to work with short deadlines be worked on. In Scrum, iterations typically take 2-3 weeks. Better are 1-2 days or even daily deployments. If you don't believe it, it's best to read the book Accelerate.
- Each iteration must start with Feedback end. This is the only way the team can learn and correct its course again and again. In order for the product owner (PO) to be able to give feedback, he or she must be given punctures or Increments be delivered.
The question answered by this axis is: how do we decompose the requirements in such a way that an agile process is well supported? First insight: the result of the decomposition must lead to Punctures or Increments lead. This rules out horizontal decomposition, as the TOE cannot provide feedback on this.
System
The domain decomposition starts at the top level with the System. The system stands for all requirements that the software should ever fulfill.
Bounded Context
A sufficiently large system should be Bounded Contexts are decomposed. These ensure that dependencies are reduced and decisions can be made within each bounded context without burdening other bounded contexts. The data garbage can symbol indicates that each bounded context has sovereignty over its own data storage.
App
Within a bounded context, the requirements can be distributed over several Apps are distributed. The symbol indicates by the role that the aim is to provide an appropriate user interface for each role. Sometimes a graphical Ui makes sense, sometimes a console application. Sometimes a desktop application does the trick, other times a web or smartphone application. Making everything the same here and forcing all roles into one and the same user interface is often not sensible.
Dialog
Within an app, all requirements within Dialogs realized. These are windows, windows, forms. In one dialog, everything could be implemented on the topic of product search. Another dialog makes it possible to create and change products. By dividing the entire requirements of an app into several dialogs, we ensure that we proceed from the user's perspective and end up with a single cut-through.
Interaction
Within a dialog, the user can Interactions trigger. If the user did not interact with the software, we would not need to write a single line of code. Only the interaction triggers logic that hopefully leads to the desired result. Interactions are triggered, for example, by menu items or clicking on buttons.
The maximum element that is considered in an iteration is an interaction.
Feature
The requirements of an interaction are often too extensive to be realized in one iteration. Consequently, we must be given the opportunity to put things on hold for the time being. We call these Features. For example, the validation of the user input can initially be postponed. The validation feature is therefore not yet implemented. Nevertheless, there is a breakthrough, as we can implement a small section of the dialog, the domain logic and the resource accesses.
I will look at domain decomposition in more detail in a later blog post.
Detailed Design
Goal: Functional requirements
Now that we have broken down the requirements in the first step, we need to think about how to solve the problem. A design is required. We call this part of the design Detailed Design. The detailed design describes how the functional requirements are to be achieved. In other words, detailed design describes the solution to a problem from the perspective of functional requirements.
It is not always possible to completely separate the functional and non-functional requirements in the design. If, for example, huge files are to be handled, this non-functional requirement (files are huge) will be reflected in the design of the functional requirements. This means that the file is not simply read into memory, which would be no problem with small files. The separation of functional and non-functional requirements may therefore seem artificial or even impossible. Nevertheless, it is important to consider both aspects in the process.
The question of how to design is an age-old one. For a long time, the answer was UML. But this works for very few teams. I recommend taking a look at Flow Design.
The module hierarchy
Goal: Changeability and work organization
With the Detailed Design we have described a solution. This consists of functional units that work together to realize the requirements. In order for the software to realize the value of the Changeabilityfulfill the requirements of the individual functional units, we must Modules assign. Module is our umbrella term for method, class, library, package, component and microservice.
It is necessary to plan what Method is realized in which Classes these methods are to be filed, in which Libraries the classes should land, etc.
A detailed description of the module hierarchy can be found in this blog post. Here, too, there is a certain overlap between detailed design and module assignment. Detailed design already considers methods. However, it is important to assign these to the classes, these to the libraries, etc. In this respect, the axis should serve to consciously consider this aspect.
Method
A method is the smallest module. The central question here is Responsibility. With the signature, you should make sure that the method can be easily tested automatically.
Class / File
Methods can be combined into classes. In languages that do not recognize classes or do not enforce them, methods must still be stored in files. The question is the same: do two methods belong in the same class or file? If there is a high probability that changes to one method will also affect the other, this could be an indication of high cohesion. In this case, the methods are stored together. If different responsibilities are involved, it is better to store them separately.
Library
Classes or files can be combined into libraries. At this level, there is a change from textual to binary use.
Package
If a library is enriched with metadata and stored on a package server, dependencies can be recognized, updates can be carried out, etc. This simplifies reuse. This simplifies reuse. Examples of package systems are NuGet, npm, pip, etc.
Component
Contracts are required for implementation based on the division of labor. These contain a common definition that can be implemented simultaneously. The contract ensures that everything fits together, at least syntactically, after implementation. Components are characterized by a separate contract from.
Microservice
If the contract is provided on a platform-neutral basis, we speak of a service or microservice. Different platforms can be integrated at this level. A service in Java can communicate with a service that was implemented in C#.
The hosts
Goal: Non-functional requirements
The last step is to plan where the logic should run. Should everything run in one thread or are several required? The need for multiple threads arises from the non-functional requirements. A desktop application with long-running operations could run in one thread. However, the Ui then freezes as long as the operation has not yet been completed. If the non-functional requirement is that the Ui should also be operable during a long-running operation, there is no way around distributing the logic across several threads. The same applies to Processes, Machinesand Sites. You will only create a distributed application if this results from the non-functional requirements. The functional requirement could be "build accounting software". Only the non-functional requirement that this should be operated simultaneously by several people at different locations leads to a distribution of the logic across several processes and several machines in different sites (if you disregard tinkering with a terminal server...).
Thread
An application can use one or more threads. The reason for multiple threads arises from non-functional requirements. With the introduction of multiple threads, the problem of locking arises. Suddenly, a data structure can be accessed from multiple threads. Locking prevents inconsistencies.
Process
Distributing an application across several processes enables the use of different platforms. This is helpful, for example, if the platform has to be changed for legacy software, e.g. from VB6 to C#/.NET. Another reason may be performance. It may be possible to achieve better utilization of the processors or cores by distributing them across several processes.
The distribution of an application to more than one process leads to the challenge of different address spaces. In order to transfer data from one process to another, it must be serialized and deserialized if it cannot be ensured that the processes have agreed on a common binary representation of the data.
Machine
If an application is to be scaled further, it can be distributed across several machines. The classic intranet application is an example of this. One machine takes on the back-end tasks, while the other machines provide front ends.
The challenge here is addressing. The machines must be connected via a network.
Site
If machines are distributed to different locations, an application can be accessed from several locations. This poses the challenge of security. Firewalls must ensure that only the desired connections are permitted.
Conclusion
The software universe is intended to help classify terms and outline a sequence for the software development process. All four axes must be taken into account in the process. Overlaps between the various aspects cannot be completely avoided. It is important here that the aspects of the four axes are all considered at some point before coding begins.
What are your experiences with the terms and the process?