This workshop aims to spread coding best practices to young developers (interns, < 3y XP ,..). For instance, we could provide exercices about logging or error handling best practices. Through a real life inspired application, the trainees could deal with coding basics: clean code, OOP principles, TDD and such like (see below) while submitting their first Pull Reque
During this workshop we will cover:
The source code is available on GitHub.
Feel free to raise any issues or participate!
Skill | Level |
novice | |
novice |
You MUST have set up these tools first:
Here are commands to validate your environment:
Java
java -version
openjdk version "21.0.1" 2023-10-17 LTS
OpenJDK Runtime Environment Temurin-21.0.1+12 (build 21.0.1+12-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.1+12 (build 21.0.1+12-LTS, mixed mode, sharing)
Maven
❯ mvn --version
Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937)
Maven home: /home/alexandre/.sdkman/candidates/maven/current
Java version: 21.0.4, vendor: Eclipse Adoptium, runtime: /home/alexandre/.sdkman/candidates/java/21.0.4-tem
Default locale: en, platform encoding: UTF-8
OS name: "linux", version: "5.15.153.1-microsoft-standard-wsl2", arch: "amd64", family: "unix"
Clean code is a based on the famous book Clean code by R. MARTIN. It aims to help (not only) beginners to embrace the principles described in the book.
Clean code refers to code that is easy to understand, maintain, and extend. It is written in a way that is clear and concise, making it easier for other developers to read and work with.
Robert C. MARTIN published the famous book "Clean Code: A Handbook of Agile Software Craftsmanship".
We strongly recommend reading it. It's pretty straightforward is goes direct to the point.
Here are some of the most important principles and rules of clean code:
Use descriptive and meaningful names for variables, functions, classes, and other entities. Names should convey the purpose and usage of the entity.
We should choose a name that specifies what is being measured and the unit of that measurement. We should use pronouncable words (eg. functionPZQ
). There are some exceptions (eg. SSN
).
int d; // elapsed time in days.
should be transform into
int elaspedTimeInDays
As variable names, class names should be representative of business vocabulary. Classes and objects should have noun or noun phrase names like Customer
, WikiPage
, Account
, and AddressParser
. Avoid words like Manager
, Processor
, Data
, or Info
in the name of a class. A class name should NOT be a verb.
Methods should have verb or verb phrase names like postPayment
, deletePage
, or save
. Accessors, mutators, and predicates should be named for their value and prefixed with get, set.
Pick one word for one abstract concept and stick with it. For instance, it's confusing to have fetch, retrieve, and get as equivalent methods of different classes. How do you remember which method name goes with which class? Likewise, it's confusing to have a controller and a manager and a driver in the same code base. What is the essential difference between a DeviceManager
and a Protocol- Controller
?
Each class or function should have only one reason to change, meaning it should have only one job or responsibility.
Software entities should be open for extension but closed for modification. This means you should be able to add new functionality without changing existing code.
Subtypes must be substitutable for their base types without altering the correctness of the program.
Clients should not be forced to depend on interfaces they do not use. Split large interfaces into smaller, more specific ones.
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
Avoid code duplication by abstracting common functionality into reusable components.
Write simple and straightforward code. Avoid unnecessary complexity.
Do not add functionality until it is necessary. Avoid over-engineering.
Different parts of the code should handle different concerns. For example, business logic should be separated from data access logic.
Use comments and documentation to explain why certain decisions were made, not just what the code is doing. However, strive to write self-explanatory code that minimizes the need for comments.
The proper use of comments is to compensate for our failure to express ourselves in code. So when you find yourself in a position where you need to write a comment, think it through and see whether there isn't some way to turn the tables and express yourself in code.
Code changes and evolves, but comments don't always follow them and And all too often the comments get separated from the code they describe and become orphaned blurbs of ever-decreasing accuracy
/*
* Copyright (c) Worldline 2017 - All Rights Reserved.
* Unauthorized copying of this file, via any medium is strictly prohibited
* Proprietary and confidential
*
*/
Ex: For explain the return value of an abstract method
/**
*Returns an instance of Responder being tested
*/
protected abstract Responder responderInstance();
Sometimes it is useful to provide basic information with a comment. For example :
/**
*return Coordinates in cartesian coordinate system
*/
public Coordinates getCoordinates();
But it is better to use the name of the function to convey the information as much as possible.
public Coordinates getCartesianCoordinates();
Ex:
// Don't run unless you
// have some time to kill
public void testWithBigFile(){
writeLinesToFile(10000000);
}
Handle errors gracefully and provide meaningful error messages. Use exceptions for exceptional conditions and avoid using them for control flow.
NULL
If you return null values as
public List<Geometry> getGeometries(){
return geometries;
}
You have to handle null
in your code.
if( myclass.getGeometries()==null){
// do some stuff
}
}
Prefer this code
public List<Geometry> getGeometries(){
return Optional.ofNullable(geometries).orElse(Collections::emptyList);
}
In this way you don't have to handle null
and prevent NullPointerException
.
Don't use exceptions for testing ( number format, array size,...)
try {
int i = 0;
while(true){
range[i++].climb();
}
} catch (ArrayIndexOutOfBoundsException e) {}
There are some things wrong with this reasoning:
Exceptions are, as their name implies, to be used only for exceptional conditions; they should never be used for ordinary control flow.
If an exception cannot be recoverable (eg. database connection is broken), don't use checked exception. However, catch the exceptions at the boudary of your system to encapsulate them into appropriate errors for your client.
Use checked exceptions for conditions from which the caller can reasonably be expected to recover.
Prefer RuntimeException
. If you don't have to check an exception, let it throw to the caller. Catch it on the boundary of your system to filter and encapsulate the error with appropriate error codes
There are plenty standard interfaces. Use them instead creating custom exceptions.
You can find below some examples
Exception | Occasion for Use |
IllegalArgumentException | Non-null parameter value is inappropriate |
IllegalStateException | Object state is inappropriate for method invocation |
NullPointerException | Parameter value is null where prohibited |
IndexOutOfBoundsException | Index parameter value is out of range |
ConcurrentModificationException | Concurrent modification of an object has been detected where it is prohibited |
UnsupportedOperationException | Object does not support method |
Document on your javadoc all the exceptions thrown by your method
/**
* ....
*
* @throws IndexoutofBoundsException : the index is too high
*/
public E get(int index){
ListIterator<E> iterator = listIterator(index);
try{
return i.next();
}catch(NoSuchElementException e){
throw new IndexoutofBoundsException("index :"+index);
}
}
Write automated tests to verify the correctness of the code. Use Test-Driven Development (TDD) to write tests before writing the actual code.
By now everyone knows that TDD asks us to write unit tests first, before we write production code. But that rule is just the tip of the iceberg.
Consider the following three laws:
Clean tests follow five other rules that form the above acronym:
They should run quickly. When tests run slow, you won't want to run them frequently. If you don't run them frequently, you won't find problems early enough to fix them easily. You won't feel as free to clean up the code.
One test should not set up the conditions for the next test. You should be able to run each test independently and run the tests in any order you like. When tests depend on each other, then the first one to fail causes a cascade of downstream failures, making diagnosis difficult and hiding downstream defects.
You should be able to run the tests in the production environment, in the QA environment, and on your laptop while riding home on the train without a network. If your tests aren't repeatable in any environment, then you'll always have an excuse for why they fail. You'll also find yourself unable to run the tests when the environment isn't available.
Either they pass or fail. You should not have to read through a log file to tell whether the tests pass. You should not have to manually compare two different text files to see whether the tests pass. If the tests aren't self-validating, then failure can become subjective and running the tests can require a long manual evaluation
Test one assertion by method in your test. Use this naming convention
@Test
void should_get_a_lastkownmileage() throws Exception {
...
}
[...]
@Test
public void should_create_partner_and_relations() {
...
}
In a method, your test should be organized using this following pattern
@Test
void should_get_a_lastkownmileage() throws Exception {
// given
// the context
// when
// the user apply some actions
//then
// we should have this result
}
Continuously improve the code by refactoring it. Refactoring involves changing the internal structure of the code without changing its external behavior to make it more readable and maintainable.
Follow a consistent coding style and formatting rules to make the code more readable.
You should choose a set of simple rules that govern the format of your code, and then you should consistently apply those rules.
If you are working on a team, then the team should agree to a single set of formatting rules and all members should comply.
How big should a source file be ?
it should be as small as you can. Small files are usually easier to understand than large files are.
for example FitNesse is close to 50 000 lines, and most of the files are 200 lines long with an upper limit of 500.
Think of a well-written newspaper article. You read it vertically. At the top you expect a headline that will tell you what the story is about and allows you to decide whether it is something you want to read. The first paragraph gives you a synopsis of the whole story.As you continue downward, the details increase.
We would like a source file to be like a newspaper article. The name should be simple but explanatory. It must tell you if you are in the right module or not.
Each group of lines represents a complete thought. those thoughts should be separated from each other with blank lines.
A source file is a hierarchy rather like an outline. To make this hierarchy of scopes visible, we indent the lines of source code in proportion to their position in the hierarchy. Statements at the level of the file, such as most class declarations, are not indented at all. Methods within a class are indented one level to the right of the class. Implementations of those methods are implemented one level to the right of the method declaration. Block implementations are implemented one level to the right of their containing block, and so on.
How meaningful is a Git commit?
This is a common issue in software development: you create a branch, make your changes, and then commit them in Git. Ideally, each commit should be self-sufficient, serving as a documentation update, a bug fix, a new feature, etc.
Well-structured and self-contained commits enhance the readability of the Git history. They also simplify the process of reverting a commit if an issue arises (using git revert
) or transferring a specific change to a different maintenance branch (using git cherry-pick
).
However, in practice, especially under tight deadlines, we often end up with commits that combine multiple changes or branches filled with several commits that should actually be a single one.
Example:
Fixed stuff
The conventional commits specification provides an easy way to create explicit messages
The commit message should be structured as follows:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
The commit contains the following structural elements, to communicate intent to the consumers of your library:
fix
: a commit of the type fix patches a bug in your codebase (this correlates with PATCH in Semantic Versioning).feat
: a commit of the type feat introduces a new feature to the codebase (this correlates with MINOR in Semantic Versioning).BREAKING CHANGE
: a commit that has a footer BREAKING CHANGE:, or appends a ! after the type/scope, introduces a breaking API change (correlating with MAJOR in Semantic Versioning). A BREAKING CHANGE can be part of commits of any type.fix:
and feat:
are allowed, for example @commitlint/config-conventional
(based on the Angular convention) recommends build:, chore:, ci:, docs:, style:, refactor:, perf:, test:
, and others.BREAKING CHANGE:
may be provided and follow a convention similar to git trailer format.Conventional commits provide a clear and structured way to write commit messages. This consistent format makes it easier for developers to understand the purpose of each commit at a glance.
Conventional commits can be parsed by tools, and a very nice use-case is that of generating release changelogs.
With a common format for commit messages, team members can quickly grasp the context and intent of changes made by others. This fosters better communication and collaboration within a team, especially in larger projects.
Below some examples of Git commit messages using this specification. You could gere more here.
Each of these examples follows the conventional commits format of
, making it clear what was changed, where it was changed, and why it matters.
feat(user-auth): add OAuth2 support for user login
fix(api): resolve error handling in user profile endpoint
docs(README): update installation instructions for better clarity
refactor(components): simplify Props validation in Button component
perf(loader): improve loading time by optimizing image assets
chore(package): update dependencies to latest versions
test(profile): add unit tests for profile update functionality
style(Button): adjust padding and fonts for better UI consistency