Sei sulla pagina 1di 29

Appunti di

Sviluppo Software in
Gruppi di Lavoro Complessi

Tiraboschi Marco

A.A. 2017-2018
Indice

1 Modelli Organizzativi di Sviluppo 1


1.1 Cattedrale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Bazaar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.3 Kibbutz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.4 Gruppi di Lavoro Agili . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.4.1 Scrum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.4.2 Tecniche di Lavoro . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6

2 Linguaggi Orientati al Lavoro Collaborativo 8


2.1 Documentazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.2 Design by Contract . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.2.1 Eiffel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.3 Aspect Oriented Programming . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.3.1 AspectJ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11

3 Configuration Management 12
3.1 Lavoro Concorrente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.1.1 Merge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
3.2 Git . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.2.1 Comandi Base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.2.2 Internals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.2.3 Branching . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
3.2.4 History Rewriting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.2.5 Remoti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.2.6 Hooks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.2.7 Workflow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20

4 Build Automation 22
4.1 Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
4.1.1 Make . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
4.1.2 Configure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.1.3 Ant . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.1.4 Gradle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.2 Continuous Integration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.2.1 Continuous Delivery . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.3 Git Submodules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.4 Docker . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4.4.1 Creazione di immagini . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4.4.2 Modi d’uso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4.4.3 Persistenza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
4.4.4 Networking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Capitolo 1

Modelli Organizzativi di Sviluppo

Le principali problematiche dell’ingegneria del software quando il gruppo di lavoro (ed il


lavoro stesso) comincia ad assumere dimensioni considerevoli, individuate anche da Frederick
Brooks in The Mythical Man-Month, sono

• le scarse tecniche per la stima del tempo, soprattutto l’approccio ottimistico che si
ha sulla stima del tempo

• la confusione tra sforzo e progresso (effort/progress), quindi tra risorse impiegate


e risultati ottenuti

• le tecniche di controllo del progresso, che viene spesso monitorato superficialmente

• la consuetudine di rispondere ai ritardi con l’aggiunta di personale, che spesso


provoca ulteriori ritardi piuttosto che velocizzare il lavoro

In particolare, l’ultimo punto è un po’ controintuitivo, ma ciò dipende dal fatto che nello
sviluppo del software non tutti i compiti sono parallelizzabili: abbiamo un lavoro pro capite
che nel caso migliore è ripartito uniformemente su tutti i lavoratori (Ω(N −1 )), ma altrettanto
uniformemente aumenta il tempo da dedicare alla formazione (O(N )). Invece i tempi da de-
dicare per l’intercomunicazione crescono quadraticamente, perché dipendono dalle coppie di
lavoratori che devono comunicare (O(N 2 )). Se possono esistere anche esigenze comunicative
tra vari sottogruppi, allora O(2N ).
1
“ Adding man power to a late project makes it later ”

1.1 Cattedrale
Il modello a cattedrale è il modello organizzativo proposto da Brooks per risolvere il proble-
ma dell’intercomunicazione, riprendendo l’esempio della sala operatoria di Mills: ogni sala
operatoria ha un chirurgo, che è colui che svolge il lavoro ed è il responsabile del team,
con il suo secondo (spesso chiamato copilota) che ha il ruolo di alter-ego. Il copilota lavora
sempre in coppia con il chirurgo, può scrivere del codice, ma non ne è mai responsabile: egli
1
Frederick Brooks, “Brook’s Law ” da The Mythical Man-Month

1
è una persona che ricerca strategie alternative e con cui il chirurgo si confronta, ma il cui
giudizio non è mai vincolante. Chirurgo e copilota sono scelti tra i migliori programmatori (i
programmatori 10×): il primo deve avere almeno dieci anni di esperienza, il secondo meno e
svolgono la totalità del lavoro implementativo. Il resto del team (administrator, editor, segre-
tario dell’amministratore e segretario dell’editore, program clerk, toolsmith, tester e language
lawyer ) svolge tutti quei lavori che toglierebbero tempo ai programmatori, come la stesura
in bella della documentazione (con ricerca di riferimenti, . . . ), la manutenzione dei sistemi,
degli strumenti, dei log, dei file . . . Con questa organizzazione, viene parallelizzato il lavoro
in modo efficace e mantenendo una proprietà fondamentale: l’integrità concettuale, che
in questo modello dipende solo dal chirurgo.
Come scala questo approccio? Innanzitutto, abbiamo abbattuto i costi comunicativi
all’interno del team, dal momento che tutti si interfacciano solo con il chirurgo (eccetto i se-
gretari, che si interfacciano con proprio referente): aumentando il numero di team (necessario
per progetti grandi), la comunicazione tra i team sarà necessaria solo a livello dei chirurghi
(che, eventualmente, possono delegare i propri copiloti). Per mantenere l’integrità concet-
tuale sulla totalità del progetto viene istituita la figura dell’architetto: ad egli non compete
alcun lavoro implementativo, ma solo la specifica del progetto. In questo modo abbiamo
ottenuto una gerarchia caratterizzata dalla centralizzazione del lavoro concettuale e
dalla distribuzione del lavoro implementativo. Tra i progetti open source che hanno
adottato questo paradigma ingegneristico ci sono i progetti GNU GCC, Emacs e Hurd.

1.2 Bazaar
Il modello a bazaar, proposto tra gli altri da Eric Raymond (coniatore del termine open
source), rappresenta l’approccio radicalmente opposto alla cattedrale: infatti, in uno sviluppo
a bazaar, tutti gli utenti del codice possono contribuire con le proprie capacità e
secondo la propria volontà. I pilastri di un’organizzazione a bazaar sono

• cominciare con l’idea personale di uno sviluppatore

• trattare gli utenti come co-sviluppatori

• ‘Release early, release often and listen to your customers’

• avere un’ampia base di beta-tester e co-developer, per risolvere velocemente i bug

Questi punti, individuati da Raymond in The Cathedral and the Bazaar, sono applicabili solo
nello sviluppo di software open source (mentre il modello a cattedrale può essere implemen-
tato anche in progetti di software proprietario): in particolare, Raymond analizza il processo
di sviluppo del kernel Linux e del proprio progetto Fetchmail. In particolare, l’ultimo punto
si pone in contrasto con la legge di Brooks e viene soprannominata la legge di Linus.
2
“ Given enough eyeballs, all bugs are shallow ”
2
Eric Raymond, “Linus’ Law ” da The Cathedral and the Bazaar

2
1.3 Kibbutz
È chiaro che cattedrale e bazaar rappresentino degli estremi in materia di modelli di organiz-
zazione dello sviluppo del software: il primo molto legato al principio di integrità concettuale
ed il secondo che è lasciato completamente alla volontà dei contributori. Una delle possibili
vie di mezzo è il modello a kibbutz 3 : esso rappresenta un modello non gerarchico, ma con
forti vincoli per i contributori. In questo modo viene sfruttata la potenzialità dell’ampia
forza lavoro del modello bazaar, implementando nel contempo delle regole che garantiscano
una certa integrità progettuale. I cardini del modello a kibbutz sono

• partecipazione volontaria e senza retribuzione dei contributori

• obiettivi ambiziosi e decisi dai membri del progetto

• lavoro regolato da regole esplicite e decise democraticamente

Questi punti, individuati in From Bazaar to Kibbutz, si ritrovano nel caso studio che è il
progetto Debian: questo progetto nasce dall’iniziativa di Ian Murdock di creare una distri-
buzione di software Linux che fosse mantenuta, con lo spirito di Linux e di GNU, da una
comunità di collaboratori. Per mantenere la coerenza della distribuzione, ogni contributo de-
ve essere conforme alle Debian Free Software Guidelines: le regole sono anche implementate
attraverso dei tool, in modo che il controllo della conformità avvenga in maniera automatiz-
zata e abbattendo il margine di errore. L’organizzazione di Debian attraverso un modello
di tipo kibbutz ha permesso alla distribuzione di comprendere più di 43000 pacchetti, sup-
portare più di 13 architetture hardware e 3 kernel di tipo Unix (Linux, FreeBSD e Hurd), il
tutto coordinando il lavoro di più di 16000 contributori.

1.4 Gruppi di Lavoro Agili


“ We are uncovering better ways of developing
software by doing it and helping others do it.
Through this work we have come to value:
Individuals and interactions over processes and tools
Working software over comprehensive documentation
Customer collaboration over contract negotiation
Responding to change over following a plan
That is, while there is value in the items on
4
the right, we value the items on the left more. ”

Negli anni ’90 si sviluppano nuove metodologie di ingegneria del software come risultato
della troppa enfasi data alla documentazione. L’introduzione di UML permise di ottenere
una documentazione talmente strutturata che alcuni gruppi di lavoro decisero di adottarlo
anche come vero e proprio linguaggio di programmazione: questo portò, nel 2002, alla nascita
di xUML (executable UML). Al contrario, molti altri gruppi di lavoro cercarono il distacco
3
M. Monga, From bazaar to kibbutz: how freedom deals with coherence in the Debian project, 2004
4
Martin Fowler, Manifesto for Agile Software Development, 2001

3
dalla documentazione, perché vista come di valore, ma di secondaria importanza rispetto al
codice: questi gruppi di lavoro presero la denominazione di agile, perché proponevano dei
metodi di sviluppo del software che risultavano snelliti della parte ‘burocratica’. Oltre ai
quattro valori, il manifesto esprime dodici princìpi

1. Our highest priority is to satisfy the customer through early and continuous deli-
very of valuable software.

2. Welcome changing requirements, even late in development. Agile processes harness


change for the customer’s competitive advantage.

3. Deliver working software frequently, from a couple of weeks to a couple of months,


with a preference to the shorter timescale.

4. Business people and developers must work together daily throughout the
project.

5. Build projects around motivated individuals. Give them the environment and
support they need, and trust them to get the job done.

6. The most efficient and effective method of conveying information to and within a
development team is face-to-face conversation.

7. Working software is the primary measure of progress.

8. Agile processes promote sustainable development. The sponsors, developers, and users
should be able to maintain a constant pace indefinitely.

9. Continuous attention to technical excellence and good design enhances agility.

10. Simplicity–the art of maximizing the amount of work not done–is essential.

11. The best architectures, requirements, and designs emerge from self-organizing teams.

12. At regular intervals, the team reflects on how to become more effective, then tunes
and adjusts its behavior accordingly.

Viene esasperato il principio di Raymond ‘Release early, release often and listen to your
customers’: infatti si evidenzia l’importanza della continuous delivery (1, 3, 7) e del coin-
volgimento della committenza (2, 4). Risulta vitale avere piccoli team autogestiti, ben
organizzati ed efficenti (5, 6, 8, 9, 11, 12). Infine è ribadita l’importanza della simplicità del
processo di sviluppo del software (10).
Il manifesto rimane, comunque, un atto politico, una presa di posizione che denuncia il
disagio dei programmatori, i quali vogliono essere impiegati per produrre software funzionan-
te e non documentazione (che forse nessuno nemmeno leggerà). A livello di metodo, esistono
varie declinazioni dei valori e princìpi agili.

4
YAGNI (You Aren’t Gonna Need It) è la pratica di non implementare funzionalità che non
sono strettamente necessarie nel prodotto finito: questo consente di mantenere il focus sulle
feature primarie del progetto e implementa quella che è la ricerca della massima semplicità del
lavoro. Questa pratica si contrappone direttamente al paradigma dell’ingegneria del software
classica del Design for Change, che prevederebbe di dare la maggiore adattabilità possibile al
sistema progettato. YAGNI permette di avere un ritmo più sostenuto e di avere più risultati
nel breve termine, tuttavia può creare problemi nel caso di cambiamenti nei requisiti (che
dovrebbero, invece, essere bene accolti, secondo il secondo principio del manifesto) se il
sistema non è abbastanza versatile.

1.4.1 Scrum
Requisiti e Specifiche I requisiti sono spesso formulati nei termini di user story (come
anche in XP, eXtreme Programming, un altro metodo agile)
5
“ As a <who> I want <what> so that <why> ”
Come specifiche, invece, vengono utilizzati dei test case, che dimostrano il funzionamento
del software in una varietà di situazioni desiderate.

Team I team organizzati secondo il metodo Scrum sono formati da 7 ± 2 membri, tra
cui un product owner (che rappresenta la committenza nei confronti del gruppo e gestisce
il backlog del prodotto fissando le priorità, i rischi e il valore dei requisiti) e uno scrum
master (che si occupa del supporto del gruppo di lavoro come facilitatore, risolvendo gli
imprevisti e garantendo il rispetto delle regole): gli altri membri del team, invece, valutano la
complessità dei requisiti, identificano i rischi e sono tenuti a rendicontare il proprio progresso.
Il coordinamento tra gruppi avviene grazie al coordinamento degli scrum master.
È importante coinvolgere la committenza: perciò, spesso gli stakeholder vengono organiz-
zati in una matrice (stakeholder matrix ) che contiene i contatti, il ruolo (sponsor, consulente,
. . . ), la disponibilità, l’influenza che esercita e gli impegni che il gruppo ha nei suoi confronti.

Frammentazione del Lavoro Il lavoro è organizzato in epopee (epic): esse sono delle
funzionalità molto complesse e che richiedono un grande sforzo. Le epopee sono suddivise
in varie storie, scritte in linguaggio naturale così che l’utente le possa comprendere: nelle
riunioni di pianificazione per ciascuna delle storie viene valutata la complessità. Un insieme
di storie viene sviluppato durante uno sprint della durata da una a tre settimane: alle fine
di ogni sprint è prevista una nuova release del progetto. Durante gli sprint vale la closed
window rule: non possono essere introdotte nello sviluppo nuove funzionalità; nel caso di
urgenze, lo sprint deve essere interrotto e ricominciato.

Riunioni Una parte molto importante della metodologia di lavoro Scrum sono le riunioni
(agile ceremonies), che servono a scandire il passo di lavoro del gruppo: in queste riunioni
i direttamente coinvolti sono detti pig, mentre gli interessati sono detti chicken. Vengono
fatte fino a sei tipi di riunioni
5
ScrumDesk, Scrum Guidelines (v.2), 2011

5
• Product Strategy: tutto il team, gestita dal product owner, da 1 a 5 giorni
Prima dell’inizio dello sviluppo, serve a definire a cosa per chi e perché il team si sta
mettendo a lavorare, delineando la visione di insieme per un lavoro efficace.

• Release Planning: tutto il team, gestita dal product owner, da 1 a 5 giorni


Prima dell’inizio della consegna, viene stabilito il significato della release e la data di
consegna, viene stimato un estimated release backlog per avere un riferimento.

• Sprint Planning: tutto il team, gestita dal product owner,per mezza giornata
Prima dell’inizio dello sprint, viene organizzato il lavoro del gruppo definendo le storie
e attribuendone le complessità con il planning poker

• Daily Standup: tutto il team (i manager partecipano come chicken), moderata dallo
scrum master, ∼15 minuti (ognuno deve avere almeno un minuto a disposizione)
Ogni giorno, i membri del team rendicontano l’attività del giorno prima, spiegando
cosa intendono fare il giorno stesso ed evidenziando eventuali problemi. Il product
owner è disponibile per rispondere a domande e viene aggiornata la task-board.

• Review : tutto il team, con i clienti, gli stakeholder e chiunque altro voglia, moderata
dallo scrum master e organizzata dal product owner, un’ora
Durante l’ultimo giorno dello sprint, viene mostrato il progresso concreto del progetto:
gli stakeholder possono approvare o meno i risultati.

• Retrospective: tutto il team (assolutamente senza manager), moderata dallo scrum


master e con la possibile partecipazione del product owner, mezz’ora
Viene discusso il funzionamento del team evidenziando cosa va bene e cosa si può
migliorare, proponendo idee per il miglioramento del lavoro
Una parte importante della pianificazione del lavoro è il planning poker : questo è un gioco
moderato dallo scrum master in cui ogni membro del team, per ogni storia, ne valuta la
complessità con una scala relativa ad una storia di riferimento: la scala più comune è basata
su Fibonacci ( 0, 1/2, 1, 2, 3, 5, 8, 13, 20, 40, 100 ), ma ne esistono molte in uso (es. le taglie
XS, S, M, L, XL). Solitamente tutti i membri mostrano contemporaneamente una carta con
il valore della complessità stimata, per evitare di essere influenzati dagli altri.

Monitoring Il lavoro viene costantemente monitorato utilizzando una task-board (possi-


bilmente non software, ma fisica) sulla quale è presente lo stato delle varie storie nello sprint:
esse vengono divise in task, i quali avranno uno stato che andrà da not started fino a done
(gli stati possono essere personalizzati dal gruppo).
Per ogni sprint si tiene traccia della velocity, ovvero i task conclusi nello sprint: ogni
giorno viene aggiornato il burn-down chart per capire se la velocità sostenuta dal gruppo è
sufficiente a finire il lavoro dello sprint per tempo.

1.4.2 Tecniche di Lavoro


Pair Programming Si programma in coppia, ma con una sola tastiera: questo costringe i
programmatori a rendere espliciti i propri ragionamenti e a mantenere il focus sull’obiettivo.

6
Inoltre, il fatto che il codice sia conosciuto appieno da due persone costituisce un’assicura-
zione contro imprevisti che possono capitare ad uno dei due programmatori. È una tecnica
molto sperimentata e si è riscontrata una stabilità della produttività rispetto al lavoro indi-
viduale: i fautori del pair programming sostengono che si riscontri un miglioramento della
qualità del codice.

Codice Condiviso Tutti i membri del gruppo sono responsabili per tutto il codice (team
ownership della codebase) e lavorano sulla stessa branch. Solitamente viene accompagnata
dalla continuous integration. La separation of concern può essere comunque rispettata trami-
te information hiding, che, però, non rappresenta più una scusa per scaricare la responsabilità
sui propri colleghi.

Refactoring È incoraggiato ottenere il prima possibile del codice funzionante: questo


implica spesso una scarsa qualità iniziale del codice. Ciò non è un problema, infatti è pratica
comune effettuare il refactoring del codice (piuttosto che cercare di produrre fin da subito
del codice pulito).

“Refactoring is a disciplined technique for restructuring an existing body of code,


altering its internal structure without changing its external behavior.” 6

Tra le più comuni operazioni di refactoring ci sono: sostituire sezioni ripetute con funzioni,
cambiare l’accesso ai campi di una classe da diretto a mediato da dei metodi get e set,
eliminare scelte condizionate utilizzando delle sottoclassi o modellare comportamenti simili
o complessi con superclassi. Spesso, se viene utilizzato il TDD, questa modalità di lavoro
viene declinata in modo tale da, prima, produrre del codice che passi il test, poi ristrutturare
il codice (in modo che passi sempre gli stessi test).

TDD (Test-Driven Development) Questa tecnica, molto utilizzata nei gruppi di lavoro
agili e non solo, rovescia il workflow tradizionale: prima viene scritto il test, che servirà da
specifica (test case), e poi viene scritto il codice che permetta il superamento del test

1. Definizione del test

2. Ripetere tutti i test (solo il nuovo test dovrebbe fallire)

3. Scrivere il codice della feature

4. Ripetere tutti i test (dovrebbero passare tutti)

5. Refactoring (verificare il superamento dei test)

6. Ripetere da capo fino all’esaurimento dei test case

Per il testing sono disponibili numerosi supporti, dai framework (come jUnit per java) ai
mock object, emulazioni di oggetti che non vogliamo rischiare di corrompere durante il test o
che non sono ancora disponibili perchè stanno venendo sviluppati da altri membri del team.
6
Martin Fowler, refactoring.com

7
Capitolo 2

Linguaggi Orientati al Lavoro


Collaborativo

2.1 Documentazione
La documentazione dei componenti è necessaria per il lavoro in gruppo: essa deve specificare
il comportamento dei componenti sia in situazioni fisiologiche sia in situazioni patologiche.
Infatti, gli utenti di un componente sono interessati sia alla sua correttezza (il grado di
assenza di difetti di specifica, progettazione o implementazione) sia alla sua robustezza (il
grado di funzionamento corretto del sistema in condizioni di input invalidi o sotto stress).
Per specifica si intende una descrizione delle proprietà del sistema utilizzato per la riso-
luzione di un problema: questo non comprende le modalità implementative del sistema. È,
quindi, una descrizione di cosa fa il sistema, non di come lo faccia.
Produrre delle specifiche di qualità risulta vitale nel contesto della programmazione ‘in
grande’, che si discosta dalla risoluzione di problemi algoritmici, ma che mira a produrre
codice in un’ottica di modificabilità e manutenibilità. Comunicare attraverso specifiche mal
scritte o mal comprese può portare a problemi di comunicazioni catalogati come interface
fault (Perry e Evangelist, 1985).

Asserzioni sono strumenti molto utili per verificare l’aderenza alle specifiche di un compo-
nente o di un sistema: un’asserzione è un’espressione logica che esprime lo stato del sistema
in un determinato punto dell’esecuzione. A seconda del tipo di asserzione, essa può generare
un semplice report in casi anomali, oppure causare l’arresto dell’esecuzione. Le asserzioni
sono state introdotte nel C da Rosenblum come direttive di preprocessore (/*@ exp @*/).
Esistono vari tipi di asserzioni, che predicano su oggetti diversi, ad esempio: assume comuni-
ca un’assunzione sullo stato dell’input o del sistema che sia un prerequisito per l’esecuzione,
promise, invece, promette che una certa condizione sarà vera alla fine dell’esecuzione, return
garantisce delle proprietà del valore di ritorno.

8
2.2 Design by Contract
Il design by contract è un paradigma di sviluppo del software che utilizza estensivamente le
asserzioni come specifica stessa del sistema: esse formano un contratto, in cui il componente
fa delle richieste e delle promesse, sotto forma di asserzioni, le quali costituiranno l’interfaccia
del componente stesso.

Triple di Hoare Le specifiche seguono la semantica delle triple di Hoare

{P } S {Q}

Se vale P, l’esecuzione di S avverrà con successo ed è garantito che varrà Q. Questo costituisce
un contratto: P sono gli obblighi dell’utente di S, mentre Q sono gli obblighi di S. Le due
espressioni sono chiamate precondizioni e postcondizioni. I casi estremi sono

• P = 1 ⇒ Il sistema garantisce il proprio funzionamento in qualsiasi circostanza

• P = 0 ⇒ Il sistema non garantisce il proprio funzionamento in alcuna circostanza

• Q = 1 ⇒ Il sistema non fornisce garanzie sui propri effetti

• Q = 0 ⇒ Il sistema garantisce degli effetti non conformi alle specifiche

Si cerca scrivere una precondizione poco stringente e una postcondizione molto restrittiva
(weakest precondition, strongest postcondition): in questo modo, il contratto costituisce la
specifica del componente.

2.2.1 Eiffel
Eiffel è un linguaggio di programmazione orientato al design by contract, implementa il
paradigma ad oggetti. Nelle interfacce delle feature sono definiti i contratti, nello stesso lin-
guaggio che si utilizza per il codice della feature stessa: per questo, viene spesso definito come
linguaggio di progetto (perché consente sia di programmare sia di specificare i componenti).

Asserzioni
Le asserzioni implementate in Eiffel sono

• require Precondizioni sulle feature

• ensure Postcondizioni sulle feature

• invariant Proprietà invarianti, in una classe o in un ciclo

• variant Proprietà varianti in un ciclo

• check Asserzioni generiche

9
Object-Orientation
In Eiffel, vengono chiamate feature sia i ‘campi ’ sia i ‘metodi ’ delle classi. I costruttori sono
feature dichiarate con la parola chiave create (create <constructor_name>). I costruttori
della classe root (la classe eseguita) non possono richiedere precondizioni, inoltre, eventuali
condizioni invarianti della classe non vengono valutate come precondizioni di un costruttore.
La visibilità delle feature di una classe è specificabile ponendo tra graffe il nome della
classe da cui la feature (o il gruppo di feature) è visibile: la visibilità di default è ANY
(qualsiasi classe), per rendere una feature privata si può specificare NONE.
feature{ CLASSES_FROM_WHICH_ARE_VISIBLE }
x: INTEGER
a: ARRAY[INTEGER]
L’ereditarietà non rispetta appieno il principio di sostituzione di Liskov, che afferma che
un componente {Pp } Sp {Qp } è sostituibile con un componente {Pc } Sc {Qc } se
Pp ⇒ Pc ∧ Qp ⇐ Qc
Ovvero, se le precondizioni del figlio ammettono un superinsieme delle situazioni ammesse
dalle precondizioni del padre e le postcondizioni del figlio ammettono un sottoinsieme del-
le situazioni ammesse dalle postcondizioni del padre. Infatti, il sostituto deve funzionare in
tutti i casi in cui funziona il sostituendo e deve rispettare le garanzie che esso promette. Java
rispetta questo principio, perché ha un’ereditarietà invariante: le precondizioni e le postcon-
dizioni delle specializzazioni sono le stesse delle generalizzazioni. Anche la controvarianza
rispetta il principio: la specializzazione ammette la generalizzazione dei parametri.
L’ereditarietà in Eiffel è covariante: una specializzazione ammette la specializzazione dei
parametri. Questo consente di modellare meglio certi vincoli, ma è la sorgente di numerosi
errori a runtime non individuabili in compilazione. Per quanto riguarda l’ereditarietà delle
asserzioni, invece, viene rispettato il principio di sostituzione di Liskov: il contratto di una
feature che ridefinisce una feature di una propria generalizzazione ammette solo precondizioni
più deboli e postcondizioni più stringenti. Infatti, le precondizioni sono specificate con
require else e vengono valutate in somma logica con le precondizioni della feature che viene
ridefinita, mentre le postcondizioni sono specificate con ensure then e vengono valutate in
prodotto logico con le postcondizioni della feature della superclasse.

Gestione degli Errori


Eiffel fornisce due possibilità di gestione dell’eccezione: terminare con un errore, oppure
riprovare. Questo consente una trattazione molto snella delle situazioni anomale.
La gestione degli errori è svolta dal costrutto rescue/retry: il sollevamento di un’ecce-
zione dirotta il flusso di esecuzione verso la routine di rescue, se in questa viene invocata
l’istruzione retry, l’esecuzione della feature viene riprovata, altrimenti viene interrotta e
costituisce un fallimento. La routine di rescue ha precondizione sempre vera e ha come
postcondizione l’invariante, retry ha precondizione sempre vera e ha come postcondizioni
l’invariante e la precondizione del corpo della feature.
Le routine di gestione degli errori devono essere utilizzate solo per migliorare la robustezza
in situazioni patologiche e non come strumenti di controllo del flusso in situazioni fisiologiche.

10
2.3 Aspect Oriented Programming
Alcuni ambiti della programmazione di un sistema risultano difficilmente separabili, come
la gestione della concorrenza, la sicurezza, la localizzazione. Questo da origine a due diversi
problemi: code tangling (un componente contiene del codice che fa riferimento a ambiti
differenti) e code scattering (il codice riferito ad un ambito è suddiviso in più componenti).
Per risolvere la problematica della separation of concern, si è sviluppato un paradigma di
programmazione chiamato AOP (Aspect Oriented Programming) in cui vengono implemen-
tate nel linguaggio di programmazione delle tecniche per la separazione degli ambiti.

“AOP can be understood as the desire to make quantified statements about the
behavior of programs, and to have these quantifications hold over programs
written by oblivious programmers.” 1

Gli aspetti principali della programmazione orientata agli aspetti, secondo Filman, sono la
quantificazione delle asserzioni sul comportamento di un programma scritto da program-
matori che sono nella totale ignoranza di tali asserzioni.

2.3.1 AspectJ
AspectJ è una generalizzazione di Java che integra l’aspect orientation: affianca alle classi
gli aspetti, con degli strumenti per identificare i punti di unione e un preprocessore per
integrare gli aspetti nelle classi.
1
“In programs P, whenever condition C arises, perform action A”

Pointcut I pointcut sono degli insiemi di join-point definiti programmaticamente: essi


possono essere definiti come un invocazione di un metodo, un assegnamento ad un campo o
la gestione di un’eccezione, che possono essere combinati utilizzando gli operatori booleani
(un pointcut può essere definito, ad esempio, come l’insieme dei punti in cui viene invocato
il metodo x oppure il metodo y).
I join-point possono essere filtrati scegliendo solo i punti all’interno (within) di una
certa classe o metodo, oppure nel flusso di esecuzione di un altro pointcut o anche rispetto
a condizioni runtime. All’interno del join-point si può accedere all’oggetto in cui è il punto
(this), l’oggetto su cui è invocato il metodo o l’assegnamento (target), gli argomenti (args)
e il join-point stesso (thisJoinPoint) per la reflection.
I pointcut possono essere definiti anche parametricamente (ad esempio, tutti gli aggior-
namenti dell’oggetto x, con x non hard coded, ma parametrico).

Weaving Il weaver è un preprocessore che, a tempo di compilazione, ‘intreccia’ il codice


degli aspetti nel codice delle classi, ottenendo un compilato che è tangled a partire da un
sorgente in cui gli aspetti sono separati: i pieces of advice possono essere eseguiti prima
(before), dopo (after) o attorno (around) i joinpoint.

1
Robert Filman, Aspect-Oriented Programming is Quantification and Obliviousness

11
Capitolo 3

Configuration Management

Il Software Configuration Management è la gestione sistematica del processo di sviluppo,


tracciando i cambiamenti in modo tale che lo stato di ogni configuration item nel tempo
sia sempre ricostruibile. In ambito software, gli oggetti che vengono versionati sono detti
artifact (manufatti). Il version control consiste, quindi nella memorizzazione delle versioni
precedenti, nella gestione della concorrenza nello sviluppo e dei conflitti e nella tracciabilità
dei contributi con identificazione del responsabile. Una configurazione non è nient’altro che
lo stato dell’intero sistema, mentre la versione lo è di un singolo manufatto.

Cosa deve essere versionato? Generalmente vengono gestiti i file di testo che costi-
tuiscono il sorgente di un prodotto software, perché i software di configuration management
riescono ad individuare nei file di testo similarità e differenze, risolvendo spesso in automatico
delle situazioni di discordanza. I componenti non sviluppati dal gruppo (librerie, compilatori
o altre dipendenze), nella pratica comune non vengono versionati: per garantire comunque
un certo grado di replicabilità è possibile inserire dei metadati che indichino la configurazione
dell’ambiente di sviluppo. Lo stesso si applica al prodotto finito, che generalmente è un file
binario: è possibile configurare un ambiente di build automation che, a partire dal sorgente
versionato, costruisce il software compilato.

3.1 Lavoro Concorrente


Se un ad progetto lavora un gruppo di persone, è necessaria una qualche politica di gestione
della concorrenza dello sviluppo. Le due principali azioni che possono essere fatte da un
utente di un repository sono il check-out di un artifact, il suo ‘ritiro’ dal repository per
effettuare una modifica, ed il check-in (o commit), che applica un insieme di modifiche
(change set) al repository.
Il primo modello di gestione della concorrenza che si è affermato è il modello pessimistico,
utilizzato nei primi sistemi (SCCS, RCS), che operavano in locale. Esso parte dal presupposto
che se due utenti facessero il check-out dello stesso artifact ci sarebbe la possibilità di un
conflitto: per cui l’accesso ai manufatti viene concesso in mutua esclusione. Il mutex lock
viene rilasciato solamente al successivo check-in dell’artifact.

12
Con l’avvento di Internet e dei sistemi client/server (CVS) e, successivamente, peer-
to-peer (Git) il modello pessimistico non è più risultato praticabile. Il modello ottimistico
prevede l’accesso concorrente ai manufatti in check-out: gli eventuali conflitti verranno risolti
in fase di check-in tramite dei merge. È possibile mantenere il lavoro di persone diverse
separato su più branch di sviluppo in modo da preoccuparsi di come riconciliare il lavoro del
gruppo solamente in fase di unione delle ramificazioni.

3.1.1 Merge
Il merge è un operazione delicata, che consiste nel riportare due flussi di lavoro paralleli in
un unico flusso. Il comando POSIX diff si occupa di valutare la differenza esistente tra due
file, mentre patch applica le differenze. Le differenze vengono solitamente valutate tra hunk,
ovvero sezioni di codice considerate indipendenti (l’hunk atomico è considerato la riga).
La politica più diffusa è di effettuare i merge a tre vie: vengono considerati i due commit
da unire e il primo nodo ancestor comune. Infatti la storia di un repository è un DAG
di commit, ognuno dei quali punta al commit precedente: in caso di biforcazioni (branch)
un genitore può avere più di un figlio, mentre in casi di ricongiungimenti (merge) un figlio
può avere più di un genitore (raramente più di due). Per ogni hunk A vengono valutate le
differenze tra esso e i descendant da unire A0 e A00 (e la differenza dei due discendenti)

• A = A0 = A00
Se l’hunk è uguale nei tre commit non c’è conflitto

• A = A0 6= A00 o A = A00 6= A0
Se l’hunk è uguale tra l’antenato ed uno dei discendenti, viene scelta la modifica intro-
dotta dal descendant diverso, perché si assume che l’altro ramo di sviluppo non si sia
interessato a quell’hunk (vince la minoranza)

• A 6= A0 = A00
Questa situazione non è molto comune, entrambi i descendant hanno introdotto la
stessa modifica: allora tale modifica viene applicata (vince la maggioranza)

• A 6= A0 6= A00 6= A
La risoluzione di questa situazione, in cui i descendant hanno introdotto due modifiche
diverse sullo stesso hunk, non è risolvibile automaticamente: verrà richiesto l’intervento
manuale dell’utente

13
3.2 Git
Git, sviluppato da Linus Torvalds, è il più diffuso sistema di SCM al giorno d’oggi.

3.2.1 Comandi Base


git init Inizializza un repository Git git commit Registra i cambiamenti

git clone Clona il repository git checkout Estrae i file di un commit

git diff Mostra la differenza tra commit git merge Unisce più storie di lavoro

git cherry-pick Applica le modifiche ap-


git status Mostra lo stato del repository
portate dal commit specificato

git log Mostra il log dei commit git remote Gestisce i repository remoti

git branch Opera sui branch git push Aggiorna i repository remoti

git add Aggiunge file all’index git pull Aggiorna il repository locale

3.2.2 Internals
I comandi Git sono script chiamati git-<commandname> e memorizzati nel path di Git.
Nel file .gitconfig (o tramite il comando git config) possono essere definiti degli alias
(scorciatoie per sequenze di comandi complessi o con molte opzioni).
Gli oggetti della git directory possono essere di tre tipi: commit, tree e blob. I blob sono
la rappresentazione in un repository Git dei file: essi sono memorizzati in forma compressa.
I tree, invece sono la rappresentazione delle directory: esse sono implementate come liste di
puntatori ad oggetti. I commit compongono la storia del repository e sono implementati con
un puntatore ad un tree (la rappresentazione della directory in cui sono memorizzati i file
del commit) e un puntatore al commit genitore (o una lista di puntatori, nel caso di nodi di
merge), oltre ai metadati riguardo l’autore e al committer. Ogni oggetto è identificato da un
hash code esadecimale (SHA-1): la directory in cui viene memorizzato è .git/objects/xy,
dove xy sono le prime due cifre dell’hash code, mentre il nome del file è composto dalle
restanti 38 cifre.

git cat-file
Stampa l’oggetto Git corrispondente allo sha specificato: in base all’opzione può stampare
il tipo (-t), la dimensione (-s) o il contenuto formattato (-p).

git hash-object
Calcola l’hash del file specificato: inoltre, con l’opzione -w lo inserisce nella git directory.

14
git add
Per ogni file da aggiungere viene creato il blob corrispondente (e per ogni directory il
tree): questi oggetti vengono indicizzati nel file .git/index, chiamato l’index o lo stage
del repository (infatti, git stage è un alias di git add).
Un’opzione molto comune (spesso abusata) è –-all (o -A), che aggiunge tutti i file della
working directory all’index. Un’altra opzione molto utile è –-patch (o -p), che permette di
specificare iterativamente quali hunk del file aggiornare.

git clone
Il repository specificato viene copiato in una sottodirectory della directory corrente: all’in-
terno verranno inseriti i file di lavoro (working directory) e i file di Git in una sottocartella
.git (git directory). Con l’opzione –-no-checkout (o -n) non viene effettuato il checkout di
nessun commit, ottenendo una working directory vuota: con l’opzione –-bare viene copiato
un bare repository, ovvero un repository composto solo dalla git directory.

git commit
Registra i cambiamenti nel repository (registra i cambiamenti che sono nello stage): il parent
del commit appena creato è il commit puntato dalla head, il puntatore del branch corrente
(se siamo in detatched head state, il puntatore di head ) viene aggiornato.
Ad un commit viene associato un messaggio, che può essere inserito interattivamente o
specificandolo con l’opzione –-message (o -m). Un’opzione spesso abusata è –-all (o -a)
che, prima di committare, aggiunge allo stage i cambiamenti applicati a tutti i file tracciati.
L’opzione –-amend sostituisce il nuovo commit a quello precedente: il suo genitore sarà lo
stesso genitore del commit precedente. Come per git add, è disponibile l’opzione –-patch
(-p) per selezionare quali cambiamenti committare.

git diff
Mostra la differenza tra la working directory e lo stage: se viene specificato un commit, la
differenza tra la working directory ed il commit, se vengono specificati due commit, tra i
due commit. Con l’opzione –-chached (o –-staged) la differenza tra lo stage e la head : se
viene specificato un commit, la differenza tra lo stage ed il commit. È possibile specificare
un insieme di file di cui mostrare le differenze, in caso di ambiguità tra nomi di file e nomi
di commit o branch, i soli nomi dei file devono essere messi dopo la stringa –- .

3.2.3 Branching
Un branch è un puntatore ad un commit che identifica una ramificazione dello sviluppo: il
branch principale predefinito è master. I branch sono memorizzati in .git/refs/heads: il
nome del file è il nome del branch e il contenuto del file è lo sha del commit a cui punta.
La head indica il commit corrente (il commit sul quale si sta costruendo il prossimo): è
memorizzato in .git/HEAD e solitamente contiene il path di un file branch.

15
git branch
Elenca i branch del repository: se viene specificato un nome, crea un nuovo branch che
punta al commit corrente. Con l’opzione –-delete (o -d) elimina il branch, che deve essere
confluito in un altro branch: per forzare l’eliminazione di un branch esiste l’opzione -D. Con
l’opzione –-move (o -m) rinomina il branch (per forzare la rinominazione esiste l’opzione -M).
L’opzione –-force (o -f) sposta il branch.

git checkout
Aggiorna lo stage e la working directory con il contenuto del branch specificato e vi fa puntare
la head. Se, invece, viene specificato un commit la head punterà a quel commit, ma non ad
un branch, risultando in un detatched head state: non esiste un puntatore che punta al ramo
di sviluppo, per cui un eventuale spostamento dell’head con git checkout o altri comandi
farebbe perdere traccia (tranne che per i log) dei commit. I commit non riferiti da nessun
puntatore vengono eliminati da un garbage collector : essendo un’operazione irreversibile, il
garbage collector ha dei tempi molto lunghi.
Si può forzare questo stato anche specificando il nome di un branch, portando la head
a puntare all’ultimo commit del ramo, con l’opzione –-detatch. Con l’opzione -b (o -B),
invece, viene creato un nuovo branch dove viene effettuato il checkout.
Il comando git checkout non modifica i file che presentino differenze non committate.
Si comporta in maniera opposta se vengono specificati dei path: vengono aggiornati tutti i
path specificati con il contenuto che si trova nel commit indicato. Si faccia attenzione: il
checkout di path specifici non aggiorna la head.

git tag
Assegna un nome simbolico ad un commit. I tag sono, quindi, dei puntatori a dei commit
ma, a differenza dei branch, essi non seguono l’evolversi del ramo e rimangono fissi: essi sono
lo strumento scelto per compiti come etichettare versioni.
I tag sono memorizzati come file con lo stesso nome del tag nella directory .git/refs/tags:
il contenuto del file è l’hash code del commit a cui puntano. È possibile cancellare (–-delete
o -d) o spostare (–-force o -f) i tag, ma è fortemente sconsigliato nel caso in cui altri utenti
abbiano già scaricato tali tag, per evitare fraintendimenti.

git merge
Unisce due (o più) rami di sviluppo: incorpora i cambiamenti fatti dal commit specificato in
quello corrente da quando le loro storie si sono separate. Crea un commit sul branch corrente
che ha come genitori i commit coinvolti nel merge.
Come comportamento di default, se il commit da integrare è un discendente diretto del
commit corrente viene effettuato il fast-forward : l’head ed il branch vengono aggiornati e
posti sul descendant, senza creare alcun nuovo commit. La creazione di un nuovo commit
con due genitori come risultato del merge può essere forzato con l’opzione –-no-ff e può
essere utile per la leggibilità della storia.

16
Con l’opzione –-squash non viene effettuato alcun merge, ma vengono prodotte nella
working directory e nello stage le stesse informazioni che ci sarebbero nel caso di un merge:
questo consente di committare queste informazioni come un commit con un singolo genitore.

3.2.4 History Rewriting


Git permette di cambiare la storia dei commit: questo può essere utile nel caso si volessero
organizzare meglio dei contributi effettuati in modo disordinato e si rende necessario nel caso
del lavoro in gruppo per allineare il proprio sviluppo al ramo di lavoro comune.

git reflog
Mostra la storia dei riferimenti, permettendo, ad esempio, di recuperare dei commit che non
sono più puntati da dei branch.

git reset
Specificando dei path (eventualmente posti dopo –- per separarli dagli altri argomenti),
aggiorna il contenuto dello stage per quei path con il contenuto del commit specificato. Il
commit di default è head.
Se non vengono specificati dei path, si può scegliere la modalità di reset. La modalità
di default è –-mixed, che aggiorna il contenuto dello stage e sposta il branch e la head al
commit specificato. Con –-soft, il contenuto dell’index non viene modificato (può essere
usato con funzionalità simile a git commit –-amend, ma con la possibilità di sovrascrivere
e raggruppare più commit), mentre con –-hard viene modificata anche la working directory.

git revert
Crea un commit che annulla le modifiche apportate in una sezione di storia che va dal
commit specificato fino alla head. Questo comando è consigliato al posto di git reset
qualora i commit da resettare siano già stati scaricati da altri utenti.

git rebase
Consente di riscrivere la storia in vari modi. Specificando un branch <B>, riscrive la storia
del branch corrente dal punto in cui si è separato da <B> come se si diramasse dal commit a
cui attualmente punta <B>.

A---B---C topic---HEAD A’--B’--C’ topic---HEAD


/ --- git rebase master ---> /
D---E---F---G master D---E---F---G master

Specificando due branch, <B1> e <B2>, <B2> viene ribasato su <B1>. Per ribasare solo parte
della storia si può usare l’opzione –-onto: git rebase –-onto <newbase> <oldbase> <b>
ribasa i commit che vanno da <oldbase> a <b> su <newbase>.

17
La modalità che rende git rebase il coltellino svizzero dell’history rewriting è quella di
rebase interattivo (–-interactive o -i), che permette di specificare, per ogni commit nella
storia da ribasare, quale operazione effettuare: l’interfaccia è una lista in un file di testo in
cui sono scritte le operazioni da effettuare

pick mantiene il commit nella storia reword modifica il messaggio del commit
squash unisce il commit al precedente drop cancella il commit dalla storia
fixup squash e elimina i log per il commit exec specifica un comando da eseguire
edit porta il repository a questo commit, da modificare con git commit –-amend

git filter-branch
Questo comando consente manipolazioni radicali della storia dei branch. L’opzione che per-
mette di riscrivere la storia eseguendo comandi sul tree selezionato è –-tree-filter: ad
esempio, per far risultare come se dei file fossero sempre stati in una sottodirectory è possibile
usare il comando
git filter-branch -f –-tree-filter ‘mkdir ._p; mv * ._p; mv ._p core;’ –- –-all

L’opzione –-index-filter permette di riscrivere la storia dello stage, senza effettuare il


checkout del tree risultando più veloce di –-tree-filter: ad esempio, per rimuovere un file
dalla storia è possibile usare il comando
git filter-branch –-index-filter ‘git rm –-cached –-ignore-unmatch filename’ HEAD

L’opzione –-subdirectory-filter permette di ricavare la storia di una sottodirectory: ad


esempio, per ottenere un repository con solo il contenuto di una sottodirectory è possibile
usare il comando git filter-branch –-subdirectory-filter foodir –- –-all .

git bisect
Trova il commit che ha introdotto un bug tramite ricerca binaria: vanno specificati un
commit di partenza in cui non c’è il bug e uno di fine in cui c’è, git ci riporta in un commit
intermedio e dovremo indicare se è good o bad o se non va considerato (skip).
Il comando git bisect run consente di eseguire uno script per verificare programmati-
camente la presenza del bug: il codice di uscita 0 indica che il commit e buono, 125 che non
può essere testato e gli altri valori indicano commit cattivi.

git stash
Salva lo stato della working directory e dell’index in uno stack così che si possa operare sul
repository senza preoccuparsi di poterli perdere: per applicare lo stato salvato si può usare
git stash apply (o git stash pop se vogliamo anche rimuoverlo dallo stack).

Chart in the next page by Justin Hileman, Changing History, or How to Git Pretty

18
3.2.5 Remoti
Con Git, è possibile collegare il proprio repository a dei repository remoti: per aggiungere,
rinominare o rimuovere un remoto si usa git remote. Con git clone si può clonare anche
un repository remoto in locale, che avrà come remoto origin il repository da cui lo abbiamo
clonato. Le informazioni possono essere recuperate dal remoto con git fetch, mentre git
pull, oltre a scaricare le informazioni, integra la storia del remoto in quella del repository
locale (di default con un merge, ma si può specificare di effettuare un rebase).
Lato server, è possibile copiare un repository con una fork : da quel momento avrà una
storia indipendente dal repository di origine. Per riconciliare un repository forkato con il
repository di origine è possibile effettuare una pull request: ovvero, si richiede al repository
di origine di integrare i cambiamenti effettuati nella propria fork.

3.2.6 Hooks
Gli hook sono degli script che vengono eseguiti prima o dopo un comando Git: si trovano in
.git/hooks e hanno l’estensione .sample quando non sono effettivi. Alcuni hook sono

• Client side – post-merge • Server side


– pre-aplypatch
– applypatch-msg – post-receive
– pre-commit
– commit-msg – pre-receive
– prepare-commit-msg
– post-update – update
– pre-push
– post-checkout – pre-rebase
– post-commit – pre-rewrite

3.2.7 Workflow
Esistono diversi modi convenzionali (workflow ) di utilizzare i branch in Git.

git flow
Uno dei primi ad affermarsi è stato git flow (ripreso, poi, anche da GitHub flow e GitLab
flow ): esso è caratterizzato non solo dal modo in cui vengono utilizzati i branch, ma anche
dal fatto che sono stati sviluppati dei comandi per la gestione ‘guidata’ di tali branch.
Il ramo master è perenne e contiene solo versione stabili e pronte alla consegna. Il secondo
ramo perenne è develop, che contiene le integrazioni delle feature. I feature branch sono rami
di develop in ognuno dei quali viene sviluppata una feature, che, alla fine dello sviluppo,
confluirà in develop. I release branch sono rami di develop che preparano ad un rilascio: è
possibile fare test e alcune ultime modifiche, dopodiché confluirà in master (sul quale verrà
posta l’etichetta della versione) e su develop. Gli hotfix consentono il bugfix di master : essi
sono branch di master, su cui vengono effettuate piccole modifiche per rimediare velocemente
ad un bug, che poi confluiranno in master e in develop.

git flow init predispone il repository per l’utilizzo di git flow.

20
git flow feature inizializza (start), integra (finish) o cancella (delete) un feature
branch. Nel caso l’inizio di un feature branch non sia allineato con lo stato attuale di develop
è possibile riallinearlo con il comando git flow feature rebase.

git flow release inizializza (start), integra (finish), cancella (delete) un release branch.

git flow hotfix inizializza (start), integra (finish), cancella (delete) un hotfix branch.

Gerrit
Un workflow più efficace su progetti che coinvolgono comunità molto ampie è Gerrit. Esso
si basa sulla peer review delle pull request: quando uno sviluppatore propone i propri cam-
biamenti, prima che questi siano integrati nel repository autoritativo (a cui la comunità fa
riferimento) devono essere approvati dai reviewer. La facoltà di approvare può essere spe-
cializzata a seconda delle aree di competenza del reviwer. Un progetto che utilizza Gerrit è
AOSP (Android Open Source Project) 1 .

1
Android Open Source Project, Contribution Workflow (Life of a Patch)

21
Capitolo 4

Build Automation

Quando ci si appresta ad assemblare un prodotto software formato da molti componenti si


può incorrere in diversi problemi. Un primo problema è il dependency hell 1 : ogni componente
ha delle dipendenze da componenti esterni o interni al progetto, rendendo complicato il grafo
delle dipendenze.

Versionamento Semantico è una pratica utilizzata per facilitare la comprensione delle


dipendenze, specificando le versioni dei componenti con tre numeri interi. Dato il numero di
versione MAJOR.MINOR.PATCH 2 viene incrementata la

1. major version quando vengono introdotti cambiamenti non retrocompatibili delle API

2. minor version quando vengono introdotte funzionalità in modo retrocompatibile

3. patch version quando vengono introdotti bugfix retrocompatibili

4.1 Tools
4.1.1 Make
Make (Stuart Feldman, 1977) è un programma che consente di automatizzare il processo di
build di un progetto software dichiarando le dipendenze: se una dipendenza è stata modificata
rispetto alla data dell’ultima build, viene eseguita l’operazione specificata

<product_file> : <dependency>
<script>

Le dipendenze così dichiarate devono formare un DAG (dipendenze circolari non sono sup-
portate), dal quale viene ricavato un ordinamento topologico: le operazioni di build (receipt)
vengono svolte seguendo questo ordinamento (nei make più recenti, receipt indipendenti
vengono risolte in parallelo).
1
Il termine deriva dalla storpiatura di dll, l’estensione delle librerie dinamiche sotto Microsoft
2
Semantic Versioning 2.0.0

22
4.1.2 Configure
Il problema di make è che si assume un ambiente di build pressoché uniforme, il che non
è quasi mai vero: le receipt prevedono spesso l’uso di compilatori, per cui tutte le persone
dovrebbero avere il compilatore che definisce chi scrive il makefile.
In C alcune funzioni esistono solo in alcuni sistemi o hanno nomi, prototipi o comporta-
menti diversi in sistemi diversi: per cui bisognerebbe inserire un grande numero di direttive
di preprocessore per gestire i vari casi.
La soluzione è utilizzare uno script di configurazione (solitamente denominato configure)
per far corrispondere le librerie presenti nell’ambiente di build con quelle richieste dal sorgente
del programma. Esistono strumenti come autoconf che permettono la produzione automatica
dello script di configurazione: configure deve produrre il corretto file di inclusione config.h
con le direttive richieste per il sistema e il Makefile. È possibile partire da dei template
(Makefile.in, config.h.in, src/Makefile.in) per poi produrre con configure dei file
adatti all’ambiente di build.

4.1.3 Ant
Apache Ant è uno strumento di build automation nato per risolvere alcune problematiche
di make. È implementato in Java (per cui è cross platform) e si presta molto bene come
strumento di build di progetti Java (ad esempio, integra molto facilmente test JUnit). La
descrizione del processo di build è effettuata con XML in un file build.xml.

4.1.4 Gradle
Gradle è uno strumento di build automation che utilizza il linguaggio Groovy. Effettua la
build sfruttando delle convenzioni (che sono, eventualmente, riconfigurabili): ad esempio,
tutto il sorgente scritto nel linguaggio x sarà sotto src/main/x, mentre le unità di test si
troveranno in src/test/x. Esso si basa su dei cataloghi online di componenti, dai quali
verranno recuperate le dipendenze specificate nel file build.gradle. Ha anche un estensivo
supporto per testing e reportistica.
Gradle viene inizializzato con gradle init, che produce: settings.gradle (con le im-
postazioni), build.gradle (in cui vanno inseriti i dati sui plugin da utilizzare, i cataloghi da
interrogare e le dipendenze), gradlew e gradlew.bat. I file gradlew e gradlew.bat sono dei
wrapper (per sistemi Unix e Windows rispettivamente) per scaricare e eseguire gradle anche
su macchine in cui non è installato: questi file possono essere committati in un repository
git per ottenere la replicabilità delle build su macchine diverse.

4.2 Continuous Integration


La Continuous Integration 3 è una pratica di sviluppo del software secondo cui i membri
di un team integrano il proprio lavoro frequentemente (almeno una volta al giorno). Ogni
3
Martin Fowler, Continuous Integration, 2001

23
integrazione viene verificata da un processo di build (e testing) automatizzato per rilevare
errori di integrazione il prima possibile. La routine di lavoro per la CI è
1. Lavoro sulla copia locale sulla macchina di sviluppo
2. Testing sulla macchina di sviluppo
3. Build sulla macchina di sviluppo
4. Push sulla macchina di integrazione
5. Build sulla macchina di integrazione
Questo approccio porta a una riduzione del rischio di problemi di integrazione (Reduced
Deployment Risk ) e permette al gruppo di sviluppare software più coerente e più velocemente.
Questa riduzione è dovuta al fatto che, avvenendo l’integrazione molto di frequente, il lavoro
da conciliare è molto meno rispetto a quanto potrebbe essere integrando solo a sviluppo
finito. Dopo l’integrazione, ogni membro del team riprenderà il proprio lavoro partendo
dalla versione integrata del progetto, producendo software che sarà già compatibile con
quanto sviluppato dagli altri fino a quel punto.
Inoltre, questo metodo di lavoro consente anche una più efficace e più frequente mi-
surazione del progresso (Believable Progress) ed un continuo riscontro per l’utente (User
Feedback ): per avere un feedback verso gli sviluppatori, la build non dovrebbe impiegare più
di 10 minuti (altrimenti si perde l’immediatezza di tale riscontro). Nei casi in cui durasse
di più, è uso comune spezzare la build in più fasi (una pipeline): se viene fatto ciò, l’uso
comune vuole che almeno il commit build avvenga in meno di 10 minuti.

4.2.1 Continuous Delivery


La Continuous Delivery è una pratica di sviluppo del software spesso associata alla CI,
secondo cui i membri di un team producono software in cicli di lavoro brevi, assicurandosi
che il software possa essere rilasciato in ogni momento. Se il software viene effettivamente
prodotto ad ogni integrazione, si parla di Continuous Deployment.
Gitlab e Bitbucket integrano un sistema di gestione della CI/CD. Le direttive di build
vanno scritte in YAML e andranno a specificare l’immagine Docker da scaricare, i comandi
da eseguire e gli artifact da caricare.

4.3 Git Submodules


I sottomoduli Git possono esere utilizzati per la gestoine delle dipendenze. Infatti, un submo-
dule permette di avere un altro repository in una sottocartella del proprio repository. L’altro
repository ha la propria storia, che non interferisce con la storia del repository corrente.
Questo può essere sfruttato per avere dipendenze esterne, come librerie di terzi. La versione
del sottomodulo è congelata, quindi non cambia con l’evolvere della storia del sottoprogetto
a meno che non venga dato il comando di aggiornamento.
Il comando git submodule permette di inizializzare (init) e aggiornare (update) i
sottomoduli. Per cancellarne uno, dovrà essere deinizializzato e manualmente rimosso.

24
4.4 Docker
Quando viene sviluppato un sistema software, si desidera che l’ambiente di sviluppo abbia
delle proprietà che rende semplice la vita dello sviluppatore e dell’operatore (facilitando un
approccio DevOps). Per lo sviluppo è desiderabile avere un ambiente predicibile, replicabi-
le, versionabile e leggero. Dal punto di vista dell’operatore, l’ambiente dovrebbe resistere
agli errori (fault tolerance), essere portabile, scalabile, componibile e con funzionalità di
scheduling e orchestrazione.
La virtualizzazione è uno strumento che è stato impiegato spesso per ottenere questi
desiderata, ma risulta uno strumento molto pesante. Docker permette di ottenere ambienti
simili a quelli ottenuti tramite strumenti classici di virtualizzazione ( Vagrant, . . . ) in modo
più leggero tramite l’isolamento dei processi e della memoria e l’astrazione del file system.
Per funzionare, Docker necessita di un kernel Linux (anche astratto).

Immagini sono la virtualizzazione di un file system. L’insieme delle immagini forma i


nodi un DAG, di cui ogni immagine è ottenuta tramite copy-on-write della propria immagine
genitore. Possono essere ottenute tramite download con pull, o tramite build, commit o
push. Per visualizzare la lista delle immagini si usa images, per ispezionarle inspect e per
rimuoverle rmi.

Container sono costituiti da un’immagine, con l’aggiunta di uno strato copy-on-write


riscrivibile che implementa le modifiche al file system rispetto all’immagine ed i processi.
Sono ottenuti con run, create o start.Per visualizzare la lista dei container si usa ps, per
ispezionarli inspect e per rimuoverli rm. I processi possono essere gestiti con start, stop,
kill, pause, unpause.

4.4.1 Creazione di immagini


Un’immagine viene definita scrivendo un dockerfile, in cui viene dichiarata un’immagine di
partenza e vengono specificati quali comandi eseguire su quel file system. Le immagini di
partenza sono preferibilmente il più leggere possibili, per cui sono molto popolari sistemi
operativi Linux molto essenziali (come Alpine).
FROM <parent_image>
MAINTAINER <maintainer>
WORKDIR <wd_path>
<commands>
Per ottenere la massima riproducibilità, anziché distribuire il dockerfile viene fatto il push
dell’immagine buildata: se si riesegue la build di uno stesso dockerfile potrebbe variare
l’immagine come conseguenza di un aggiornamento delle dipendenze.

4.4.2 Modi d’uso


Esistono diverse possibilità quando si passa all’esecuzione di un container. L’esecuzione
effimera (ephemeral, run –rm) prevede la rimozione automatica del container una volta che

25
termina l’esecuzione, utile nei casi in cui il container serva una volta tantum. L’esecuzione
interattiva (–interactive, -i) permette l’utilizzo dello standard input per interagire con il
container. Un container in modalità detached (o daemonized, -d) lavora in background.
Dopo l’avvio è possibile recuperare i log del container, oppure riattaccarlo allo standard
i/o con docker attach. Con docker exec è possibile eseguire un processo in un container
avviato.

4.4.3 Persistenza
Ci sono vari modi per garantire la persistenza dei dati in Docker. Un primo modo è quello
di creare dei volumi nominati (con docker volume create) nel guest space, che saranno
disponibili per la lettura e la scrittura da parte dei container che indicano il nome del volume
con l’opzione –-volume.
Per copiare dei file scrivendoli o leggendoli da un volume è possibile mappare il volume
al file system dell’host, ma ciò può essere scomodo. È possibile, invece, utilizzare cp per
copiare file da e verso volumi creati con dei container

CID=$(docker create --volume <name> <container>)


docker cp $CID:<guestfilepath> <hostfilepath> # Esportazione dal volume
docker cp <hostfilepath> $CID:<guestfilepath> # Importazione nel volume

4.4.4 Networking
È possibile creare reti di container (docker network create) implementando, così, dei
micro-servizi: i container nella stessa rete possono comunicare tra di loro. Per rendere pub-
blico all’esterno di Docker un servizio di rete offerto da un container è necessario pubblicare
la porta sulla quale il server è in ascolto (-p <portnum>).

26

Potrebbero piacerti anche