1. Clean Programming
Principi guida, tecniche e consigli per produrre codice facile da
scrivere, leggere e manutenere
25 January 2020
Davide Muzzarelli
Developer consultant
6. Code sink (1)
Pochi o nessun commento
Commenti ridondanti o completamente inutili
Pezzi di codice commentato in giro
Nessun test
Ok, qualche test, ma nesunno unit test
Funzioni con troppi metodi
Funzioni che modificano le variabili di input
Funzioni troppo lunghe
Troppi livelli d'innestamento
Due o più linguaggi nello stesso file
Righe troppo lunghe 6
7. Code sink (2)
One liners e codice complicato
Nomi troppo corti e acronimi
Nomi troppo astratti o non esplicativi
Inconsistenza nei nomi
Classi e funzioni che fanno troppe cose
Sovra-ingegnerizzazione
Variabili globali
Valori hard coded
Espressioni in forma negativa
Abuso di mixin ed ereditarietà
Notazione ungherese 7
8. Il codice che ci piace
Leggibile
Manutenibile
Riusabile
Testabile 8
9. La più importante è la leggibilità
"Any fool can write code that a computer can understand. Good programmers write
code that humans can understand." - Martin Fowler
"If you want your code to be easy to write, make it easy to read" - Robert C. Martin 9
10. Esperienza personale
Risultati ottenuti su piccoli team (fino a 15 persone l'uno):
Codice più efficiente
Test notevolmente più veloci
Riduzione dei bug
Meno richieste di riscrivere codice
Riduzione dei tempi di sviluppo
Stesso numero di righe
Meno WTF che volavano
Più voglia di lavorare 10
15. L'importanza dei nomi - Lunghezza (1)
Nomi sintetici, ma abbastanza lunghi per essere esaustivi.
Evitare gli acronimi. Eccezioni che confermano la regola: acronimi universali.
Bad:
inv = ... // Invoice o Inventory?
pnt = ... // Point o Pointer?
cnt = ... // Count o Counter?
srv = ... // Server o Service?
parser = ... // Di cosa?
bid = ... // Company ID o Customer ID oppure un'offerta?
tmp = ... // Senza significato
Good:
i18n = ... // Internazionalization
cmd = ... // Command
server = ...
sql_parser = ...
customer_id = ...
14
16. L'importanza dei nomi - Lunghezza (2)
Quando il linguaggio è tipato, e la funzione fa solo una cosa, si possono usare variabili di
una sola lettera.
func main() {
r := strings.NewReader("Clear is better than clever")
p := make([]byte, 4)
for {
n, err := r.Read(p)
if err == io.EOF {
break
}
fmt.Println(string(p[:n]))
}
}
15
17. L'importanza dei nomi - Notazione ungara
Evitare la notazione ungara (prefisso che indica il tipo di variabile).
Bad:
// Pointer (p) to a zero-terminated (z) string (s) buffer
var pszBuffer *Buffer := &new(Buffer())
Good:
var buffer *Buffer := &strings.Buffer()
Con tutto il rispetto per Charles Simonyi 16
18. L'importanza dei nomi - Funzioni (1)
Le funzioni esprimono azioni.
Usare i verbi dove possibile.
count_users()
enable_feature()
activate()
execute()
17
19. L'importanza dei nomi - Funzioni (2)
Non abusare dei get.
Con get ci si aspetta di ottenere una variabile già a disposizione.
Alternative che rivelano cosa fanno:
fetch()
download()
find()
compute()
search()
18
20. L'importanza dei nomi - Booleani
Usare affermazioni o stati.
Usare la forma positiva ove possibile.
Ecco alcune idee:
is_active = True
available = False
can_skip = True
use_ssl = False
keep_logs = True
19
21. L'importanza dei nomi - Interfacce
Preferire il suffisso -er.
Interfacce:
Writer
Reader
Serializer
Speaker
20
22. L'importanza dei nomi - Librerie
Evitare nomi che potrebbero voler usare gli utilizzatori della libreria.
Bad:
buffer
buffers
cache
datetime
Good:
buffering
iobuf
caching
clock
21
23. L'importanza dei nomi - Ultimi consigli
Evitare battute e nomi ridicoli
Usare delle metafore condivise col team
Quando una funzione fa troppe cose è difficile dargli un nome, conviene dividerla 22
25. Estetica - Consigli
Usare lo stile scelto in maniera consistente in tutto il team.
Gli occhi leggono più velocemente quando la formattazione è uniforme e verticale.
Scegliere una guida di riferimento come quelle di Google
Impostare un code formatter ad ogni salvataggio
Sfruttare lo spazio in verticale, non in orizzontale
Rimanere entro le 80 colonne 24
26. Estetica - Codice compatto
Il codice compatto consuma energie mentali.
Bad:
fibonacci = lambda n: (n in (0,1) and [n] or [fib(n-1) + fib(n-2)])[0]
Good:
def fibonacci(n):
if n in (0,1):
return n
return fib(n-1) + fib(n-2)
25
29. Singolo scopo
Ogni elemento del programma deve fare una e una cosa sola.
Il codice risulta più breve
Si riesce a riusare un sacco di codice
I test diventano facili da scrivere e veloci da eseguire
E' più facile nominare le cose
Il refactoring diventa facilissimo 28
30. Singolo scopo - Astrazione
Immagina una view che fa troppe cose:
1. Prende i dati dal form
2. Valida i dati
3. Esegue delle operazioni
4. Salva i dati su database
5. Renderizza l'HTML finale
Ogni passaggio è un buon candidato per diventare una funzione separata.
Mantenersi sullo stesso livello di astrazione delegando i dettagli. 29
31. Singolo scopo - Esempio
Bad:
def users_view(request):
if request.method == 'GET':
users = User.objects.get_all()
...
return Template.render('users.html', context)
elif request.method == 'POST':
for user_data in request.POST[users]:
user = User(name=user_data.name, email=user_data.email)
user.save()
...
return Template.render('users_uploaded.html', context)
Good:
def users_view(request):
if request.method == 'GET':
return list_users()
elif request.method = 'POST':
return add_new_users(request)
30
33. Variabili - Uso breve
Usare le variabili per il più breve tempo possibile:
Liberano la memoria più velocemente
Alta probabilità di sfruttare lo stack
Si riduce il rischio di memory leak
A volte i compilatori e gli interpreti sono in grado di ottimizzarle ulteriormente
NB: Spesso copiare una variabile è più efficiente che usare un puntatore. 32
34. Variabili - Loop
Non assegnare il valore di una variabile fintanto che non la si usa.
Creare le variabili di stato immediatamente prima del loop.
Bad:
counter = 1
... // Some code
for user in users:
...
counter += 1
Good:
33
35. ... // Some code
counter = 1
for user in users:
...
counter += 1
36. Variabili - Globali
Evitarle.
Riducono la leggibilità del codice perché lontane da dove vengono usate
L'accesso è più lento per via dello scoping
Spesso non sono utilizzabili in ambiente multi-thread o multi-process
Possono richiedere un meccanismo di sincronizzazione rallentando così
l'applicazione
Rendono il debugging più difficile perché è più complicato trovare cosa ha
modificato il valore
Costringono all'uso di integration test invece dei più semplici unit test
Rendono i refactoring molto difficili
Sono rari i casi in cui sono necessari; logging e metering sono due di questi. 34
37. Variabili - Costanti
Evitare i valori hard coded. Usare sempre le costanti.
Bad:
worker.pay_per_hour * 8 * 5
Good:
HOURS_PER_DAY = 8
WORKING_DAYS_PER_WEEK = 5
worker.pay_per_hour * HOURS_PER_DAY * WORKING_DAYS_PER_WEEK
35
39. Funzioni - Parametri (1)
Quando i parametri sono tanti i test si complicano e il codice non si riesce più a riusare.
Le migliori funzioni hanno zero parametri.
Tre parametri cominciano ad essere troppi.
Dividere la funzione in più funzioni specializzate.
Unire i parametri in oggetti. 37
40. Funzioni - Parametri (2)
Quando i parametri sono troppi:
Forse si vogliono fare troppe cose, dividere il codice in più funzioni
Raccogliere i parametri omogenei in un oggetto:
make_circle(x, y, radius) # Bad
make_circle(Point, radius)) # Good
La funzione potrebbe avere più senso dentro ad un piccolo oggetto
In caso di flag, dividere la funzione in due diverse:
# Bad
render(json=False)
# Good
render_html()
render_json()
38
41. Funzioni - Effetti collaterali
Evitare gli effetti collaterali.
Bad:
def check_password(user, password):
encrypted = crypt.crypt(password, PREFIX + SALT)
if encrypted != self.password:
return False
user.is_logged = True # <-- Bad bad bad!!
return True
Se una funzione deve cambiare lo stato di un oggetto è il caso di implementare un
metodo. 39
43. Classi
Una classe è definita per il suo comportamento, non per i suoi valori.
E' perfettamente lecito avere classi anche con un solo metodo.
Le classi senza metodi si chiamano DTO (Data Transfer Object).
Evitare i fat objects (errore comune usando gli ORM).
E' più facile testare e riutilizzare il codice quando è diviso in tante piccole classi. 41
44. Classi - Gerarchia
La gerarchia migliore è nessuna gerarchia.
Evitare di ereditare due o più classi contemporaneamente.
Evitare le classi mixin: complicano il codice e lo rendono estremamente rigido.
Soluzione: comporre è meglio che ereditare. 42
45. File sorgenti
Evitare di usare più linguaggi nello stesso file.
Il codice si complica
Spesso si finisce per identare troppo
Leggere è più faticoso
L'evidenziazione della sintassi può non essere disponibile
Modifiche e rifattorizzazioni richiedono più tempo
Non si può lavorare in due sullo stesso codice 43
47. Controllo del flusso - Yoda
Evitare la Yoda notation.
Bad:
if (NULL == user)
Good:
if (user == NULL)
45
48.
49. Controllo del flusso - Precedenza (1)
Negli if-else preferire i casi positivi all'inizio.
Bad:
if (a != b):
# Not equal
else:
# Equal
if (!user.is_admin()):
# Not admin
Good:
if (a == b):
# Equal
else:
# Not equal
if (user.is_admin()):
# Admin
46
51. Controllo del flusso - Nesting (2)
Francesco Cirillo - Tecnica del pomodoro e Anti-IF® Design Course 48
52. Controllo del flusso - Nesting (3)
Pratichiamo il fast exit.
Bad:
if http_port_is_open:
if ssl_key is Null:
server.listen(80)
else:
server.listen(80, ssl=True)
else:
raise Error('Port already used')
Good:
if !http_port_is_open:
raise Error('Port already used')
use_ssl = ssl_key is not Null
server.listen(80, ssl=use_ssl)
49
53. Controllo di flusso - Nesting (4)
Separiamo questo garbuglio:
def collect_emails(users):
emails = set()
for user in users:
for friend in user.friends:
if friend.email is None:
continue
else:
emails.add(friend.email)
return emails
in due funzioni:
def collect_emails(users):
emails = []
for user in users:
emails += collect_friends_emails(user.friends)
return set(emails)
def collect_friends_emails(friends):
return [for x in friends if x is not None]
50
54. Controllo di flusso - Legge di Demetrio
Un oggetto non dovrebbe esporre la sua struttura interna.
Ovvero: parla con gli amici, non con gli sconosciuti.
Bad:
return Template.load('templates/home.html').compile().render(context)
Good:
engine = TemplateEngine('templates/')
html = engine.render('home.html', context)
return html
Vantaggio collaterale: sembra che nel secondo caso gli IDE performino meglio. 51
56. Interfacce
Definire il numero minimo di metodi.
Non definire le proprietà, piuttosto incapsularle in metodi.
Le migliori interfacce hanno un solo metodo.
Se il linguaggio li mette a disposizione, usare i protocolli.
Come si chiamano i protocolli nei vari linguaggi:
Python: protocol
JavaScript: protocol
Go: interface
Java: trait (devono essere dichiarati)
Swift: protocol (devono essere dichiarati)
Rust: trait (devono essere dichiarati) 53
58. Test
Quando il codice è breve:
Ci sono meno combinazioni da provare
I test diventano più piccoli
Capita raramente di dover modificare un test
E' molto più difficile rompere i test durante i refectoring 55
59. Test - Esempio
Quanto è testabile questa funzione?
def please_test_me(user, check_password, post):
message = ''
if check_password:
password = post.get('password')
if password is None:
raise Error('No password')
else:
if user.password_is_valid(password):
message = 'allowed'
else:
message = 'not_allowed'
else:
if user.is_admin():
message = 'allowed'
elif user.is_staff():
message = 'can_view'
else
message = 'not_allowed'
return render('response.html', user, message)
56
60. Test - Soluzione
Divisa così è più testabile.
def extract_password(post):
password = post.get('password')
if password is None:
raise Error('No password')
return password
def message_by_status(user):
if user.is_admin():
return 'allowed'
elif user.is_staff():
return 'can_view'
else
return 'not_allowed'
57
63. Commenti - Come e quando
Precisi e compatti
Spiegare il codice complicato
Motivare dettagli non intutivi
Commentare solo quando serve
I commenti sono inutili se:
Il codice è già tipizzato
Classi e funzioni sono brevi e fanno una sola cosa
Sono usati nomi chiari ed esplicativi 60
64. Commenti - Codice
Quando il codice si commenta da solo:
Bad:
...
# Check if client is billable
if user.billable_hours > 20 or (today - last_invoice_date) > 30:
...
Good:
if user.isBillable():
...
61
65. Commenti - Superflui
Quando i commenti sono superflui:
class User:
"""A user."""
_is_active # The user is active
def is_admin(self) -> bool:
"""Return True if User has admin privileges. If not return False."""
...
62
67. Strumenti
Guide di stile http://google.github.io/styleguide/
Linter: individuano potenziali bug, errori di stile, pattern sospetti
Code formatter: formattano il codice seguendo uno standard uguale per tutti
Race detector: segnalano le race condition durante l'esecuzione di un programma
Code review: obbligano la revisione del codice da parte di altri sviluppatori 64