Codifica pulita in Java

1. Panoramica

In questo tutorial, esamineremo i principi di codifica pulita. Capiremo anche perché il codice pulito è importante e come ottenerlo in Java. Inoltre, vedremo se ci sono strumenti disponibili per aiutarci.

2. Che cos'è il codice pulito?

Quindi, prima di passare ai dettagli del codice pulito, capiamo cosa si intende per codice pulito. Onestamente, non può esserci una buona risposta a questo. Nella programmazione, alcune preoccupazioni si estendono e quindi si traducono in principi generali. Ma poi, ogni linguaggio di programmazione e paradigma presenta il proprio insieme di sfumature, che ci impone di adottare pratiche adeguate.

In generale, il codice pulito può essere riassunto come un codice che qualsiasi sviluppatore può leggere e modificare facilmente . Anche se questo può sembrare un'eccessiva semplificazione del concetto, vedremo più avanti nel tutorial come si accumula. Ovunque sentiamo parlare di codice pulito, forse ci imbattiamo in qualche riferimento a Martin Fowler. Ecco come descrive il codice pulito in uno dei posti:

Qualsiasi sciocco può scrivere codice che un computer può capire. I bravi programmatori scrivono codice che gli esseri umani possono capire.

3. Perché dovremmo preoccuparci del codice pulito?

Scrivere codice pulito è una questione di abitudine personale tanto quanto una questione di abilità. Come sviluppatore, cresciamo attraverso l'esperienza e la conoscenza nel tempo. Ma dobbiamo chiederci perché dovremmo investire nello sviluppo di codice pulito, dopo tutto? Capiamo che altri probabilmente troveranno più facile leggere il nostro codice, ma questo incentivo è sufficiente? Scopriamolo!

I principi di codifica pulita ci aiutano a raggiungere molti obiettivi desiderabili relativi al software che intendiamo produrre. Vediamoli per capirlo meglio:

  • Base di codice mantenibile : Qualsiasi software che sviluppiamo ha una vita produttiva e durante questo periodo richiederà modifiche e manutenzione generale. Il codice pulito può aiutare a sviluppare software facile da modificare e mantenere nel tempo.
  • Risoluzione dei problemi più semplice : il software può mostrare comportamenti imprevisti a causa di una varietà di fattori interni o esterni. Spesso può richiedere una rapida inversione di tendenza in termini di correzioni e disponibilità. Il software sviluppato con principi di codifica puliti è più facile da risolvere per i problemi .
  • Onboarding più veloce : il software durante la sua vita vedrà molti sviluppatori crearlo, aggiornarlo e mantenerlo, con gli sviluppatori che si uniscono in diversi momenti nel tempo. Ciò richiede un onboarding più rapido per mantenere alta la produttività e il codice pulito aiuta a raggiungere questo obiettivo.

4. Caratteristiche del Clean Code

I codebase scritti con principi di codifica puliti mostrano diverse caratteristiche che li distinguono. Esaminiamo alcune di queste caratteristiche:

  • Focalizzato : è necessario scrivere una parte di codice per risolvere un problema specifico . Non dovrebbe fare nulla di strettamente non correlato alla risoluzione del problema dato. Questo si applica a tutti i livelli di astrazione nella base di codice come metodo, classe, pacchetto o modulo.
  • Semplice : questa è di gran lunga la caratteristica più importante e spesso ignorata del codice pulito. La progettazione e l'implementazione del software devono essere le più semplici possibile , il che può aiutarci a ottenere i risultati desiderati. L'aumento della complessità in una base di codice li rende soggetti a errori e difficili da leggere e mantenere.
  • Testabile : il codice pulito, pur essendo semplice, deve risolvere il problema in questione. Deve essere intuitivo e facile testare la base di codice, preferibilmente in modo automatizzato . Ciò aiuta a stabilire il comportamento di base della base di codice e semplifica la modifica senza interrompere nulla.

Questi sono ciò che ci aiuta a raggiungere gli obiettivi discussi nella sezione precedente. È utile iniziare a sviluppare con queste caratteristiche in mente rispetto al refactoring in un secondo momento. Ciò porta a un costo totale di proprietà inferiore per il ciclo di vita del software.

5. Codifica pulita in Java

Ora che abbiamo esaminato abbastanza background, vediamo come possiamo incorporare principi di codifica puliti in Java. Java offre molte best practice che possono aiutarci a scrivere codice pulito. Li classificheremo in diversi bucket e capiremo come scrivere codice pulito con esempi di codice.

5.1. Struttura del progetto

Sebbene Java non imponga alcuna struttura di progetto, è sempre utile seguire uno schema coerente per organizzare i nostri file sorgente, test, configurazioni, dati e altri artefatti di codice . Maven, un popolare strumento di compilazione per Java, prescrive una particolare struttura di progetto. Anche se non possiamo usare Maven, è sempre bello attenersi a una convenzione.

Vediamo alcune delle cartelle che Maven suggerisce di creare:

  • src / main / java : per i file di origine
  • src / main / resources : per i file di risorse, come le proprietà
  • src / test / java : per file sorgente di test
  • src / test / resources : per i file di risorse di test, come le proprietà

Simile a questo, ci sono altre strutture di progetto popolari come Bazel suggerito per Java, e dovremmo sceglierne uno in base alle nostre esigenze e al nostro pubblico.

5.2. Convenzione sulla denominazione

Le seguenti convenzioni di denominazione possono fare molto per rendere il nostro codice leggibile e quindi manutenibile . Rod Johnson, il creatore di Spring, sottolinea l'importanza delle convenzioni di denominazione in Spring:

"... se sai cosa fa qualcosa, hai buone possibilità di indovinare il nome della classe Spring o dell'interfaccia per esso ..."

Java prescrive una serie di regole a cui attenersi quando si tratta di denominare qualsiasi cosa in Java. Un nome ben formato non solo aiuta a leggere il codice, ma comunica anche molto sull'intenzione del codice. Facciamo alcuni esempi:

  • Classi : la classe in termini di concetti orientati agli oggetti è un modello per oggetti che spesso rappresentano oggetti del mondo reale. Quindi è significativo usare nomi per nominare le classi che le descrivono sufficientemente:
public class Customer { }
  • Variabili : le variabili in Java acquisiscono lo stato dell'oggetto creato da una classe. Il nome della variabile dovrebbe descrivere chiaramente l'intento della variabile:
public class Customer { private String customerName; }
  • Metodi : i metodi in Java fanno sempre parte delle classi e quindi generalmente rappresentano un'azione sullo stato dell'oggetto creato dalla classe. È quindi utile nominare i metodi usando i verbi:
public class Customer { private String customerName; public String getCustomerName() { return this.customerName; } }

Anche se abbiamo discusso solo di come denominare un identificatore in Java, tieni presente che ci sono ulteriori best practice come l'involucro del cammello, che dovremmo osservare per la leggibilità. Possono esserci più convenzioni relative alla denominazione di interfacce, enumerazioni e costanti.

5.3. Struttura del file di origine

Un file sorgente può contenere diversi elementi. Sebbene il compilatore Java imponga una struttura, una gran parte è fluida . Tuttavia, aderire a un ordine specifico in cui inserire gli elementi in un file sorgente può migliorare significativamente la leggibilità del codice. Esistono diverse guide di stile popolari da cui trarre ispirazione, come una di Google e un'altra di Spring.

Vediamo come dovrebbe apparire un tipico ordinamento degli elementi in un file sorgente:

  • Dichiarazione del pacchetto
  • Importa dichiarazioni
    • Tutte le importazioni statiche
    • Tutte le importazioni non statiche
  • Esattamente una classe di primo livello
    • Variabili di classe
    • Variabili di istanza
    • Costruttori
    • Metodi

Oltre a quanto sopra, i metodi possono essere raggruppati in base alla loro funzionalità o ambito . Non esiste una convenzione valida e l'idea dovrebbe essere decisa una volta e poi seguita in modo coerente.

Vediamo un file sorgente ben formato:

# /src/main/java/com/baeldung/application/entity/Customer.java package com.baeldung.application.entity; import java.util.Date; public class Customer { private String customerName; private Date joiningDate; public Customer(String customerName) { this.customerName = customerName; this.joiningDate = new Date(); } public String getCustomerName() { return this.customerName; } public Date getJoiningDate() { return this.joiningDate; } }

5.4. Spazi bianchi

Sappiamo tutti che è più facile leggere e comprendere brevi paragrafi rispetto a un grande blocco di testo. Non è molto diverso anche quando si tratta di leggere il codice. Gli spazi e le righe vuote ben posizionati e coerenti possono migliorare la leggibilità del codice.

The idea here is to introduce logical groupings in the code which can help organize thought processes while trying to read it through. There is no one single rule to adopt here but a general set of guidelines and an inherent intention to keep readability at the center of it:

  • Two blank lines before starting static blocks, fields, constructors and inner classes
  • One blank line after a method signature that is multiline
  • A single space separating reserved keywords like if, for, catch from an open parentheses
  • A single space separating reserved keywords like else, catch from a closing parentheses

The list here is not exhaustive but should give us a bearing to head towards.

5.5. Indentation

Although quite trivial, almost any developer would vouch for the fact that a well-indented code is much easier to read and understand. There is no single convention for code indentation in Java. The key here is to either adopt a popular convention or define a private one and then follow it consistently across the organization.

Let's see some of the important indentation criteria:

  • A typical best practice is to use four spaces, a unit of indentation. Please note that some guidelines suggest a tab instead of spaces. While there is no absolute best practice here, the key remains consistency!
  • Normally, there should be a cap over the line length, but this can be set higher than traditional 80 owing to larger screens developers use today.
  • Lastly, since many expressions will not fit into a single line, we must break them consistently:
    • Break method calls after a comma
    • Break expressions before an operator
    • Indent wrapped lines for better readability (we here at Baeldung prefer two spaces)

Let's see an example:

List customerIds = customer.stream() .map(customer -> customer.getCustomerId()) .collect(Collectors.toCollection(ArrayList::new));

5.6. Method Parameters

Parameters are essential for methods to work as per specification. But, a long list of parameters can make it difficult for someone to read and understand the code. So, where should we draw the line? Let's understand the best practices which may help us:

  • Try to restrict the number of parameters a method accepts, three parameters can be one good choice
  • Consider refactoring the method if it needs more than recommended parameters, typically a long parameter list also indicate that the method may be doing multiple things
  • We may consider bundling parameters into custom-types but must be careful not to dump unrelated parameters into a single type
  • Finally, while we should use this suggestion to judge the readability of the code, we must not be pedantic about it

Let's see an example of this:

public boolean setCustomerAddress(String firstName, String lastName, String streetAddress, String city, String zipCode, String state, String country, String phoneNumber) { } // This can be refactored as below to increase readability public boolean setCustomerAddress(Address address) { }

5.7. Hardcoding

Hardcoding values in code can often lead to multiple side effects. For instance, it can lead to duplication, which makes change more difficult. It can often lead to undesirable behavior if the values need to be dynamic. In most of the cases, hardcoded values can be refactored in one of the following ways:

  • Consider replacing with constants or enums defined within Java
  • Or else, replace with constants defined at the class level or in a separate class file
  • If possible, replace with values which can be picked from configuration or environment

Let's see an example:

private int storeClosureDay = 7; // This can be refactored to use a constant from Java private int storeClosureDay = DayOfWeek.SUNDAY.getValue()

Again, there is no strict guideline around this to adhere to. But we must be cognizant about the fact the some will need to read and maintain this code later on. We should pick a convention that suits us and be consistent about it.

5.8. Code Comments

Code comments can be beneficial while reading code to understand the non-trivial aspects. At the same time, care should be taken to not include obvious things in the comments. This can bloat comments making it difficult to read the relevant parts.

Java allows two types of comments: Implementation comments and documentation comments. They have different purposes and different formats, as well. Let's understand them better:

  • Documentation/JavaDoc Comments
    • The audience here is the users of the codebase
    • The details here are typically implementation free, focusing more on the specification
    • Typically useful independent of the codebase
  • Implementation/Block Comments
    • The audience here is the developers working on the codebase
    • The details here are implementation-specific
    • Typically useful together with the codebase

So, how should we optimally use them so that they are useful and contextual?

  • Comments should only complement a code, if we are not able to understand the code without comments, perhaps we need to refactor it
  • We should use block comments rarely, possibly to describe non-trivial design decisions
  • We should use JavaDoc comments for most of our classes, interfaces, public and protected methods
  • All comments should be well-formed with a proper indentation for readability

Let's see an example of meaningful documentation comment:

/** * This method is intended to add a new address for the customer. * However do note that it only allows a single address per zip * code. Hence, this will override any previous address with the * same postal code. * * @param address an address to be added for an existing customer */ /* * This method makes use of the custom implementation of equals * method to avoid duplication of an address with same zip code. */ public addCustomerAddress(Address address) { }

5.9. Logging

Anyone who has ever laid their hands onto production code for debugging has yearned for more logs at some point in time. The importance of logs can not be over-emphasized in development in general and maintenance in particular.

There are lots of libraries and frameworks in Java for logging, including SLF4J, Logback. While they make logging pretty trivial in a codebase, care must be given to logging best practices. An otherwise done logging can prove to be a maintenance nightmare instead of any help. Let's go through some of these best practices:

  • Avoid excessive logging, think about what information might be of help in troubleshooting
  • Choose log levels wisely, we may want to enable log levels selectively on production
  • Be very clear and descriptive with contextual data in the log message
  • Use external tools for tracing, aggregation, filtering of log messages for faster analytics

Let's see an example of descriptive logging with right level:

logger.info(String.format("A new customer has been created with customer Id: %s", id));

6. Is That All of It?

While the previous section highlights several code formatting conventions, these are not the only ones we should know and care about. A readable and maintainable code can benefit from a large number of additional best practices that have been accumulated over time.

We may have encountered them as funny acronyms over time. They essentially capture the learnings as a single or a set of principles that can help us write better code. However, note that we should not follow all of them just because they exist. Most of the time, the benefit they provide is proportional to the size and complexity of the codebase. We must access our codebase before adopting any principle. More importantly, we must remain consistent with them.

6.1. SOLID

SOLID is a mnemonic acronym that draws from the five principles it sets forth for writing understandable and maintainable software:

  • Single Responsibility Principle: Every interface, class, or method we define should have a clearly defined goal. In essence, it should ideally do one thing and do that well. This effectively leads to smaller methods and classes which are also testable.
  • Open-Closed Principle: The code that we write should ideally be open for extension but closed for modification. What this effectively means is that a class should be written in a manner that there should not be any need to modify it. However, it should allow for changes through inheritance or composition.
  • Liskov Substitution Principle: What this principle states is that every subclass or derived class should be substitutable for their parent or base class. This helps in reducing coupling in the codebase and hence improve reusability across.
  • Interface Segregation Principle: Implementing an interface is a way to provide a specific behavior to our class. However, a class must not need to implement methods that it does not require. What this requires us to do is to define smaller, more focussed interfaces.
  • Dependency Inversion Principle: According to this principle, classes should only depend on abstractions and not on their concrete implementations. This effectively means that a class should not be responsible for creating instances for their dependencies. Rather, such dependencies should be injected into the class.

6.2. DRY & KISS

DRY stands for “Don's Repeat Yourself”. This principle states that a piece of code should not be repeated across the software. The rationale behind this principle is to reduce duplication and increase reusability. However, please note that we should be careful in adopting this rather too literally. Some duplication can actually improve code readability and maintainability.

KISS stands for “Keep It Simple, Stupid”. This principle states that we should try to keep the code as simple as possible. This makes it easy to understand and maintain over time. Following some of the principles mentioned earlier, if we keep our classes and methods focussed and small, this will lead to simpler code.

6.3. TDD

TDD stands for “Test Driven Development”. This is a programming practice that asks us to write any code only if an automated test is failing. Hence, we've to start with the design development of automated tests. In Java, there are several frameworks to write automated unit tests like JUnit and TestNG.

The benefits of such practice are tremendous. This leads to software that always works as expected. As we always start with tests, we incrementally add working code in small chunks. Also, we only add code if the new or any of the old tests fail. Which means that it leads to reusability as well.

7. Tools for Help

Writing clean code is not just a matter of principles and practices, but it's a personal habit. We tend to grow as better developers as we learn and adapt. However, to maintain consistency across a large team, we've also to practice some enforcement. Code reviews have always been a great tool to maintain consistency and help the developers grow through constructive feedback.

However, we do not necessarily have to validate all these principles and best practices manually during code reviews. Freddy Guime from Java OffHeap talks about the value of automating some of the quality checks to end-up with a certain threshold with the code quality all the time.

There are several tools available in the Java ecosystem, which take at least some of these responsibilities away from code reviewers. Let's see what some of these tools are:

  • Code Formatters: Most of the popular Java code editors, including Eclipse and IntelliJ, allows for automatic code formatting. We can use the default formatting rules, customize them, or replace them with custom formatting rules. This takes care of a lot of structural code conventions.
  • Static Analysis Tools: There are several static code analysis tools for Java, including SonarQube, Checkstyle, PMD and SpotBugs. They have a rich set of rules which can be used as-is or customized for a specific project. They are great in detecting a lot of code smells like violations of naming conventions and resource leakage.

8. Conclusion

In this tutorial, we've gone through the importance of clean coding principles and characteristics that clean code exhibits. We saw how to adopt some of these principles in practice, which developing in Java. We also discussed other best practices that help to keep the code readable and maintainable over time. Finally, we discussed some of the tools available to help us in this endeavor.

Per riassumere, è importante notare che tutti questi principi e pratiche servono a rendere il nostro codice più pulito. Questo è un termine più soggettivo e quindi deve essere valutato contestualmente.

Sebbene siano disponibili numerose serie di regole da adottare, dobbiamo essere consapevoli della nostra maturità, cultura e requisiti. Potremmo dover personalizzare o, per quella materia, escogitare un nuovo insieme di regole del tutto. Tuttavia, qualunque sia il caso, è importante rimanere coerenti in tutta l'organizzazione per trarne i benefici.