lunedì 14 marzo 2016

Progetto in Java: Cruciverba - I buoni pattern di programmazione

In questo post elencherò alcuni pattern di programmazione utili per lo sviluppo di un "buon" programma prendendo spunto dagli elaborati che sono stati consegnati al Prof. Zanzotto per il corso di LMP.

Parte I - La Legge di Murphy

« Se qualcosa può andar male, andrà male.»
La legge di Murphy può essere utilizzata come uno dei principi cardine della Programmazione Difensiva. La programmazione difensiva consiste, in poche parole, nel cercare di prevenire tutte le possibili condizioni di errore che possono avvenire all'interno del codice. Supponiamo di avere il seguente metodo java :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private boolean controllaVerticali(Schema s, ListaParole p) {
		boolean risultato = true;
		//controllo tutte le colonne
		//ma appena il controllo su una fallisce, interrompo il controllo
		//e restituisco false
		int r = 0; //indice della riga in cui inizia la parola "verticale" corrente
		String parolaCorrente;
		for (int c=0;c<s.schema[0].length && risultato ; c++){
			r = trovaProssimoNonNeroSuColonna(s,c,r); //cerco la prima lettera della prossima parola su questa colonna
			while (r<s.schema.length){ //controllo tutta la colonna corrente

				parolaCorrente = "";
				//aggiungo alla parola corrente tutti i caratteri fino alla prossima casella nera o alla fine della colonna 
				while(r<s.schema.length && s.schema[r][c].usabile){
					parolaCorrente += s.schema[r][c].carattere;
				}
				if (!controllaParolaSensata(parolaCorrente, p))
					return false;
				r = trovaProssimoNonNeroSuColonna(s,c,r+parolaCorrente.length()); //cerco la prima lettera della prossima parola su questa colonna
			}


		}
		return risultato;
	}

Alla linea 8 abbiamo la seguente istruzione:

for (int c=0;c<s.schema[0].length && risultato ; c++)
                    

L'istruzione precedente può generare condizioni di errore di questo tipo:
  • s è null
  • s.schema è null
  • s.schema[0] contiene un elemento uguale a null
  • s.schema[0] corrisponde ad un elemento non valido. Il vettore s.schema potrebbe non contenere alcun elemento.
Per evitare che l'applicazione termini con una una famigerata "NullPointerException" è necessario effettuare dei controlli sulle variabili che vengono utilizzate all'interno dei nostri metodi. Una possibile soluzione a questo problema è :

if(s != null && s.schema != null && s.schema[0] != null){
   for (int c=0;c<s.schema[0].length && risultato ; c++){
	  ...
	}
}

Parte II - Public vs Private e l' Information Hiding

Un altro aspetto importante della programmazione ad oggetti, specialmente nel caso del linguaggio java, è cercare di rispettare il principio dell'information hiding.
L'information Hiding si basa sul principio che i dettagli interni dell'implementazione di una classe dovrebbero essere nascosti alle altre classi: in questo modo è possibile migliorare la modularizzazione delle applicazioni. Scegliere se rendere un attributo di una classe private o pubblic riflette l'uso dell'information hiding sul nostro codice in quanto non permette alle altre classi di accedere a tale parte della classe se non attraverso la classe stessa.

Esamiamo il seguente metodo:


1
2
3
4
5
6
7
private boolean controllaParolaSensata(String parolaCorrente, ListaParole p) {
 for (Parola parola : p){
	 if (parola.stringa.equals(parolaCorrente))
		 return true;
	 }
 return false;
}

E' giusto accedere all'attributo parola.stringa della classe Parola in modo pubblico ? La risposta è no in quanto è un attributo che deve gestire solamente la classe Parola e non Schema. Inoltre, rendendolo privato, si elimina la possibilità di modificarlo direttamente: azione che deve accadere solamente tramite la classe che gestisce tale attributo (attraverso i metodi get e set).
Una soluzione a questo problema potrebbe essere la seguente:

public class Parola {
 private String stringa;
 ...
 public String getStringa() {
  if (stringa == null){
    string = "";
   }
  return stringa;
 }

In questo modo rendendo privato l'attributo e aggiungendo il metodo getString() obblighiamo il programmatore ad accedere all'attributo solamente tramite la classe Parola. In aggiunta è stato attuata anche una tecnica di programmazione difensiva prevenendo una delle situazioni descritte nella Parte I.
Un altro esempio che può aiutare a comprendere meglio il problema descritto precedentemente è il seguente:

1
2
3
4
5
6
7
8
// scrive all'interno della riga la parola
public static void scriviInCaselle(Schema schema, Parola parola, Coordinata coordinata) {
 int i;
 for (i = 0; i < parola.getParola().length(); i++) {
	schema.getCaselle()[coordinata.getX() + i][coordinata.getY()].setLettera(parola.getParola().charAt(i));
	schema.getCaselle()[coordinata.getX() + i][coordinata.getY()].setFree(false);
 }
}

Supponiamo di cambiare l'attributo caselle della classe Schema da Caselle[][] a Matrix (un oggetto che rappresenta una matrice). A questo punto, le istruzioni alla riga 5 e 6, non sono piu valide in quanto il tipo Matrix è diverso da tipo Caselle[][] e non è possibile accedervi con l'istruzione
[coordinata.getX() + i][coordinata.getY()]
Cambiando il tipo dell'attributo caselle è necessario cambiare tutte le istruzioni di codice del tipo [coordinata.getX() + i][coordinata.getY()] presenti all'interno del nostro programma. Questo problema è sinonimo di un alto accoppiamento tra la classi che stiamo utilizzando e la classe Schema: effettuare delle modifiche alla classe Schema comporta modificare tutte le classi che fanno utilizzo di quest'ultima.
Per risolvere questo problema è utile introdurre un metodo all'interno di Schema che restituisca una casella



public class Schema {
 ...
 public Casella getCasella(int x, int y) {
    return caselle[x][y];
 }
}

In questo modo se in futuro dovesse cambiare il metodo di accesso ad una casella sarà necessario modificare solamente il codice all'interno del metodo getCasella(...).

III - L'arabo è difficile da capire

Un aspetto fondamentale che spesso viene trascurato è l'inserimento dei commenti all'interno del codice. I commenti non solo aiutano a capire il flusso del programma ad un programmatore che non non conosce il programma ma anche a far riflettere sulla implementazione del programma: processo che può essere utile per rivedere la logica del codice.
Ad esempio:

private Schema controllaColonna(int row, Schema s, ListaParole p, Stack<Insieme> pila) {
boolean check = true;
int j = 0;
for(; j < s.schema.length - 1 && check; j++){
 if(!s.schema[j][row].piena && !s.schema[j+1][row].piena){
	ArrayList<Character> parola = new ArrayList<Character>();
	int z = 0;
	for(z = 0; j < s.schema.length - 1 && !s.schema[j+1][row].piena; j++, z++){
		parola.add(z, s.schema[j][row].carattere);
	}
	if(z == s.schema.length - 1){
	 parola.add(z, s.schema[j][row].carattere);
	}
	z = 0;
	for(; z < p.size() && z >= 0; z++){
	 if(parola.toString().compareTo(p.get(z).stringa)==0){
		z = -1;
	  }
	}
	if(z == p.size()){
	  pila.pop();
	  check = false;
	}
 }
}
if(j == s.schema.length - 1){
	Schema schem = s.clona();
	return schem;
}
return null;
}

L'assenza di commenti all'interno del codice precedente non permette una immediata comprensione della logica utilizzata all'interno del metodo. Inoltre è necessario dare un nome significativo alle variabili, specialemente quando si lavora con molti indici (per i cicli for).

Nessun commento:

Posta un commento