Orange degree
Principles
Single Level of Abstraction
The Single Level of Abstraction principle focuses on HOW things are done in a code block.
Methods in a class or lines of code in a method can have different levels of abstraction. Code that jumps all the time between different levels of abstraction makes it hard to understand - it becomes difficult to understand what really happens.
It could be that you have a "MyDatabaseAndAllTheStuff" class. You start with adding the connection handling. Then you add some queries for your application. Then you need logging and metrics and add that too. In the end you need to add some tweaks for customer A which are different from customer B.
Another example could be, that you have a method that read, transforms, and returns data - a typical ETL process. You know you have to read the data from a file, so you start with the file IO logic. The file is a CSV file, so you add the parsing for that by reading row by row and split the row into cells and then parse the cells. After that you finally add the logic for you transformation, i.e. summarizing a list of transactions and group them by their categories. The last step will be to output the results as CSV again.
You get the point: both examples sound like a big mess - but familiar too, you have seen such things before.
The principle to solve this is:
Have a similar level of abstraction in a code block
A class should have methods which work on the same level of abstraction and the code in a method should do the same.
Regarding the examples from above, you should make a class for connection handling, another one for queries, logging and metrics. Use different patterns to consider tweaks to fulfill special customer needs. Make separate methods for loading, transforming and formatting the data.
In later grades we will talk about architectural patterns to changes those kind of implementations into much cleaner and more robust solutions.
Single Responsibility Principle
The Single Responsibility Principle is similar to the Single Level of Abstraction. But instead of focusing on HOW something is done in a code block, it focuses on WHAT is done in a code block. The more precise you can explain, in one or a few sentences, what a code block does, the better.
A code block should have a single and precise responsibility
A class or a method shall have one responsibility only. If the name of that class or method describes that responsibility very good, you did a major thing right.
Separation of Concerns
The Separation of Concerns principle is similar to the previous two principles, but this time it is about WHO is with WHOM?
The idea is that separate concerns, separate interests, should not be solved in one class. Often you can go even that far and say, that those things should not be solved in the same module/assembly/library.
By intuition most developers separate fundamental concerns, such as database access, logging, or UI. Often things such as logging, metrics, traces and similar things are bundled together but could be seen as separate concerns. The point is, that there are no hard boundaries how concerns are defined. The gray zones are quite big.
A good rule of thumb is
Could this be used separately? Then separate it.
Separating code by its concern is one action in your toolkit to establish loose coupling in your code. Loose coupling is another principle we will look into later.
Source Code Conventions
Source code conventions can be very controversial. The reason for that is, that it contains two aspects: feelings and correctness.
The feelings part is that a developers feels home if the code uses the style they are used to. From a functional standpoint, the only thing that matters is, what the compiler will made of the code or how it is interpreted by the machine.
The correctness part is, that source code conventions can help to identify problems easier. This is primarily due to good naming conventions and good commenting.
Naming rules
Naming of classes, methods, variables, etc. should help the reader to understand the code. Often are naming rules used to express the scope. Which naming rules are used does not really matter and is often decided by the developer team. The important point is, that all developers of a project follow those rules. Speaking names are usually the best choice.
Instead of
decimal S4(decimal l, int c, decimal p)
{
decimal s = l * p * c;
return s;
}
Better
decimal GetSumByCalculationScheme4(decimal length, int count, decimal price)
{
decimal sum = length * price * count;
return sum;
}
Good commenting
Commenting can be helpful. Too much commenting can distract the reader.
Some developers say, that good code should be written like a good novel and comments should not be required. The idea behind that is, that good code explains itself. I agree to that, partly. But sometimes, code is written so elegant, efficient and due to that so compact, that it is hard to understand when reading it the first time or reading it again after several years. In those cases, inline comments become very important and should be added to the code. Always keep in mind, that maybe not you will be the next one working on that code.
Modern programming languages support documentation comments which are provided with/in the module/assembly/library. That information is then presented to the developer consuming that module/assembly/library in the IntelliSense or code completion of the used IDE. The documentation comments are often presented to the developer while working on the source code of that module/assembly/library. In that case the code documentation is helpful even for internal/private methods.
From my experience I can truly recommend to document everything. Every class, method, parameter, return value, field, everything. This should not be a problem in the era of AI. Explaining the WHAT is done and WHY it is done is great start.
/// <summary>
/// Gets the sum by using the calculation scheme 4.
/// </summary>
/// <param name="length">The length in mm.</param>
/// <param name="count">The number of items.</param>
/// <param name="price">The price per item.</param>
/// <returns>
/// The sum in the same currency as <paramref name="price" />.
/// </returns>
/// <remarks>
/// Find more information about calculation scheme 4 here:
/// <see cref="https://www.example.com/..." />
/// </remarks>
decimal GetSumByCalculationScheme4(decimal length, int count, decimal price)
{
decimal sum = length * price * count;
return sum;
}
Providing examples and explanations of exceptions might be very appreciated.
Practices
Issue Tracking
- If you find a bug in the code - write it down.
- If there are things left to do in your code - write it down.
- If you find technical debt in the code - write it down.
- If you have an idea - write it down.
Things that are written down will not be forgotten.
Some developers write down notes on paper about what is left to do. That is better than nothing, but usually it does not help the team.
Use a system/platform to save the issues. By that, they can be organized, assigned as tasks to team members, or be evaluated later.
There are a lot of such platforms today, in the cloud or for local hosting, such as codeberg and forgejo.
Automated Integration Tests
One risk when changing code is, that those changes causes unwanted side effects which the customer will report as a bug. One way to catch those side effects is to use automated integration tests. With those test, you verify that your code still does what it should do from an outside perspective.
You can use UI test frameworks to test that clicking on a button in your have does what it is supposed to do and you can check that the state of your UI changes as expected.
You can call your REST API with prepared calls to verify that you system works as expected.
It is recommended to use test containers and infrastructure-as-a-code to setup a real-world environment for your integration tests.
We will have a look at unit tests in a later stage.
Read, Read, Read
Reading educates - always!
Read books to understand the theory behind problems and to learn concepts how to solve them. But books about software development are usually deprecated when released. The technical evolution is high in this field. Things are changing quickly.
Read code! Read the code of others to learn from read-world solutions. Read the code mentioned in blogs and talks. Let this inspire you for your own work.
Try out the things you've read about for yourself. Get a deep understanding what these new things do and how that can help you in your work.
Reviews
Reviews can be done in different ways and at different times.
With pair/mob programming you'll get immediately feedback to your code. In the discussions and collaboration with others you can find the optimal solution for a problem. Depending on the group, this can be efficient or inefficient, educational or boring. Some developers use quick-and-dirty to start with and iterate over their code until it fulfills their ambitions. Other developers tempt to write the final code from the beginning. They have a clear picture in mind from the beginning. Those different work styles can collide when coding together in one session.
Code reviews are often the better alternative. The code is reviewed by one or more other persons iteratively. Usually the author writes the code in a feature branch and creates a pull-request that it get merged with the dev branch. Guarding checks require other people to review the code and to approve those changes. Issues with the code will be fixed by the author and reviewed again. With this iterative process, people can work at their individual speed and in their preferred way.