Filosofia della programmazione ad oggetti Aggiungi un commento

30 gennaio 2009, 15:12

Da quando mi sono avvicinato alla programmazione ad oggetti, anni fa, durante il corso di Programmazione da 12 crediti, mi è sempre parso un metodo piuttosto comodo per programmare, secondo certe esigenze. Non dico di non aver trovato di meglio, ma quasi.

Un oggetto non è altro che un insieme vivente e coerente di caratteristiche e comportamenti. Vivente perchè può nascere, morire, figliare (e i figli ne ereditano qualsiasi tratto)… ma anche perdersi, impazzire, esplodere trascinando nella tomba altri oggetti (e il programma stesso, se non si sta attenti).

Al tempo dell’esame di Programmazione, sapevo cos’è un oggetto solo secondo la patriarcale concezione Java in cui “tutto deriva da Object”, dunque qualsiasi programma si compone di oggetti, qualsiasi struttura dati è un oggetto e così via: ammirevole, perchè permette di poter richiedere, ad esempio, il campo length su qualsiasi array, ma questo l’ho scoperto solo più tardi.

La vera bellezza degli oggetti mi è stata svelata anni dopo, quando ho imparato parte di C# per poter programmare nel framework XNA. Là, potevo definire una classe “Renderable”, che definisce qualsiasi oggetto visualizzabile sullo schermo; definire una classe “Ball” figlia di Renderable, e le classi RedBall, BlueBall, BlackBall tutte figlie di Ball. Un oggetto rappresenta, quindi, numerosi livelli di astrazione prima della definizione vera e propria di un’entità. E l’approccio, se usato bene, funziona anche quando si ha bisogno (ancora) di pochi oggetti, perchè saranno comunque brevi, ben definiti e riutilizzabili: modulari.

Fin qui, tutto rose e fiori. Ma se il paradigma ad oggetti va fuori controllo, se si tenta di usare oggetti contro natura, ci si trova con un’orribile zuppa di codice piena di oggetti qua e là, magari usati una sola volta, variabili singleton, enti statici. Un ibrido di paradigma imperativo e ad oggetti, realmente nessuno dei due. Se poi non è commentata, peggio ancora: diventa inutilizzabile; ma questo vale per qualsiasi codice.

Il mio assegnamento di stage è la creazione di uno strumento interattivo per la realizzazione del metodo del simplesso. L’ho chiamato Simply. Sto creando l’interfaccia a mano, il che è già un conseguimento non da poco; ma meglio ancora, sto cercando di applicare il paradigma ad oggetti dove possibile. Ad esempio, ma finora sono solo prototipi di idee, l’algoritmo del simplesso è un oggetto, ogni variabile è un oggetto (in grado di dirmi se è una variabile di base, se è originale o slack e di rispondere con il proprio numero se interrogata), ogni parte dell’interfaccia è un oggetto in cui non avviene nessuna computazione: l’interfaccia deve preoccuparsi di fare l’interfaccia. Fare altrimenti, complicherebbe solo le cose e snaturerebbe il paradigma ad oggetti. Un cane non è anche un gatto che è un canarino, è semplicemente un cane. (Tutti sono animali, però).

Auguratemi buon lavoro e buona voglia, ne ho bisogno!

P.s.: A tutti gli amanti del Gibber Italicus, in foto: Non volevo offendere voi né il nobile animale, ma solo porre un esempio di forme non propriamente definibili con forme “standard”.

Gestione della memoria in C – Le considerazioni di un inesperto Aggiungi un commento

14 dicembre 2007, 11:44

Oggi ho finalmente dato l’esame che preparavo da due settimane, Laboratorio di algoritmi e strutture dati (per chi se lo stesse chiedendo, esiste solo per gli immatricolati prima del 2005/2006). Voto superiore alle mie aspettative, perchè il progettino richiesto… ecco, per farla breve, non funzionava. Ma avevo studiato la teoria e ho capito da solo i miei errori nella pratica, e questo mi è valso la promozione e un voto accettabile, anzi buono, tutto sommato.

Il progettino in questione prevedeva di implementare, in C o C++, l’algoritmo di Kosaraju per trovare le componenti fortemente connesse di un grafo orientato. Diviso in 4 parti: lettura dei dati da un file e implementazione delle strutture dati necessarie, implementazione della visita in profondità (DFS) sul grafo, creazione del grafo trasposto, isolamento delle componenti fortemente connesse. Corrispondono grosso modo ai passi dell’algoritmo, che consiste in:

  • Dato un grafo, effettuarne una DFS;
  • Creare il grafo trasposto;
  • Effettuare una DFS del grafo trasposto, visitando i vertici nell’ordine di fine visita decrescente dato dalla prima DFS;
  • Isolare gli alberi DFS dell’ultima visita, che risultano essere le componenti fortemente connesse.

L’implementazione di questi algoritmi è di per sé facile, una volta conosciute le basi del linguaggio. Non serve altro che tradurre in C (o C++) l’algoritmo in pseudocodice presentato nell’ottimo libro Introduzione agli algoritmi e strutture dati di Cormen, Leiserson, Rivest e Stein, una vera bibbia per trovare velocemente tutto ciò che c’è da sapere sugli algoritmi e le strutture di dati elementari (si parla di astrazioni naturalmente, non dell’implementazione in un particolare linguaggio).

I problemi vengono a monte, quando si tratta di implementare la struttura dati necessaria, soprattutto per me che da troppo poco tempo mastico il linguaggio C.

Questo potente strumento di programmazione, con tutta la sua libertà, ha uno svantaggio: non gestisce automaticamente la memoria del sistema, ma ne affida la gestione completamente al programmatore. Che, nel mio caso, può essere inesperto, non sapere esattamente come funzionano le cose e trovarsi davanti ad errori del tutto inaspettati e a prima vista incomprensibili.

Ogni variabile che non sia di un tipo primitivo di dati, dev’essere allocata: bisogna, con un comando esplicito (cioè malloc e le sue varianti calloc e realloc), dichiarare che si vuole riservare X bytes di memoria per quell’oggetto. Alla fine dell’utilizzo, è buona norma liberare la memoria così riservata, con il comando free. Se non lo si fa, si rischia di “scrivere dove non si dovrebbe” (parole del mio professore di Laboratorio di algoritmi). Stesso discorso se si alloca la quantità sbagliata di memoria o se si usa più memoria del previsto, andando, per esempio, fuori dei confini di un array. Supponiamo di avere allocato spazio per 10 caratteri, assegnati a una variabile name di tipo char *, usando regolarmente una malloc:
Allocazione di 10 caratteri.
E ora, supponiamo di aver dichiarato, per esempio, un array di int, numbers[], senza dichiararne la dimensione e dimenticando di allocare la memoria necessaria, riempendolo volta per volta. Come può benissimo accadere, il primo valore, numbers[0] (ogni int occupa una parola di 4 bytes, qui sto rappresentando i bytes come caratteri ASCII) non è molto distante dal blocco di memoria puntato da name.
Assegnazione di un primo valore correttamente allocato
Ora, ecco cosa succede se assegno qualche valore a numbers[1] e numbers[2]:
Assegnazione di valori su memoria non allocata...
E infine, assegnando un valore anche a numbers[3]:
...con relativo comportamento imprevisto!
E’ successo un guaio: i valori per cui non era stato allocato spazio, scritti su blocchi di memoria contigui, sono andati a sovrascrivere parte del mio nome, che credevo memorizzato al sicuro nella variabile name. Per evitare questo problema, sarebbe stato sufficiente dichiarare la dimensione dell’array, o allocare una dimensione massima (ad esempio 1024 bytes), o ancora mantenere un indice che segnalasse la dimensione corrente dell’array e riallocasse lo spazio (con realloc), secondo il bisogno. C’è più di un modo per gestire efficacemente la memoria in C, l’importante è non trascurare questo aspetto della programmazione.

Il prototipo della funzione standard per allocazione di memoria è void *malloc(size_t size). Per utilizzarla con successo, bisogna ricordare che:

  1. malloc restituisce un puntatore a void che deve essere esplicitamente convertito nel tipo della variabile per cui allocare memoria;
  2. Bisogna allocare la giusta quantità di memoria, altrimenti si va incontro agli errori di cui ho parlato prima.

Adempiere al secondo punto è importante. Di solito si usa la funzione sizeof(...) con, come parametro, il tipo di dato che la variabile allocata conterrà. Non so quanto sia banale, ma è un errore che ho ripetuto spesso, prima di rendermi conto cosa stavo facendo. Se si alloca un puntatore a char, bisogna richiedere spazio per char; se si alloca una variabile di tipo Node, definito come struct _node *, bisogna richiedere spazio per struct _node, e così via. Un errore come quello precedentemente descritto, porta a perdere la metà di una stringa; errori del genere con variabili più complesse e di maggiori dimensioni, porta a errori di paginazione, corruzione dello heap, sovrascrittura della memoria di sistema, vulnerabilità nel programma, cecità, tumori e morte.

In definitiva, ho capito che programmare in C richiede disciplina, forse più degli altri linguaggi. Tutto considerato, posso dire che finora mi è andata bene!

Target=”Strict” Aggiungi un commento

24 maggio 2007, 14:17

Perché due persone su 5 che so di sicuro abbiano letto il post precedente, mi hanno chiesto come mai non ho messo l’attributo “target” ai link delle foto? Sto usando la DTD (Document Type Definition) XHTML 1.0 Strict. L’attributo “target” non esiste in questa DTD. Perché dovrei usarlo? Il concetto di “anchor” ha una semantica ben precisa che è l’apertura di un nuovo documento al posto di quello vecchio. Questo significato è in auge sin dai primi ipertesti, risalenti al 1987. Senza parlare del fatto che in ogni caso, l’apertura di nuove finestre è un fastidio da evitare e comunque dovrebbe essere l’utente a decidere, mai il browser. Se proprio c’è l’assoluta necessità di aprire un documento automaticamente in una nuova finestra, c’è sempre Javascript.

O meglio, la domanda è un’altra, cioè la scarsa conoscenza da parte di molti degli standard. Parlo di conoscenza, non di rispetto, per ora. Più avanti parlerò anche di rispetto.

Gli standard del W3C esistono per un semplice motivo: assicurare la qualità e la continuità delle pagine web. I documenti scritti in un qualsiasi linguaggio derivato dall’SGML (Standard General Markup Language, Linguaggio Generico di Marcatura) devono poter essere sempre leggibili, usabili, allo stesso modo, siano visualizzati in Italia, in Corea o in Alaska, su Mac Os X o su Windows Vista o su Gentoo Linux, utilizzando il motore Gecko (Firefox, Iceweasel), il Trident (Internet Explorer) o il kHTML (Safari, Konqueror), su uno schermo 21” 16:10, su un portatile o su un PDA. Per questo esistono delle linee guida, che non sempre sono rispettate. Anzi, nel malfamato caso di Internet Explorer, spesso gli standard sono infangati, così come dai suoi sostenitori.

Scrivere un documento utilizzando derivazioni proprietarie del linguaggio standard, significa che in futuro quel documento potrebbe non essere più accessibile, per via di cambiamenti nei motori di rendering delle pagine. Inoltre, inserire deliberatamente tag errati per la DTD scelta (che dovrebbe sempre essere Strict, per ragioni di usabilità e accessibilità) significa rendere più difficoltosa la fruizione del validatore, perché per prima cosa si dovranno filtrare gli errori “reali” da quelli inseriti.

In definitiva, perché non scrivere codice pulito e valido? Non è difficile. Basta attenersi alle guide e alle reference che il W3C mette a disposizione, per HTML/XHTML e CSS, di cui ho dato solo due esempi.