diff --git a/.gitignore b/.gitignore index e9476bf801209d6d7e931b6c8b91fd8026cb83d1..a8f434c9d8689b974ecd9113ecb82028f5a16419 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ resized/ .vscode/ uml/ book + +# PlantUML plugin +mdbook-plantuml-img/ +.mdbook-plantuml-cache/ diff --git a/_posts/2022-10-24-Progettazione.md b/_posts/2022-10-24-Progettazione.md deleted file mode 100644 index 75847d3c25757f82ebaf3f15a608343b98ece682..0000000000000000000000000000000000000000 --- a/_posts/2022-10-24-Progettazione.md +++ /dev/null @@ -1,555 +0,0 @@ ---- -layout: post -title: "[07] Progettazione" -date: 2022-10-24 14:30:00 +0200 -toc: true ---- - -# Progettazione - -## Introduzione - -Durante le lezioni, per discutere di progettazione siamo partiti da un esempio di programma in C che stampa una canzone. -Il [codice considerato](https://paste.studentiunimi.it/paste/JS+VOUKO#Rj3CMN-TmbNu3zeTleTTM0JdjjROWeVySXTgzG6aWKY) è talmente illeggibile che Jekyll rifiuta di compilare se lo si prova ad includere in un file Markdown. - -Successivamente abbiamo scomposto il codice per renderlo logicamente più sensato e facilmente modificabile, sono state __estratte le parti comuni__ e spostate in una funzione apposita, mentre le __parti mutabili sono state salvate in alcune strutture dati__; la canzone viene così stampata tramite un ciclo. -In questo modo scrivendo un codice più semplice siamo stati in grado di creare una soluzione più generale e più aperta ai cambiamenti. - -```java -public class TwelveDaysOfChristmas { - static String[] days = {"first", "second", ..., "twelfth"}; - static String[] gifts = { "a partdrige in a pear tree", "two turtle doves", ... }; - - static String firstLine(int day) { - return "On the " + days[day] + - " day of Christmas my true love gave to me:\n"; - } - - static String allGifts(int day) { - if (day == 0) { - return "and " + gifts[0]; - } else { - return gifts[day] + "\n" + allGifts(day-1); - } - } - - public static void main(String[] args) { - System.out.println(firstLine(0)); - System.out.println(gifts[0]); - for (int day == 1; day < 12; day++) { - System.out.println(firstLine(day)); - System.out.println(allGifts(day)); - } - } -} -``` - -È importante quindi __adottare la soluzione più semplice__ (che __non è quella più stupida__!) e una misura convenzionale per dire quanto una cosa è semplice - almeno in Università - si esprime in termini del tempo dedicato dal programmatore all'implementazione. -Tale misura si sposa bene con il __TDD__, che richiede __brevi iterazioni__ di circa 10 minuti: se la feature attuale richiede più tempo è opportuno ridurre la portata scomponendo il problema. - -## Refactoring -Durante il refactoring è opportuno rispettare le seguenti regole: -- le __modifiche al codice non devono modificare le funzionalità__: -il refactoring DEVE essere invisibile al cliente; -- __non possono essere aggiunti test aggiuntivi__ rispetto alla fase verde appena raggiunta. - -Se la fase di refactoring sta richiedendo troppo tempo allora è possibile fare rollback all'ultima versione verde e __pianificare meglio__ l'attività di refactoring, per esempio scomponendolo in più step. -Vale la regola del _"do it twice"_: il secondo approccio a un problema è solitamente più veloce e migliore. - -### Motivazioni - -Spesso le motivazioni dietro un refactoring sono: -- precedente __design molto complesso e poco leggibile__, a causa della velocità del passare ad uno _scenario verde_; -- __preparare il design di una funzionalità__ che non si integra bene in quello esistente; dopo aver raggiunto uno _scenario verde_ in una feature, è possibile che la feature successiva sia difficile da integrare. -In questo caso, se il _refactoring_ non è banale è bene fermarsi, tornare indietro e evolvere il codice per facilitare l'iterazione successiva (__design for change__). -- presenza di __debito tecnico__ su lavoro fatto in precendenza, ovvero debolezze e "scorciatoie" che ostacolano notevolmente evoluzioni future: _"ogni debito tecnico lo si ripaga con gli interessi"_. - -## Design knowledge - -La design knowledge è la __conoscenza del design__ architetturale di un progetto. -È possibile utilizzare: -- la __memoria__: non è efficace perché nel tempo si erode, specialmente in coppia; -- i __documenti di design__ (linguaggio naturale o diagrammi): se non viene aggiornato di pari passo con il codice rimane disallineato, risultando più dannoso che d'aiuto. -- le __piattaforme di discussione__ (version control, issue management): possono aiutare ma le informazioni sono sparse in luoghi diversi e di conseguenza difficili da reperire e rimane il problema di mantenere aggiornate queste informazioni. -- gli __UML__: tramite diagrammi UML si è cercato di sfruttare l'approccio ___generative programming___, ovvero la generazione automatica del codice a partire da specificazioni di diagrammi. -Con l'esperienza si è visto che non funziona. -- il __codice__ stesso: tramite la lettura del codice è possibile capire il design ma è difficile rappresentare le ragioni della scelta. - -È bene sfruttare tutte le tecniche sopra proposte __combinandole__, partendo dal codice. \ -È inoltre importante scrivere documentazione per spiegare le ragioni dietro le scelte effettuate e non le scelte in sé, che si possono dedurre dal codice. - -### Condivisione - -Per condividere tali scelte di design (il _know how_) è possibile sfruttare: -- __metodi__: con pratiche (come Agile) o addirittura l'object orientation stessa, che può essere un metodo astratto per condividere scelte di design. -- __design pattern__: fondamentali per condividere scelte di design, sono utili anche per generare un vocabolario comune (sfruttiamo dei nomi riconosciuti da tutti per descrivere i ruoli dei componenti) e aiutano l'implementazione (i pattern hanno delle metodologie note per essere implementati). -I pattern non si concentrano sulle prestazioni di un particolare sistema ma sulla generalità e la riusabilità di soluzioni a problemi comuni; -- __principi__: per esempio i principi SOLID. - -## Conoscenze preliminari di concetti e termini - -Prima di proseguire è bene richiamare concetti e termini fondamentali presumibilmente visti durante il corso di Programmazione II. - -### Object orientation - -Per essere definito _object oriented_, un linguaggio di programmazione deve soddisfare tre proprietà: -- __ereditarietà__: ovvero la possibilità di poter definire una classe ereditando proprietà e comportamenti di un'altra classe. -- __polimorfismo__: quando una classe può assumere diverse forme in base alle interfacce che implementa. -Il prof fa l'esempio del _tennista scacchista_: in un torneo di tennis è poco utile sostituire una persona che gioca a tennis ed è brava con gli scacchi (quindi una classe che implementa entrambe le interfacce) con una che gioca a scacchi. -Il collegamento tra capacità e oggetto è fatto __a tempo di compilazione__: non è importante quindi se la capacità non è ancora definita; -- __collegamento dinamico__: in Java il tipo concreto degli oggetti e quindi il problema di stabilire _quale metodo chiamare_ viene risolto durante l'esecuzione. -In C++ occorre esplicitare questo comportamento utilizzando la keyword `virtual`. - -### <a style="color: darkgreen">SOLID</a> principles - -Ci sono 5 parti che compongono questo principio: -1. __<span style="color: darkgreen"><big>S</big></span>INGLE RESPONSIBILITY__: una classe, un solo scopo. -Così facendo, le classi rimangono semplici e si agevola la riusabilità. -2. __<span style="color: darkgreen"><big>O</big></span>PEN-CLOSE PRINCIPLE__: -le classi devono essere aperte ai cambiamenti (_opened_) ma senza modificare le parti già consegnate e in produzione (_closed_). -Il refactoring è comunque possibile, ma deve essere preferibile estendere la classe attuale. -3. __<span style="color: darkgreen"><big>L</big></span>ISKOV SUBSTITUTION PRINCIPLE__: -c'è la garanzia che le caratteristiche eredidate dalla classe padre continuinino ad esistere nelle classi figlie. -Questo concetto si collega all'aspetto __contract-based__ del metodo Agile: le _precondizioni_ di un metodo di una classe figlia devono essere ugualmente o meno restrittive del metodo della classe padre. -Al contrario, le _postcondizioni_ di un metodo della classe figlia non possono garantire più di quello che garantiva il metodo nella classe padre. -Fare _casting_ bypassa queste regole. -4. __<span style="color: darkgreen"><big>I</big></span>NTERFACE SEGREGATION__: -più le capacità e competenze di una classe sono frammentate in tante interfacce più è facile utilizzarla in contesti differenti. -In questo modo un client non dipende da metodi che non usa. -Meglio quindi avere __tante interfacce specifiche__ e piccole (composte da pochi metodi), piuttosto che poche, grandi e generali. -5. __<span style="color: darkgreen"><big>D</big></span>EPENDENCY INVERSION__: -il codice dal quale una classe dipende non deve essere più __concreto__ di tale classe. -Per esempio, se il _telaio della FIAT 500_ dipende da uno specifico motore, è possibile utilizzarlo solo per quel specifico motore. -Se invece il telaio dipende da _un_ concetto di motore, non c'è questa limitazione. -In conlusione, le classi concrete devono tendenzialmente dipendere da classi astratte e non da altre classi concrete. - -### Reference escaping - -Il _reference escaping_ è una violazione dell'incapsulamento. - -Può capitare, per esempio: -- quando un getter ritorna un riferimento a un segreto; -```java -public Deck { - private List<Card> cards; - - public List<Card> getCards() { - return this.cards; - } -} -``` -- quando un setter assegna a un segreto un riferimento che gli viene passato; -```java -public Deck { - private List<Card> cards; - - public setCards(List<Card> cards) { - this.cards = cards; - } -} -``` - -- quando il costruttore assegna al segreto un riferimento che gli viene passato; -```java -public Deck { - private List<Card> cards; - - public Deck(List<Card> cards) { - this.cards = cards; - } -} -``` - -### Encapsulation e information hiding - -__Legge di Parnas (L8)__. -> Solo ciò che è nascosto può essere cambiato liberamente e senza pericoli. - -Lo stato mostrato all'esterno non può essere modificato, mentre quello nascosto sì. - -Questo principio serve per __facilitare la comprensione del codice__ e renderne più facile la modifica parziale senza fare danni. - -### Immutabilità - -Una classe è immutabile quando non c'è modo di modificare lo stato di ogni suo oggetto dopo la creazione. - -Per assicurare tale proprietà è necessario: -- __non fornire metodi di modifica__ allo stato; -- avere tutti gli __attributi privati__ per i tipi potenzialmente mutabili (come `List<T>`); -- avere tutti gli __attributi final__ se non già privati; -- assicurare l'__accesso esclusivo__ a tutte le parti non mutabili, ovvero non avere reference escaping. - -## Code smell - -I _code smell_ sono dei segnali che suggeriscono problemi nella progettazione del codice. -Di seguito ne sono elencati alcuni: -- __codice duplicato__: si può fare per arrivare velocemente al verde ma è da togliere con il refactoring. -Le parti di codice in comune possono quindi essere fattorizzate. -- __metodi troppo lunghi__: sono poco leggibili e poco riusabili; -- __troppi livelli di indentazione__: scarsa leggibilità e riusabilità, è bene fattorizzare il codice; -- __troppi attributi__: suggerisce che la classe non rispetta la single responsability, ovvero fa troppe cose; -- __lunghe sequenze di _if-else_ o _switch___; -- __classe troppo grande__; -- __lista parametri troppo lunga__; -- __numeri _magici___: è importante assegnare alle costanti numeriche all'interno del codice un nome per comprendere meglio il loro scopo; -- __commenti che spiegano cosa fa il codice__: indicazione che il codice non è abbastanza chiaro; -- __nomi oscuri o inconsistenti__; -- __codice morto__: nel programma non deve essere presente del codice irraggiungibile o commentato. Utilizzando strumenti di versioning è possibile riaccedere a codice precedentemente scritto con facilità. -- __getter e setter__: vedi principio di __tell don't ask__. - -## <a href="https://martinfowler.com/bliki/TellDontAsk.html">Principio Tell-Don't-Ask</a> - -{% responsive_image path: 'assets/07_tell-dont-ask.png' %} - -> Non chiedere i dati, ma dì cosa vuoi che si faccia sui dati - -Il responsabile di un'informazione è anche responsabile di tutte le operazioni su quell'informazione. - -Il principio _Tell-Don't-Ask_ sancisce che piuttosto di __chiedere__ ad un oggetto dei dati e fare delle operazioni con quei dati è meglio __dire__ a questo oggetto cosa fare con i dati che contiene. - -#### Esempio - -Se desideriamo stampare il contenuto di tutte le carte in un mazzo possiamo partire da questo codice. - -```java -class Main { - public static void main(String[] args) { - Deck deck = new Deck(); - Card card = new Card(); - - card.setSuit(Suit.DIAMONDS); - card.setRank(Rank.THREE); - deck.getCards().add(card); - deck.getCards().add(new Card()); // <-- !!! - - System.out.println("There are " + deck.getCards().size() + " cards:"); - for (Card currentCard : deck.getCards()) { - System.out.println( - currentCard.getRank() + - " of " + - currentCard.getSuit() - ); - } - } -} -``` - -All'interno del ciclo reperiamo gli attributi della carta e li utilizziamo per stampare le sue informazioni. -Inoltre, nella riga evidenziata viene aggiunta una carta senza settare i suoi attributi. -La responsabilità della gestione dell'informazione della carta è quindi __erroneamente delegata__ alla classe chiamante. - -Per risolvere, è possibile trasformare la classe `Card` nel seguente modo: - -```java -class Card { - private Suit suit; - private Rank rank; - - public Card(@NotNull Suit s, @NotNull Rank r) { - suit = s; - rank = r; - } - - @Override - public String toString() { - return rank + " of " + suit; - } -} -``` - -l'informazione viene ora interamente gestita dalla classe `Card`, che la gestisce nel metodo `toString()` per ritornare la sua rappresentazione testuale. - -## Interface segregation - -Le interfacce possono _"nascere"_ tramite due approcci: -- __up front__: scrivere direttamente l'interfaccia; -- __down front__: scrivere il codice e quindi tentare di estrarne un'interfaccia. - -L'approccio down-front si adatta meglio al TDD ed è illustrato nel seguente esempio. - -## Esempio con gerarchia Card / Deck - -In questo esempio sono trattati numerosi principi, come l'_interface segreagation_, _linking dinamico/statico_, _implementazione di interfacce multiple_ e il _contract based design_ vs la _programmazione difensiva_. - -### Interface segregation all'opera - -```java -public static List<Card> drawCards(Deck deck, int number) { - List<Card> result = new ArrayList<>(); - for (int i = 0; i < number && !deck.isEmpty(); i++) { - result.add(deck.draw()); - } - return result; -} -``` - -Consideriamo il metodo `drawCards` che prende come parametri un `Deck` e un intero. \\ -Le __uniche competenze__ riconosciute a `Deck` sono l'indicazione _se è vuoto_ (`isEmpty()`) e il _pescare una carta_ dal mazzo (`draw()`). -`Deck` può quindi __implementare un'interfaccia__ che mette a disposizione queste capacità. - -È possibile modificare il metodo in modo da accettare un qualunque oggetto in grado di eseguire le operazioni sopra elencate, ovvero che implementi l'interfaccia __`CardSource`__. - -<a id="cardsource" style="display:none;"></a> - -```java -public interface CardSource { - /** - * @return The next available card. - * @pre !isEmpty() - */ - Card draw(); - - /** - * @return True if there is no card in the source - */ - boolean isEmpty(); -} -``` - -```java -public class Deck implements CardSource { ... } -``` - -```java -public static List<Card> drawCards(CardSource deck, int number) { - List<Card> result = new ArrayList<>(); - for (int i = 0; i < number && !deck.isEmpty(); i++) { - result.add(deck.draw()); - } - return result; -} -``` - -### Collegamento statico e dinamico - -Notare come è necessario specificare __staticamente__ che `Deck` implementi `CardSource`, ovvero occorre forzare la dichiarazione del fatto che `Deck` sia un _sottotipo_ di `CardSource` (Java è strong typed) e quindi sia possibile mettere un oggetto `Deck` ovunque sia richiesto un oggetto `CardSource`. \\ -In altri linguaggi come __Go__ c'è una __maggiore dinamicità__ perché non c'è bisogno di specificare nel codice che un oggetto è sottotipo di qualcos'altro, è sufficiente solo che implementi un metodo con la stessa signature. -Il controllo che l'oggetto passato ad una funzione abbia le capacità necessarie avviene a runtime e non prima. - -Un problema della troppa dinamicità (__duck typing__) è che se i metodi di un oggetto non hanno dei nomi abbastanza specifici si possono avere dei problemi. -Per esempio, in un programma per il gioco del tennis se una funzione richiede un oggetto che implementa il metodo `play()`, e riceve in input un oggetto che non c'entra nulla con il tennis (per esempio un oggetto di tipo `GiocatoreDiScacchi`) che ha il metodo `play()`, si possono avere degli effetti indesiderati. - -### _Loose coupling_ -Il _loose coupling_ è la capacità di una variabile o un parametro di accettare l'assegnamento di oggetti aventi tipo diverso da quello della variabile o parametro, a patto che sia un sottotipo. - -```java -Deck deck = new Deck(); -CardSource source = deck; -List<Card> cards = drawCards(deck, 5); -``` - -### Interfacce multiple - -Tornando all'esempio, la classe `Deck` (che implementa `CardSource`) __può implementare anche altre interfacce__, come `Shuffable` o `Iterable<Card>`. -Al metodo precedente interessa solo che Deck abbia le capacità specificate in `CardSource`, se poi implementa anche altre interfaccie è ininfluente. - -{% plantuml style="width:100%" %} - -class Client1 -class Client2 -class Client3 - -interface Iterable<Card> << interface >> { - + {abstract} Iterator<Card> iterator() -} - -class Deck implements Iterable, Shuffable, CardSource { - + void shuffle() - + Card draw() - + boolean isEmpty() - + Iterator<Card> iterator() -} - -interface Shuffable << interface >> { - + {abstract} void shuffle() -} - -interface CardSource << interface >> { - + {abstract} Card draw() - + {abstract} boolean isEmpty() -} - -Client2 ..> Iterable -Client1 ..> Shuffable -Client3 ..> Iterable -Client3 ..> CardSource - -hide empty fields -hide empty methods - -{% endplantuml %} - -### _Contract-based_ design vs programmazione difensiva - -Tornando alla <a href="#cardsource">specificazione</a> dell'interfaccia di `CardSource`, è possibile notare dei commenti in formato Javadoc che specificano le __precondizioni__ e le __postcondizioni__ (il valore di ritorno) del metodo. Secondo il ___contract-based_ design__, esiste un _"contratto"_ tra chi implementa un metodo e chi lo chiama. - -Per esempio, considerando il metodo `draw()`, __è responsabilità del chiamante__ verificare il soddisfacimento delle precondizioni (_"il mazzo non è vuoto"_) prima di invocare il metodo. -Se `draw()` viene chiamato quando il mazzo è vuoto ci troviamo in una situazione di __violazione di contratto__ e può anche esplodere la centrale nucleare. - -Per specificare il contratto si possono utilizzare delle __asserzioni__ o il `@pre` nei __commenti__. -Le prime sono particolarmenti utili in fase di sviluppo perché interrompono l'esecuzione del programma in caso di violazione, ma vengono solitamente rimosse in favore delle seconde nella fase di deployment. - -Un'altro approccio è la __programmazione difensiva__ che al contrario delega la responsabilità del soddisfacimento delle precondizioni al _chiamato_, e non al chiamante. - -### Classi astratte - -Una classe astratta che implementa un'interfaccia __non deve necessariamente implementarne__ tutti i metodi, ma può delegarne l'implementazione alle sottoclassi impedendo l'istanziamento di oggetti del suo tipo. - -Le interfacce diminuiscono leggermente le performance, però migliorano estremamente la generalità (che aiutano l'espandibilità ed evolvibilità del programma), quindi vale la pena di utilizzarle. - -È possibile utilizzare le __classi astratte__ anche per classi complete, ma che __non ha senso che siano istanziate__. -Un buon esempio sono le classi _utility_ della libreria standard di Java. - -#### Classe utility della libreria standard di Java - -Un esempio è __`Collections.shuffle(List<?> list)`__ che accetta una lista omogenea di elementi e la mischia. -Il _tipo_ degli elementi è volutamente ignorato in quanto non è necessario conoscerlo per mischiarli. - -Per l'__ordinamento__, invece, è necessario conoscere il tipo degli oggetti in quanto bisogna confrontarli tra loro per poterli ordinare. -La responsabilità della comparazione è però delegata all'oggetto, che deve aderire all'interfaccia `Comparable<T>`. - -__`Collections.sort(...)`__ ha, infatti, la seguente signature: -```java -public static <T extends Comparable<? super T>> void sort(List<T> list) -``` - -La notazione di generico __aggiunge dei vincoli__ su `T`, ovvero il tipo degli elementi contenuti nella lista: -- `T extends Comparable<...>` significa che `T` deve estendere - e quindi implementare - l'interfaccia `Comparable<...>`; -- `Comparable<? super T>` significa che tale interfaccia può essere implementata su un antenato di `T` (o anche `T` stesso). - -`Comparable` è un altro esempio di _interface segregation_: serve per specificare che un oggetto ha bisogno della caratteristica di essere comparabile. - -__Digressione__: la classe Collections era l'unico modo per definire dei metodi sulle interfacce (es: dare la possibilità di avere dei metodi sulle collezioni, ovvero liste, mappe, ecc), ma ora si possono utilizzare i metodi di default. - -# Analisi del testo naturale - -Come organizzare la partenza del design suddividendo in classi e responsabilità? - -I due approcci principali sono: -- __pattern__: riconoscere una situazione comune da una data; -- __TDD__: partendo dalla soluzione più semplice si definiscono classi solo all'occorrenza. - -Un'altra tecnica che vedremo è l'__estrazione dei nomi__ (noun extraction), per un certo senso _naive_ ma adatta in caso di storie complesse. - -## Noun extraction - -Basandosi sulle specifiche - come i commenti esplicativi delle User Stories - si parte dai sostantivi (o frasi sostantivizzate), si _sfolsticono_ con dei criteri, si cercano le relazioni tra loro e quindi si produce la gerarchia delle classi. - -Per spiegare il procedimento considereremo il seguente esempio: - -> - The __library__ contains __books__ and __journals__. -> It may have several __copies__ of a given book. -> Some of the books are for __short term loans__ only. -> All other books may be borrowed by any __library member__ for three __weeks__. -> - __Memebers of the library__ can normally borrow up to six __items__ at a time, but __members of staff__ may borrow up to 12 items at one time. -> Only member of staff may borrow journals. -> - The __system__ must keep track of when books and journals are borrowed and returned, enforcing the __rules__ described above. - -Nell'esempio sopra sono stati evidenziati i sostantivi e le frasi sostantivizzate. - -### Criteri di _sfoltimento_ - -I criteri di _sfoltimento_ servono per diminuire il numero di sostantivi considerando solo quelli rilevanti per risolvere il problema. -In questa fase, in caso di dubbi è possibile rimandare la decisione a un momento successivo. - -Di seguito ne sono riportati alcuni: -- __Ridondanza__: sinonimi, termini diversi per indicare lo stesso concetto. Anche se è stata utilizzata una locuzione diversa potrebbe essere comunque ridondante, sopratutto in lingue diverse dall'inglese in cui ci sono molti sinonimi. \\ -Nell'esempio: _library member_ e _member of the library_, _loan_ e _short term loan_. -- __Vaghezza__: nomi generici, comuni a qualunque specifica; potrebbero essere sintomo di una _classe comune astratta_. \\ -Nell'esempio: _items_. -- __Nomi di eventi e operazioni__: nomi che indicano azioni e non hanno un concetto di _stato_. \\ -Nell'esempio: _loan_. -- __Metalinguaggio__: parti _statiche_ che fanno parte della logica del programma e che quindi non necessitano di essere rappresentati come classi. \\ -Nell'esempio: _system_, _rules_. -- __Esterne al sistema__: concetti esterni o verità _"assolute"_ al di fuori del controllo del programma. \\ -Esempio: _library_, _week_ (una settimana ha 7 giorni). -- __Attributi__: informazioni atomiche e primitive (stringhe, interi, ...) relative a una classe, che quindi non necessitano la creazione di una classe di per sé. \\ -Esempio: _name of the member_ (se ci fosse stato). - -Al termine di questa fase, si avrà una lista di classi _"certe"_ e _"incerte"_. -In questo esempio, sono soppravvisuti i termini _journal_, _book_, _copy_ (of _book_), _library member_ e _member of staff_. - -### Relazioni tra classi - -Il prossimo passo è definire le relazioni tra le classi. - -Inizialmente, si collegano con delle _linee_ (non frecce) senza specificare la direzione dell'associazione. -Parliamo di __associazioni__ e non __attributi__ perché non è necessariamente vero che tutte le associazioni si trasformino in attributi. - -{% plantuml style="width: 90%" %} -class Book -class CopyOfBook -class LibraryMember -class StaffMember -class Journal - -Journal -- StaffMember : borrows/returns -Book -- CopyOfBook : is a copy -CopyOfBook -- LibraryMember : borrows/returns -CopyOfBook -- StaffMember : borrows/returns - -hide empty fields -hide empty methods -{% endplantuml %} - -Il prossimo passo è specificare le __cardinalità__ delle relazioni, come specificato dal linguaggio UML (opposto in questo aspetto al diagramma ER). -La precisione richiesta in questo punto è soggettiva: da una parte, specificare puntualmente il numero massimo di elementi di una associazione può aiutare ad ottimizzare successivamente, dall'altra porta confusione. - -{% plantuml style="width: 90%" %} -class Book -class CopyOfBook -class LibraryMember -class StaffMember -class Journal - -Journal "0..*" -- "0..1" StaffMember : borrows/returns -Book "1" -- "1..*" CopyOfBook : is a copy -CopyOfBook "0..*" -- "0..1" LibraryMember : borrows/returns -CopyOfBook "0..*" -- "0..1" StaffMember : borrows/returns - -hide empty fields -hide empty methods -{% endplantuml %} - -Dopo aver ragionato sulle cardinalità, si iniziano a cercare __generalizzazioni__ e fattorizzazioni. -In questo caso, notiamo che: -- `StaffMember` _è un_ `LibraryMember` con in più la possibilità di prendere `Journal`. -Inoltre, un altro indicatore è che hanno la stesso tipo di relazioni con gli altri oggetti. -- `Items` è un termine generico per indicare `CopyOfBook` e `Journal`. - -{% plantuml style="width: 90%" %} -class BorrowableItem -class LibraryMember -class Book -class CopyOfBook extends BorrowableItem -class StaffMember extends LibraryMember -class Journal extends BorrowableItem - -Book "1" -- "1..*" CopyOfBook : is a copy -BorrowableItem "0..*" -- "0..1" LibraryMember : borrows/returns - -hide empty fields -hide empty methods -{% endplantuml %} - -Distinguere `CopyOfBook` e `Journal` è inutile, perché di fatto un `Journal` _è una copia di_ un giornale. -Si può quindi fattorizzare __rimuovendo la generalizzazione__, come mostrato di seguito. - -{% plantuml style="width: 90%" %} -class Book -class BorrowableItem -class LibraryMember -class StaffMember extends LibraryMember -class Journal - -Book "0..1" - "1..*" BorrowableItem : is a copy -BorrowableItem "0..*" -- "0..1" LibraryMember : borrows/returns -BorrowableItem "1..*" - "0..1" Journal : is a copy - -hide empty fields -hide empty methods -{% endplantuml %} - -È imporante però preoccuparsi delle __cardinalità__ delle relazioni: è sì vero che un `BorrowableItem` può non _essere una copia di_ un `Book` e di un `Journal`, ma deve essere copia di _esattamente_ una delle due opzioni. -UML prevede un __linguaggio OCL__ ([Object Constraint Language](https://en.wikipedia.org/wiki/Object_Constraint_Language)) per esprimere vincoli divesamente impossibili da esprimere in un diagramma. -È anche possibile scrivere il _constraint_ in linguaggio naturale come __nota__. diff --git a/book.toml b/book.toml index 8c31f2b2991f3723a0eea6851aae4eb990317dcf..716a32deade27da2041a6c2215a0a51b3c41805e 100644 --- a/book.toml +++ b/book.toml @@ -7,6 +7,7 @@ title = "Ingegneria del software" [preprocessor.plantuml] plantuml-cmd = "plantuml" +clickable-img = true [output.html] mathjax-support = true diff --git a/src/07_progettazione/00_index.md b/src/07_progettazione/00_index.md new file mode 100644 index 0000000000000000000000000000000000000000..23dc9522f6de2c6c7e52b0cf9b83d7a46f502ebe --- /dev/null +++ b/src/07_progettazione/00_index.md @@ -0,0 +1,68 @@ +# Progettazione + +Durante le lezioni, per discutere di progettazione siamo partiti da un esempio di programma in C che stampa una canzone. +Il codice considerato è completamente illegibile: + +```c +#include <stdio.h> +main(t,_,a) +char *a; +{ +return!0<t?t<3?main(-79,-13,a+main(-87,1-_,main(-86,0,a+1)+a)): +1,t<_?main(t+1,_,a):3,main(-94,-27+t,a)&&t==2?_<13? +main(2,_+1,"%s %d %d\n"):9:16:t<0?t<-72?main(_,t, +"@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#\ +;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l \ +q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# \ +){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#n'wk nw' \ +iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c \ +;;{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')# \ +}'+}##(!!/") +:t<-50?_==*a?putchar(31[a]):main(-65,_,a+1):main((*a=='/')+t,_,a+1) +:0<t?main(2,2,"%s"):*a=='/'||main(0,main(-61,*a, +"!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry"),a+1); +} +``` + +Successivamente abbiamo scomposto il codice per renderlo logicamente più sensato e facilmente modificabile, sono state __estratte le parti comuni__ e spostate in una funzione apposita, mentre le __parti mutabili sono state salvate in alcune strutture dati__; la canzone viene così stampata tramite un ciclo. +In questo modo scrivendo un codice più semplice siamo stati in grado di creare una soluzione più generale e più aperta ai cambiamenti. + +```java +public class TwelveDaysOfChristmas { + static String[] days = {"first", "second", ..., "twelfth"}; + static String[] gifts = { "a partdrige in a pear tree", "two turtle doves", ... }; + + static String firstLine(int day) { + return "On the " + days[day] + + " day of Christmas my true love gave to me:\n"; + } + + static String allGifts(int day) { + if (day == 0) { + return "and " + gifts[0]; + } else { + return gifts[day] + "\n" + allGifts(day-1); + } + } + + public static void main(String[] args) { + System.out.println(firstLine(0)); + System.out.println(gifts[0]); + for (int day == 1; day < 12; day++) { + System.out.println(firstLine(day)); + System.out.println(allGifts(day)); + } + } +} +``` + +È importante quindi __adottare la soluzione più semplice__ (che __non è quella più stupida__!) e una misura convenzionale per dire quanto una cosa è semplice - almeno in Università - si esprime in termini del tempo dedicato dal programmatore all'implementazione. +Tale misura si sposa bene con il __TDD__, che richiede __brevi iterazioni__ di circa 10 minuti: se la feature attuale richiede più tempo è opportuno ridurre la portata scomponendo il problema. + +- [**Refactoring**](./01_refactoring.md): modifiche al codice senza nuove funzionalità; +- [**Design knowledge**](./02_design-knowledge.md): dove mantenere la conoscenza del design? +- [**Conoscenze preliminari**](./03_conoscenze-preliminari.md): object orientation, SOLID principles, reference escaping, encapsulation e information hiding, immutabilità, code smell +- [**Principio Tell-Don't-Ask**](./04_tell-dont-ask.md) +- [**Interface segregation**](./05_interface-segregation.md) +- [**Esempio**](./06_esempio/00_index.md) applicando i principi precedenti +- [**Analisi del testo naturale**](./07_analisi-testo-naturale.md) con _noun extraction_ diff --git a/src/07_progettazione/01_refactoring.md b/src/07_progettazione/01_refactoring.md new file mode 100644 index 0000000000000000000000000000000000000000..300e41bdd44a37b41ee9428b1671ac895bf7d582 --- /dev/null +++ b/src/07_progettazione/01_refactoring.md @@ -0,0 +1,16 @@ +# Refactoring +Durante il refactoring è opportuno rispettare le seguenti regole: +- le __modifiche al codice non devono modificare le funzionalità__: +il refactoring DEVE essere invisibile al cliente; +- __non possono essere aggiunti test aggiuntivi__ rispetto alla fase verde appena raggiunta. + +Se la fase di refactoring sta richiedendo troppo tempo allora è possibile fare rollback all'ultima versione verde e __pianificare meglio__ l'attività di refactoring, per esempio scomponendolo in più step. +Vale la regola del _"do it twice"_: il secondo approccio a un problema è solitamente più veloce e migliore. + +## Motivazioni + +Spesso le motivazioni dietro un refactoring sono: +- precedente __design molto complesso e poco leggibile__, a causa della velocità del passare ad uno _scenario verde_; +- __preparare il design di una funzionalità__ che non si integra bene in quello esistente; dopo aver raggiunto uno _scenario verde_ in una feature, è possibile che la feature successiva sia difficile da integrare. +In questo caso, se il _refactoring_ non è banale è bene fermarsi, tornare indietro e evolvere il codice per facilitare l'iterazione successiva (__design for change__). +- presenza di __debito tecnico__ su lavoro fatto in precendenza, ovvero debolezze e "scorciatoie" che ostacolano notevolmente evoluzioni future: _"ogni debito tecnico lo si ripaga con gli interessi"_. diff --git a/src/07_progettazione/02_design-knowledge.md b/src/07_progettazione/02_design-knowledge.md new file mode 100644 index 0000000000000000000000000000000000000000..8710c09c115a3014a33c4874b7145820f81103ac --- /dev/null +++ b/src/07_progettazione/02_design-knowledge.md @@ -0,0 +1,21 @@ +# Design knowledge + +La design knowledge è la __conoscenza del design__ architetturale di un progetto. +È possibile utilizzare: +- la __memoria__: non è efficace perché nel tempo si erode, specialmente in coppia; +- i __documenti di design__ (linguaggio naturale o diagrammi): se non viene aggiornato di pari passo con il codice rimane disallineato, risultando più dannoso che d'aiuto. +- le __piattaforme di discussione__ (version control, issue management): possono aiutare ma le informazioni sono sparse in luoghi diversi e di conseguenza difficili da reperire e rimane il problema di mantenere aggiornate queste informazioni. +- gli __UML__: tramite diagrammi UML si è cercato di sfruttare l'approccio ___generative programming___, ovvero la generazione automatica del codice a partire da specificazioni di diagrammi. +Con l'esperienza si è visto che non funziona. +- il __codice__ stesso: tramite la lettura del codice è possibile capire il design ma è difficile rappresentare le ragioni della scelta. + +È bene sfruttare tutte le tecniche sopra proposte __combinandole__, partendo dal codice. \ +È inoltre importante scrivere documentazione per spiegare le ragioni dietro le scelte effettuate e non le scelte in sé, che si possono dedurre dal codice. + +## Condivisione + +Per condividere tali scelte di design (il _know how_) è possibile sfruttare: +- __metodi__: con pratiche (come Agile) o addirittura l'object orientation stessa, che può essere un metodo astratto per condividere scelte di design. +- __design pattern__: fondamentali per condividere scelte di design, sono utili anche per generare un vocabolario comune (sfruttiamo dei nomi riconosciuti da tutti per descrivere i ruoli dei componenti) e aiutano l'implementazione (i pattern hanno delle metodologie note per essere implementati). +I pattern non si concentrano sulle prestazioni di un particolare sistema ma sulla generalità e la riusabilità di soluzioni a problemi comuni; +- __principi__: per esempio i principi SOLID. diff --git a/src/07_progettazione/03_conoscenze-preliminari.md b/src/07_progettazione/03_conoscenze-preliminari.md new file mode 100644 index 0000000000000000000000000000000000000000..4820c07e2ab7b73008f63884649749efdcd93689 --- /dev/null +++ b/src/07_progettazione/03_conoscenze-preliminari.md @@ -0,0 +1,110 @@ +# Conoscenze preliminari + +Prima di proseguire è bene richiamare concetti e termini fondamentali presumibilmente visti durante il corso di Programmazione II. + +## Object orientation + +Per essere definito _object oriented_, un linguaggio di programmazione deve soddisfare tre proprietà: +- __ereditarietà__: ovvero la possibilità di poter definire una classe ereditando proprietà e comportamenti di un'altra classe. +- __polimorfismo__: quando una classe può assumere diverse forme in base alle interfacce che implementa. +Il prof fa l'esempio del _tennista scacchista_: in un torneo di tennis è poco utile sostituire una persona che gioca a tennis ed è brava con gli scacchi (quindi una classe che implementa entrambe le interfacce) con una che gioca a scacchi. +Il collegamento tra capacità e oggetto è fatto __a tempo di compilazione__: non è importante quindi se la capacità non è ancora definita; +- __collegamento dinamico__: in Java il tipo concreto degli oggetti e quindi il problema di stabilire _quale metodo chiamare_ viene risolto durante l'esecuzione. +In C++ occorre esplicitare questo comportamento utilizzando la keyword `virtual`. + +## <a style="color: darkgreen">SOLID</a> principles + +Ci sono 5 parti che compongono questo principio: +1. __<span style="color: darkgreen"><big>S</big></span>INGLE RESPONSIBILITY__: una classe, un solo scopo. +Così facendo, le classi rimangono semplici e si agevola la riusabilità. +2. __<span style="color: darkgreen"><big>O</big></span>PEN-CLOSE PRINCIPLE__: +le classi devono essere aperte ai cambiamenti (_opened_) ma senza modificare le parti già consegnate e in produzione (_closed_). +Il refactoring è comunque possibile, ma deve essere preferibile estendere la classe attuale. +3. __<span style="color: darkgreen"><big>L</big></span>ISKOV SUBSTITUTION PRINCIPLE__: +c'è la garanzia che le caratteristiche eredidate dalla classe padre continuinino ad esistere nelle classi figlie. +Questo concetto si collega all'aspetto __contract-based__ del metodo Agile: le _precondizioni_ di un metodo di una classe figlia devono essere ugualmente o meno restrittive del metodo della classe padre. +Al contrario, le _postcondizioni_ di un metodo della classe figlia non possono garantire più di quello che garantiva il metodo nella classe padre. +Fare _casting_ bypassa queste regole. +4. __<span style="color: darkgreen"><big>I</big></span>NTERFACE SEGREGATION__: +più le capacità e competenze di una classe sono frammentate in tante interfacce più è facile utilizzarla in contesti differenti. +In questo modo un client non dipende da metodi che non usa. +Meglio quindi avere __tante interfacce specifiche__ e piccole (composte da pochi metodi), piuttosto che poche, grandi e generali. +5. __<span style="color: darkgreen"><big>D</big></span>EPENDENCY INVERSION__: +il codice dal quale una classe dipende non deve essere più __concreto__ di tale classe. +Per esempio, se il _telaio della FIAT 500_ dipende da uno specifico motore, è possibile utilizzarlo solo per quel specifico motore. +Se invece il telaio dipende da _un_ concetto di motore, non c'è questa limitazione. +In conlusione, le classi concrete devono tendenzialmente dipendere da classi astratte e non da altre classi concrete. + +## Reference escaping + +Il _reference escaping_ è una violazione dell'incapsulamento. + +Può capitare, per esempio: +- quando un getter ritorna un riferimento a un segreto; +```java +public Deck { + private List<Card> cards; + + public List<Card> getCards() { + return this.cards; + } +} +``` +- quando un setter assegna a un segreto un riferimento che gli viene passato; +```java +public Deck { + private List<Card> cards; + + public setCards(List<Card> cards) { + this.cards = cards; + } +} +``` + +- quando il costruttore assegna al segreto un riferimento che gli viene passato; +```java +public Deck { + private List<Card> cards; + + public Deck(List<Card> cards) { + this.cards = cards; + } +} +``` + +## Encapsulation e information hiding + +__Legge di Parnas (L8)__. +> _Solo ciò che è nascosto può essere cambiato liberamente e senza pericoli._ + +Lo stato mostrato all'esterno non può essere modificato, mentre quello nascosto sì. + +Questo principio serve per __facilitare la comprensione del codice__ e renderne più facile la modifica parziale senza fare danni. + +## Immutabilità + +Una classe è immutabile quando non c'è modo di modificare lo stato di ogni suo oggetto dopo la creazione. + +Per assicurare tale proprietà è necessario: +- __non fornire metodi di modifica__ allo stato; +- avere tutti gli __attributi privati__ per i tipi potenzialmente mutabili (come `List<T>`); +- avere tutti gli __attributi final__ se non già privati; +- assicurare l'__accesso esclusivo__ a tutte le parti non mutabili, ovvero non avere reference escaping. + +## Code smell + +I _code smell_ sono dei segnali che suggeriscono problemi nella progettazione del codice. +Di seguito ne sono elencati alcuni: +- __codice duplicato__: si può fare per arrivare velocemente al verde ma è da togliere con il refactoring. +Le parti di codice in comune possono quindi essere fattorizzate. +- __metodi troppo lunghi__: sono poco leggibili e poco riusabili; +- __troppi livelli di indentazione__: scarsa leggibilità e riusabilità, è bene fattorizzare il codice; +- __troppi attributi__: suggerisce che la classe non rispetta la single responsability, ovvero fa troppe cose; +- __lunghe sequenze di _if-else_ o _switch___; +- __classe troppo grande__; +- __lista parametri troppo lunga__; +- __numeri _magici___: è importante assegnare alle costanti numeriche all'interno del codice un nome per comprendere meglio il loro scopo; +- __commenti che spiegano cosa fa il codice__: indicazione che il codice non è abbastanza chiaro; +- __nomi oscuri o inconsistenti__; +- __codice morto__: nel programma non deve essere presente del codice irraggiungibile o commentato. Utilizzando strumenti di versioning è possibile riaccedere a codice precedentemente scritto con facilità. +- __getter e setter__: vedi principio di __tell don't ask__. diff --git a/src/07_progettazione/04_tell-dont-ask.md b/src/07_progettazione/04_tell-dont-ask.md new file mode 100644 index 0000000000000000000000000000000000000000..b7688cdba6a20b0072a6e541ceb98d1c502319b3 --- /dev/null +++ b/src/07_progettazione/04_tell-dont-ask.md @@ -0,0 +1,61 @@ +# <a href="https://martinfowler.com/bliki/TellDontAsk.html">Principio Tell-Don't-Ask</a> + + + +> Non chiedere i dati, ma dì cosa vuoi che si faccia sui dati + +Il responsabile di un'informazione è anche responsabile di tutte le operazioni su quell'informazione. + +Il principio _Tell-Don't-Ask_ sancisce che piuttosto di __chiedere__ ad un oggetto dei dati e fare delle operazioni con quei dati è meglio __dire__ a questo oggetto cosa fare con i dati che contiene. + +## Esempio + +Se desideriamo stampare il contenuto di tutte le carte in un mazzo possiamo partire da questo codice. + +```java +class Main { + public static void main(String[] args) { + Deck deck = new Deck(); + Card card = new Card(); + + card.setSuit(Suit.DIAMONDS); + card.setRank(Rank.THREE); + deck.getCards().add(card); + deck.getCards().add(new Card()); // <-- !!! + + System.out.println("There are " + deck.getCards().size() + " cards:"); + for (Card currentCard : deck.getCards()) { + System.out.println( + currentCard.getRank() + + " of " + + currentCard.getSuit() + ); + } + } +} +``` + +All'interno del ciclo reperiamo gli attributi della carta e li utilizziamo per stampare le sue informazioni. +Inoltre, nella riga evidenziata viene aggiunta una carta senza settare i suoi attributi. +La responsabilità della gestione dell'informazione della carta è quindi __erroneamente delegata__ alla classe chiamante. + +Per risolvere, è possibile trasformare la classe `Card` nel seguente modo: + +```java +class Card { + private Suit suit; + private Rank rank; + + public Card(@NotNull Suit s, @NotNull Rank r) { + suit = s; + rank = r; + } + + @Override + public String toString() { + return rank + " of " + suit; + } +} +``` + +l'informazione viene ora interamente gestita dalla classe `Card`, che la gestisce nel metodo `toString()` per ritornare la sua rappresentazione testuale. diff --git a/src/07_progettazione/05_interface-segregation.md b/src/07_progettazione/05_interface-segregation.md new file mode 100644 index 0000000000000000000000000000000000000000..22dcacce38c285b6a41c6da97b24b52e91fc6611 --- /dev/null +++ b/src/07_progettazione/05_interface-segregation.md @@ -0,0 +1,7 @@ +# Interface segregation + +Le interfacce possono _"nascere"_ tramite due approcci: +- __up front__: scrivere direttamente l'interfaccia; +- __down front__: scrivere il codice e quindi tentare di estrarne un'interfaccia. + +L'approccio down-front si adatta meglio al TDD ed è illustrato nel seguente esempio. diff --git a/src/07_progettazione/06_esempio/00_index.md b/src/07_progettazione/06_esempio/00_index.md new file mode 100644 index 0000000000000000000000000000000000000000..cfa990e9aa5fe07e04838ed37ecab1320dc8e514 --- /dev/null +++ b/src/07_progettazione/06_esempio/00_index.md @@ -0,0 +1,10 @@ +# Esempio con gerarchia Card / Deck + +In questo esempio sono trattati numerosi principi, come l'_interface segreagation_, _linking dinamico/statico_, _implementazione di interfacce multiple_ e il _contract based design_ vs la _programmazione difensiva_. + +- [**Interface segregation**](./01_interface-segregation.md) +- [**Collegamento statico e dinamico**](./02_collegamento-statico-dinamico.md) +- [**Loose coupling**](./03_loose-coupling.md) +- [**Interfacce multiple**](./04_interfacce-multiple.md) +- [**_Contract-based_ design vs programmazione difensiva**](./05_contract-based.md) +- [**Classi astratte**](./06_classi-astratte.md) diff --git a/src/07_progettazione/06_esempio/01_interface-segregation.md b/src/07_progettazione/06_esempio/01_interface-segregation.md new file mode 100644 index 0000000000000000000000000000000000000000..0675e34edebde6df709590d550d07211c34b97e2 --- /dev/null +++ b/src/07_progettazione/06_esempio/01_interface-segregation.md @@ -0,0 +1,48 @@ +# Interface segregation all'opera + +```java +public static List<Card> drawCards(Deck deck, int number) { + List<Card> result = new ArrayList<>(); + for (int i = 0; i < number && !deck.isEmpty(); i++) { + result.add(deck.draw()); + } + return result; +} +``` + +Consideriamo il metodo `drawCards` che prende come parametri un `Deck` e un intero. \ +Le __uniche competenze__ riconosciute a `Deck` sono l'indicazione _se è vuoto_ (`isEmpty()`) e il _pescare una carta_ dal mazzo (`draw()`). +`Deck` può quindi __implementare un'interfaccia__ che mette a disposizione queste capacità. + +È possibile modificare il metodo in modo da accettare un qualunque oggetto in grado di eseguire le operazioni sopra elencate, ovvero che implementi l'interfaccia __`CardSource`__. + +<a id="cardsource" style="display:none;"></a> + +```java +public interface CardSource { + /** + * @return The next available card. + * @pre !isEmpty() + */ + Card draw(); + + /** + * @return True if there is no card in the source + */ + boolean isEmpty(); +} +``` + +```java +public class Deck implements CardSource { ... } +``` + +```java +public static List<Card> drawCards(CardSource deck, int number) { + List<Card> result = new ArrayList<>(); + for (int i = 0; i < number && !deck.isEmpty(); i++) { + result.add(deck.draw()); + } + return result; +} +``` diff --git a/src/07_progettazione/06_esempio/02_collegamento-statico-dinamico.md b/src/07_progettazione/06_esempio/02_collegamento-statico-dinamico.md new file mode 100644 index 0000000000000000000000000000000000000000..61782d566c5ca5aeba7ec6ce9750333446dcc2ea --- /dev/null +++ b/src/07_progettazione/06_esempio/02_collegamento-statico-dinamico.md @@ -0,0 +1,8 @@ +# Collegamento statico e dinamico + +Notare come è necessario specificare __staticamente__ che `Deck` implementi `CardSource`, ovvero occorre forzare la dichiarazione del fatto che `Deck` sia un _sottotipo_ di `CardSource` (Java è strong typed) e quindi sia possibile mettere un oggetto `Deck` ovunque sia richiesto un oggetto `CardSource`. \ +In altri linguaggi come __Go__ c'è una __maggiore dinamicità__ perché non c'è bisogno di specificare nel codice che un oggetto è sottotipo di qualcos'altro, è sufficiente solo che implementi un metodo con la stessa signature. +Il controllo che l'oggetto passato ad una funzione abbia le capacità necessarie avviene a runtime e non prima. + +Un problema della troppa dinamicità (__duck typing__) è che se i metodi di un oggetto non hanno dei nomi abbastanza specifici si possono avere dei problemi. +Per esempio, in un programma per il gioco del tennis se una funzione richiede un oggetto che implementa il metodo `play()`, e riceve in input un oggetto che non c'entra nulla con il tennis (per esempio un oggetto di tipo `GiocatoreDiScacchi`) che ha il metodo `play()`, si possono avere degli effetti indesiderati. diff --git a/src/07_progettazione/06_esempio/03_loose-coupling.md b/src/07_progettazione/06_esempio/03_loose-coupling.md new file mode 100644 index 0000000000000000000000000000000000000000..0ba63ea584e25acca485c9890c4f45ab5bd1f10f --- /dev/null +++ b/src/07_progettazione/06_esempio/03_loose-coupling.md @@ -0,0 +1,8 @@ +# _Loose coupling_ +Il _loose coupling_ è la capacità di una variabile o un parametro di accettare l'assegnamento di oggetti aventi tipo diverso da quello della variabile o parametro, a patto che sia un sottotipo. + +```java +Deck deck = new Deck(); +CardSource source = deck; +List<Card> cards = drawCards(deck, 5); +``` diff --git a/src/07_progettazione/06_esempio/04_interfacce-multiple.md b/src/07_progettazione/06_esempio/04_interfacce-multiple.md new file mode 100644 index 0000000000000000000000000000000000000000..831f1eabdb27703e49c716ce2d6d823f7510b37c --- /dev/null +++ b/src/07_progettazione/06_esempio/04_interfacce-multiple.md @@ -0,0 +1,42 @@ +# Interfacce multiple + +Tornando all'esempio, la classe `Deck` (che implementa `CardSource`) __può implementare anche altre interfacce__, come `Shuffable` o `Iterable<Card>`. +Al metodo precedente interessa solo che Deck abbia le capacità specificate in `CardSource`, se poi implementa anche altre interfaccie è ininfluente. + +```plantuml +@startuml +scale 1024 width + +class Client1 +class Client2 +class Client3 + +interface Iterable<Card> << interface >> { + + {abstract} Iterator<Card> iterator() +} + +class Deck implements Iterable, Shuffable, CardSource { + + void shuffle() + + Card draw() + + boolean isEmpty() + + Iterator<Card> iterator() +} + +interface Shuffable << interface >> { + + {abstract} void shuffle() +} + +interface CardSource << interface >> { + + {abstract} Card draw() + + {abstract} boolean isEmpty() +} + +Client2 ..> Iterable +Client1 ..> Shuffable +Client3 ..> Iterable +Client3 ..> CardSource + +hide empty fields +hide empty methods +@enduml +``` \ No newline at end of file diff --git a/src/07_progettazione/06_esempio/05_contract-based.md b/src/07_progettazione/06_esempio/05_contract-based.md new file mode 100644 index 0000000000000000000000000000000000000000..d04decba42dc5dfbbf95e060b68c56f7ee9fd5a3 --- /dev/null +++ b/src/07_progettazione/06_esempio/05_contract-based.md @@ -0,0 +1,11 @@ +# _Contract-based_ design vs programmazione difensiva + +Tornando alla [specificazione](./01_interface-segregation.md#cardsource) dell'interfaccia di `CardSource`, è possibile notare dei commenti in formato Javadoc che specificano le __precondizioni__ e le __postcondizioni__ (il valore di ritorno) del metodo. Secondo il ___contract-based_ design__, esiste un _"contratto"_ tra chi implementa un metodo e chi lo chiama. + +Per esempio, considerando il metodo `draw()`, __è responsabilità del chiamante__ verificare il soddisfacimento delle precondizioni (_"il mazzo non è vuoto"_) prima di invocare il metodo. +Se `draw()` viene chiamato quando il mazzo è vuoto ci troviamo in una situazione di __violazione di contratto__ e può anche esplodere la centrale nucleare. + +Per specificare il contratto si possono utilizzare delle __asserzioni__ o il `@pre` nei __commenti__. +Le prime sono particolarmenti utili in fase di sviluppo perché interrompono l'esecuzione del programma in caso di violazione, ma vengono solitamente rimosse in favore delle seconde nella fase di deployment. + +Un'altro approccio è la __programmazione difensiva__ che al contrario delega la responsabilità del soddisfacimento delle precondizioni al _chiamato_, e non al chiamante. diff --git a/src/07_progettazione/06_esempio/06_classi-astratte.md b/src/07_progettazione/06_esempio/06_classi-astratte.md new file mode 100644 index 0000000000000000000000000000000000000000..22795cfb6a67597fb3d533743b76c05757e8970f --- /dev/null +++ b/src/07_progettazione/06_esempio/06_classi-astratte.md @@ -0,0 +1,29 @@ +# Classi astratte + +Una classe astratta che implementa un'interfaccia __non deve necessariamente implementarne__ tutti i metodi, ma può delegarne l'implementazione alle sottoclassi impedendo l'istanziamento di oggetti del suo tipo. + +Le interfacce diminuiscono leggermente le performance, però migliorano estremamente la generalità (che aiutano l'espandibilità ed evolvibilità del programma), quindi vale la pena di utilizzarle. + +È possibile utilizzare le __classi astratte__ anche per classi complete, ma che __non ha senso che siano istanziate__. +Un buon esempio sono le classi _utility_ della libreria standard di Java. + +#### Classe utility della libreria standard di Java + +Un esempio è __`Collections.shuffle(List<?> list)`__ che accetta una lista omogenea di elementi e la mischia. +Il _tipo_ degli elementi è volutamente ignorato in quanto non è necessario conoscerlo per mischiarli. + +Per l'__ordinamento__, invece, è necessario conoscere il tipo degli oggetti in quanto bisogna confrontarli tra loro per poterli ordinare. +La responsabilità della comparazione è però delegata all'oggetto, che deve aderire all'interfaccia `Comparable<T>`. + +__`Collections.sort(...)`__ ha, infatti, la seguente signature: +```java +public static <T extends Comparable<? super T>> void sort(List<T> list) +``` + +La notazione di generico __aggiunge dei vincoli__ su `T`, ovvero il tipo degli elementi contenuti nella lista: +- `T extends Comparable<...>` significa che `T` deve estendere - e quindi implementare - l'interfaccia `Comparable<...>`; +- `Comparable<? super T>` significa che tale interfaccia può essere implementata su un antenato di `T` (o anche `T` stesso). + +`Comparable` è un altro esempio di _interface segregation_: serve per specificare che un oggetto ha bisogno della caratteristica di essere comparabile. + +__Digressione__: la classe Collections era l'unico modo per definire dei metodi sulle interfacce (es: dare la possibilità di avere dei metodi sulle collezioni, ovvero liste, mappe, ecc), ma ora si possono utilizzare i metodi di default. diff --git a/src/07_progettazione/07_analisi-testo-naturale.md b/src/07_progettazione/07_analisi-testo-naturale.md new file mode 100644 index 0000000000000000000000000000000000000000..205d29a5367c21e109a1dcbfddf19f25644d6ce3 --- /dev/null +++ b/src/07_progettazione/07_analisi-testo-naturale.md @@ -0,0 +1,148 @@ +# Analisi del testo naturale + +Come organizzare la partenza del design suddividendo in classi e responsabilità? + +I due approcci principali sono: +- __pattern__: riconoscere una situazione comune da una data; +- __TDD__: partendo dalla soluzione più semplice si definiscono classi solo all'occorrenza. + +Un'altra tecnica che vedremo è l'__estrazione dei nomi__ (noun extraction), per un certo senso _naive_ ma adatta in caso di storie complesse. + +## Noun extraction + +Basandosi sulle specifiche — come i commenti esplicativi delle User Stories — si parte dai sostantivi (o frasi sostantivizzate), si _sfolsticono_ con dei criteri, si cercano le relazioni tra loro e quindi si produce la gerarchia delle classi. + +Per spiegare il procedimento considereremo il seguente esempio: + +> - _The __library__ contains __books__ and __journals__. +> It may have several __copies__ of a given book. +> Some of the books are for __short term loans__ only. +> All other books may be borrowed by any __library member__ for three __weeks__._ +> - ___Members of the library__ can normally borrow up to six __items__ at a time, but __members of staff__ may borrow up to 12 items at one time. +> Only member of staff may borrow journals._ +> - _The __system__ must keep track of when books and journals are borrowed and returned, enforcing the __rules__ described above._ + +Nell'esempio sopra sono stati evidenziati i sostantivi e le frasi sostantivizzate. + +### Criteri di _sfoltimento_ + +I criteri di _sfoltimento_ servono per diminuire il numero di sostantivi considerando solo quelli rilevanti per risolvere il problema. +In questa fase, in caso di dubbi è possibile rimandare la decisione a un momento successivo. + +Di seguito ne sono riportati alcuni: +- __Ridondanza__: sinonimi, termini diversi per indicare lo stesso concetto. Anche se è stata utilizzata una locuzione diversa potrebbe essere comunque ridondante, sopratutto in lingue diverse dall'inglese in cui ci sono molti sinonimi. \ +Nell'esempio: _library member_ e _member of the library_, _loan_ e _short term loan_. +- __Vaghezza__: nomi generici, comuni a qualunque specifica; potrebbero essere sintomo di una _classe comune astratta_. \ +Nell'esempio: _items_. +- __Nomi di eventi e operazioni__: nomi che indicano azioni e non hanno un concetto di _stato_. \ +Nell'esempio: _loan_. +- __Metalinguaggio__: parti _statiche_ che fanno parte della logica del programma e che quindi non necessitano di essere rappresentati come classi. \ +Nell'esempio: _system_, _rules_. +- __Esterne al sistema__: concetti esterni o verità _"assolute"_ al di fuori del controllo del programma. \ +Esempio: _library_, _week_ (una settimana ha 7 giorni). +- __Attributi__: informazioni atomiche e primitive (stringhe, interi, ...) relative a una classe, che quindi non necessitano la creazione di una classe di per sé. \ +Esempio: _name of the member_ (se ci fosse stato). + +Al termine di questa fase, si avrà una lista di classi _"certe"_ e _"incerte"_. +In questo esempio, sono soppravvisuti i termini _journal_, _book_, _copy_ (of _book_), _library member_ e _member of staff_. + +### Relazioni tra classi + +Il prossimo passo è definire le relazioni tra le classi. + +Inizialmente, si collegano con delle _linee_ (non frecce) senza specificare la direzione dell'associazione. +Parliamo di __associazioni__ e non __attributi__ perché non è necessariamente vero che tutte le associazioni si trasformino in attributi. + +```plantuml +@startuml +scale 1024 width + +class Book +class CopyOfBook +class LibraryMember +class StaffMember +class Journal + +Journal -- StaffMember : borrows/returns +Book -- CopyOfBook : is a copy +CopyOfBook -- LibraryMember : borrows/returns +CopyOfBook -- StaffMember : borrows/returns + +hide empty fields +hide empty methods +@enduml +``` + +Il prossimo passo è specificare le __cardinalità__ delle relazioni, come specificato dal linguaggio UML (opposto in questo aspetto al diagramma ER). +La precisione richiesta in questo punto è soggettiva: da una parte, specificare puntualmente il numero massimo di elementi di una associazione può aiutare ad ottimizzare successivamente, dall'altra porta confusione. + +```plantuml +@startuml +scale 1024 width + +class Book +class CopyOfBook +class LibraryMember +class StaffMember +class Journal + +Journal "0..*" -- "0..1" StaffMember : borrows/returns +Book "1" -- "1..*" CopyOfBook : is a copy +CopyOfBook "0..*" -- "0..1" LibraryMember : borrows/returns +CopyOfBook "0..*" -- "0..1" StaffMember : borrows/returns + +hide empty fields +hide empty methods +@enduml +``` + +Dopo aver ragionato sulle cardinalità, si iniziano a cercare __generalizzazioni__ e fattorizzazioni. +In questo caso, notiamo che: +- `StaffMember` _è un_ `LibraryMember` con in più la possibilità di prendere `Journal`. +Inoltre, un altro indicatore è che hanno la stesso tipo di relazioni con gli altri oggetti. +- `Items` è un termine generico per indicare `CopyOfBook` e `Journal`. + +```plantuml +@startuml +scale 1024 width + +class BorrowableItem +class LibraryMember +class Book +class CopyOfBook extends BorrowableItem +class StaffMember extends LibraryMember +class Journal extends BorrowableItem + +Book "1" -- "1..*" CopyOfBook : is a copy +BorrowableItem "0..*" -- "0..1" LibraryMember : borrows/returns + +hide empty fields +hide empty methods +@enduml +``` + +Distinguere `CopyOfBook` e `Journal` è inutile, perché di fatto un `Journal` _è una copia di_ un giornale. +Si può quindi fattorizzare __rimuovendo la generalizzazione__, come mostrato di seguito. + +```plantuml +@startuml +scale 1024 width + +class Book +class BorrowableItem +class LibraryMember +class StaffMember extends LibraryMember +class Journal + +Book "0..1" - "1..*" BorrowableItem : is a copy +BorrowableItem "0..*" -- "0..1" LibraryMember : borrows/returns +BorrowableItem "1..*" - "0..1" Journal : is a copy + +hide empty fields +hide empty methods +@enduml +``` + +È imporante però preoccuparsi delle __cardinalità__ delle relazioni: è sì vero che un `BorrowableItem` può non _essere una copia di_ un `Book` e di un `Journal`, ma deve essere copia di _esattamente_ una delle due opzioni. +UML prevede un __linguaggio OCL__ ([Object Constraint Language](https://en.wikipedia.org/wiki/Object_Constraint_Language)) per esprimere vincoli divesamente impossibili da esprimere in un diagramma. +È anche possibile scrivere il _constraint_ in linguaggio naturale come __nota__. diff --git a/src/SUMMARY.md b/src/SUMMARY.md index fe1290fb9fc897257c8fee3ec57eb83270b7443c..be67256a77bcf68addf63c6c1ffaa0bfefadbacf 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -1,6 +1,6 @@ # Summary -# 1. Prodotto e processo +# Prodotto e processo - [Ingegneria, qualità e processi](./01_introduzione/00_index.md) - [Informazioni logistiche](./01_introduzione/01_logistica.md) @@ -56,8 +56,23 @@ - [Build automation](./06_git-workflow/04_strumenti-opensource/01_build-automation.md) - [Bug tracking](./06_git-workflow/04_strumenti-opensource/02_bug-tracking.md) -# 2. Progettazione e implementazione +# Progettazione e implementazione -# 3. Verifica e convalida +- [Progettazione](./07_progettazione/00_index.md) + - [Refactoring](./07_progettazione/01_refactoring.md) + - [Design knowledge](./07_progettazione/02_design-knowledge.md) + - [Conoscenze preliminari](./07_progettazione/03_conoscenze-preliminari.md) + - [Principio Tell-Don't-Ask](./07_progettazione/04_tell-dont-ask.md) + - [Interface segregation](./07_progettazione/05_interface-segregation.md) + - [Esempio](./07_progettazione/06_esempio/00_index.md) + - [Interface segregation all'opera](./07_progettazione/06_esempio/01_interface-segregation.md) + - [Collegamento statico e dinamico](./07_progettazione/06_esempio/02_collegamento-statico-dinamico.md) + - [Loose coupling](./07_progettazione/06_esempio/03_loose-coupling.md) + - [Interfacce multiple](./07_progettazione/06_esempio/04_interfacce-multiple.md) + - [Contract-based design vs programmazione difensiva](./07_progettazione/06_esempio/05_contract-based.md) + - [Classi astratte](./07_progettazione/06_esempio/06_classi-astratte.md) + - [Analisi del testo naturale](./07_progettazione/07_analisi-testo-naturale.md) -# 4. Reti di Petri +# Verifica e convalida + +# Reti di Petri