Archivi tag: modularità

Modularità delle funzioni

Proseguiamo la nostra esplorazione nel mondo delle funzioni personalizzate e rendiamo lo sketch che abbiamo realizzato con l’esercizio Bouncing Ball (parte 1, parte 2) modulare.

Per diventare dei buoni programmatori è necessario lavorare costantemente alla riscrittura del proprio codice: è un processo continuo di riorganizzazione e ottimizzazione.

Riprendiamo il codice a cui eravamo arrivati con l’ultimo esercizio:

int ellipseX;
int ellipseY;
int speedX = 1;
int speedY = 1;

void setup() {
  size(700, 500);
  ellipseX = 0;
  ellipseY = height/2;
}

void draw() {
  background(0);
  ellipse(ellipseX, ellipseY, 50, 50);
  if(ellipseX > width || ellipseX < 0) {
    speedX = speedX * -1;
  }
  if(ellipseY > height || ellipseY < 0) {
    speedY = speedY * -1;
  }
  ellipseX = ellipseX + speedX;
  ellipseY = ellipseY + speedY;
  //println("EllipseX: " + ellipseX + " SpeedX: " + speedX);
}

Divisione in  moduli

In quanti moduli possiamo suddividere il nostro programma? Rileggiamo il codice e analizziamo ogni porzione di codice:

  • Righe 1 - 4: dichiarazione delle variabili
  • Righe 6- 10: configurazione generale con la funzione setup()
  • Righe 13-14: disegniamo la nostra palla
  • Righe 15-20: controlliamo che la palla non vada oltre i bordi della finestra e, se necessario, invertiamo la direzione del movimento.
  • Righe 21-22: facciamo muovere la nostra palla

Chiaramente i primi due punti non possono essere modificati: le variabili devono funzionare in vari punti del nostro programma quindi sono pubbliche mentre la parte di configurazione è già all'interno della funzione setup().

Gli altri 3 punti, invece, sono tutti all'interno della funzione draw() ma si occupano di tre attività differenti: disegnare, controllare e muovere la palla. Creiamo un modulo per ciascuna di esse.

Ricordo che non dovendo restituire nessun valore, utilizzeremo la parola void e che possiamo nominare ciascuna funzione come preferiamo purché non si utilizzino parole già riservate.

La funzione drawBall() sarà quella dedicata al disegno della palla

void drawBall() {
 background(0);
 ellipse(ellipseX, ellipseY, 50, 50);
}

moveBall() si occuperà del movimento

void moveBall() {
 ellipseX = ellipseX + speedX;
 ellipseY = ellipseY + speedY;
}

E infine checkEdges() verificherà se la palla ha raggiunto un bordo

void checkEdges() {
 if (ellipseX > width || ellipseX < 0) {
 speedX = speedX * -1;
 }
 if (ellipseY > height || ellipseY < 0) {
 speedY = speedY * -1;
 }
}

Bouncing Ball: modulare

Ecco il codice riscritto:

int ellipseX;
int ellipseY;
int speedX = 1;
int speedY = 1;

void setup() {
  size(700, 500);
  ellipseX = 0;
  ellipseY = height/2;
}

void draw() {
  drawBall();
  checkEdges();
  moveBall();
}

void drawBall() {
  background(0);
  ellipse(ellipseX, ellipseY, 50, 50);
}

void moveBall() {
  ellipseX = ellipseX + speedX;
  ellipseY = ellipseY + speedY;
}

void checkEdges() {
  if (ellipseX > width || ellipseX < 0) {
    speedX = speedX * -1;
  }
  if (ellipseY > height || ellipseY < 0) {
    speedY = speedY * -1;
  }
}

Se clicchiamo su Run non noteremo alcuna differenza di funzionamento tra questo codice e quello incollato a inizio post.

Qualcuno potrebbe giustamente notare che, però, siamo passati da 24 linee di codice a 34: ben 10 righe di codice in più senza aver, di fatto, cambiato nulla nel funzionamento del nostro programma. Di contro, però, il nostro codice è più pulito e leggibile.

Il reale miglioramento si capirà quando, con il prossimo post, cominceremo a parlare di oggetti.

Funzioni personalizzate

Iniziamo con un nuovo importante capitolo nel nostro percorso di introduzione alla programmazione con Processing. Se state seguendo questa serie di post dall’inizio, arrivati a questo punto vi sarete resi conto che negli esempi che ho proposto ho sempre cercato di seguire due regole auree della programmazione:

  1. scrivere il minor numero di righe di codice possibile
  2. scrivere codice semplice da leggere e interpretare.

Per quanto riguarda il primo punto, non penso che i programmatori siano pigri ma, al contrario, mi piace pensare che amino arrivare dritti al punto, senza perdersi in fronzoli o distrazioni.

In merito alla leggibilità, capirete quanto è importante scrivere bene il proprio codice quando vi capiterà di dover rimettere mano a dei progetti realizzati qualche mese o, addirittura, qualche anno prima.

Come anticipavo nell’incipit, con questo post cominceremo  un percorso che ci porterà a capire e a utilizzare la programmazione orientata agli oggetti (OOP), uno dei paradigmi fondamentali della programmazione moderna.

Un mondo di funzioni

Fin dal primo post abbiamo parlato di funzioni e, con il susseguirsi degli articoli, ne abbiamo utilizzate parecchie. Abbiamo imparato che queste funzioni ci permettono, ad esempio, di impostare la grandezza della finestra di lavoro oppure di disegnare forme geometriche.

Alcune di esse hanno bisogno di uno o più parametri per funzionare correttamente, penso, ad esempio, a fill() altre, invece, devono semplicemente essere richiamate e non necessitano di ulteriori dati come setup() o draw().

I creatori di Processing hanno già inserito all’interno del linguaggio una serie di funzioni per rendere il linguaggio semplice da utilizzare. Provate a pensare a quanto sarebbe difficile disegnare un cerchio senza avere a disposizione la funzione ellipse().

Come per le variabili built-in, le parole che identificano le funzioni già inserite all’interno di Processing sono riconoscibili perché vengono evidenziate automaticamente. Ovviamente è possibile creare delle funzioni personalizzate a patto che il nome scelto non sia già riservato da una funzione built-in.

Modularità e riusabilità

Il numero di righe di codice di un programma determina la sua complessità; cominciare a utilizzare funzioni personalizzate e, come vedremo tra qualche settimana, gli oggetti ci permette di rendere il codice modulare e riusabile.

Perché questo punto è molto importante? Un programma modulare è facile da debuggare e da modificare e/o riadattare alle proprie esigenze. Vi ricordate la differenza tra un programma con parametri hard-coded e lo stesso programma scritto con le variabili?

Il principio è lo stesso ed è come dividere il contenuto di un manuale in capitoli: grazie alla modularità delle funzioni possiamo trovare subito le porzioni di codice che ci interessa leggere o modificare.

In merito alla riusabilità: se scriviamo delle funzioni complesse, possiamo tranquillamente copiarle e incollarle in un nuovo progetto senza perdere tempo a riscriverle da capo con un notevole risparmio di tempo e risorse.

Return e void

Arriviamo al punto: come si scrive una funzione? Innanzitutto dobbiamo capire se la nostra funzione è pensata per restituire un valore oppure no. Facciamo un esempio per ciascun caso:

  1. Una funzione che, ogni volta che viene richiamata, mi disegni un fiore sullo schermo.
  2. Una funzione matematica che aggiunga 5 a ogni numero che viene utilizzato come parametro.

Nel primo caso la funzione non restituisce alcun valore: disegna semplicemente un fiore sullo schermo. In questa funzione potremmo comunque passare dei parametri come, ad esempio, la posizione iniziale del fiore ma non ci verrà restituito nessun valore né numerico né di testo dalla funzione stessa.

Nel secondo caso, invece, utilizziamo una funzione proprio per ricevere in cambio un numero.

Le due parole magiche che dovrò utilizzare in Processing sono voidreturn.

La nostra prima funzione: Flower

void setup() {
  size(700, 700);
  background(255);  
}

void draw() {
  flower(width/2, height/2);
}

void flower(int posizioneX, int posizioneY) {
  noStroke();
  fill(0, 255, 0);
  rectMode(CENTER);
  rect(posizioneX, posizioneY+100, 25, 100);
  fill(255, 0, 255);
  ellipse(posizioneX-50, posizioneY, 70, 70);
  ellipse(posizioneX, posizioneY-50, 70, 70);
  ellipse(posizioneX+50, posizioneY, 70, 70);
  ellipse(posizioneX, posizioneY+50, 70, 70);
  fill(255, 255, 0);
  ellipse(posizioneX, posizioneY, 50, 50);
  noFill();
}

Ecco il risultato:Funzioni personalizzate: flower

La parte di codice che ci interessa parte dalla linea 10:

void flower(int posizioneX, int posizioneY) {

Sto dicendo a Processing di creare una funzione che non restituirà nessun parametro (void) di nome flower che accetta due parametri in input di tipo integer: posizioneX posizioneY. La parentesi graffa alla fine mi serve per aprire il blocco di codice della funzione.

Nelle linee successive disegno il fiore utilizzando più volte la funzione ellipse per i petali e il pistillo e un rect per il gambo. I parametri posizioneXposizioneY vengono richiamati per impostare correttamente la posizione del fiore nella finestra.

Con la parentesi graffa finale, chiudo il blocco di codice della funzione.

Una volta scritta la funzione, è sufficiente richiamarla in setup() o in draw() passandogli le due variabili di tipo integer che la funzione si aspetta di ricevere.

Ora se io volessi disegnare più fiori all’interno della finestra, sarebbe sufficiente richiamare la funzione flower il numero di volte necessario:

void setup() {
  size(700, 700);
  background(255);  
}

void draw() {
  flower(width/2, height/2);
  flower(100, 200);
  flower(500, 300);
  flower(100, 600);
}

void flower(int posizioneX, int posizioneY) {
  noStroke();
  fill(0, 255, 0);
  rectMode(CENTER);
  rect(posizioneX, posizioneY+100, 25, 100);
  fill(255, 0, 255);
  ellipse(posizioneX-50, posizioneY, 70, 70);
  ellipse(posizioneX, posizioneY-50, 70, 70);
  ellipse(posizioneX+50, posizioneY, 70, 70);
  ellipse(posizioneX, posizioneY+50, 70, 70);
  fill(255, 255, 0);
  ellipse(posizioneX, posizioneY, 50, 50);
  noFill();
}

Funzioni personalizzate in Processing

Se volessi creare un prato pieno di fiori, potrei inserire la funzione all’interno di un ciclo e, ovviamente, potrei aggiungere dei parametri ulteriori per creare, ad esempio, fiori di colore e grandezza diversi:

void setup() {
  size(700, 700);
  background(255);
  for(int i = 0; i < 20; i++) {
    flower(int(random(width)), int(random(height)), color(random(255), random(255), random(255)), int(random(60, 100)));
  }
}

void draw() {
  
}

void flower(int posizioneX, int posizioneY, color colore, int petali) {
  noStroke();
  fill(0, 255, 0);
  rectMode(CENTER);
  rect(posizioneX, posizioneY+100, 25, 100);
  fill(colore);
  ellipse(posizioneX-50, posizioneY, petali, petali);
  ellipse(posizioneX, posizioneY-50, petali, petali);
  ellipse(posizioneX+50, posizioneY, petali, petali);
  ellipse(posizioneX, posizioneY+50, petali, petali);
  fill(255, 255, 0);
  ellipse(posizioneX, posizioneY, 50, 50);
  noFill();
}

Un campo di fiori

Funzioni che restituiscono valori

Passiamo ora al secondo esempio:

void setup() {
  println(aggiungiCinque(10));
}

void draw() {
}

int aggiungiCinque(int numero) {
  return numero+5;
}

Abbiamo creato una funzione che restituisce un tipo di dato integer chiamata aggiungiCinque che accetta un parametro di tipo integer. Con la parola chiave return restituiamo il valore passato come parametro a cui abbiamo sommato 5;

Nella funzione setup() abbiamo chiamato la nostra funzione aggiungiCinque(10) passando il valore 10. Nella console, ci verrà stampato, correttamente, il valore 15.

È di fondamentale importanza indicare correttamente sia il tipo di valore che la funzione restituirà in output sia quello che può accettare in input. Se, infatti, passassimo il valore 10.5, un float, Processing ci restituirebbe un errore come nell'immagine:

Errore in input

Modificando la funzione per accettare un float ma lasciando la restituzione di un parametro integer otteniamo un altro tipo di errore sul return:

Errore sull'output