Classi e OOP introduzione (parte 1/4)
a cura di Enrico Barillari e Sabrina Cosolo (requisiti: minima esperienza di programmazione)

Premessa
Noi che scriviamo non ci consideriamo dei Guru dell'informatica, ma, appartenendo alla categoria dei «dinosauri» informatici che hanno iniziato a scrivere programmi su un IBM XT o su un Commodore 64, siamo qui da tanto tempo che abbiamo accumulato una certa quantità di dati nei nostri banchi di memoria.
Con questo articolo, vogliamo offrire a voi che vi avvicinate alla Programmazione Orientata agli Oggetti (OOP) una introduzione, scritta in modo semplice e comprensibile, ad alcuni concetti base della OOP, condita con qualche pillola di saggezza zen da «dinosauri», cercando di stimolarvi a iniziare subito ad usare piccoli accorgimenti che derivano dall'esperienza, in modo da produrre programmi ben scritti, facilmente modificabili e facilmente mantenibili da voi stessi e da coloro che lavorano o lavoreranno con voi.
Non pretendiamo di spiegarvi tutto l'OOP in un solo articolo, ma speriamo di far capire a chi non ha ancora avuto modo di affrontarlo che non è una parete di roccia adatta solo all'Uomo Ragno, ma solo una montagna da salire pian piano a passo regolare, fino a raggiungere la cima godendosi il panorama che la circonda.
Questo articolo è solo il cartello indicatore ai piedi del sentiero, ma speriamo possa dare a chi lo legge lo stimolo a provare la salita.

Cosa troverete in queste pagine:
Se arriverete in fondo a questo articolo, avrete forse le idee un po' più chiare sui concetti di astrazione, ereditarietà, incapsulamento, polimorfismo, saprete che cosa è un namespace, che cosa è un assembly, quali sono le regole base per scrivere una classe, quali sono i possibili attributi di visibilità di un oggetto, saprete come implementare una classe base in C#, implementare una classe erede della classe base, saprete come costruire una classe che implementa una collezione di oggetti del tipo della nostra classe erede, avrete le idee un po'più chiare su cos'è un'interfaccia e come si implementa.

Dell'articolo saranno corredo due Progetti esempio, uno minuscolo per cercare di spiegare un concetto difficile come l'incapsulamento, l'altro più complesso per testare le classi che implementeremo. Del codice che sta dentro ai progetti esempio, che usano form e controlli standard visuali come label, bottoni e textbox non parleremo che in modo molto stringato, focalizzandoci su ciò che serve a testare le classi implementate nell'articolo, piuttosto che su come si implementa il codice di gestione dell'evento click di un bottone su una form, ma naturalmente potrete curiosare al suo interno per vedere come gli esempi sono stati realizzati.

Che cos'è una classe:
Le classi sono l'elemento fondamentale su cui è costruita la programmazione orientata agli oggetti (OOP), una classe è il «progetto» per la costruzione di un oggetto, essa ci dice come l'oggetto è fatto, cioè quali sono le sue proprietà (i dati che contiene) e i metodi e gli eventi con cui dall'esterno è possibile interagire con l'oggetto stesso per modificare le sue proprietà, applicarvi algoritmi di elaborazione e leggere i risultati.
L'insieme delle proprietà, dei metodi e degli eventi pubblici di una classe viene definita come la sua interfaccia.

Con un paragone nel campo dell'edilizia, la classe è il progetto disegnato dall'architetto, la casa è l'oggetto costruito seguendo il progetto.

La peculiarità di una classe è quella di essere utilizzabile nella sua forma base oppure di poter essere ereditata e modificata,

allo stesso modo in cui un progetto può essere utilizzato per costruire più edifici uguali oppure può essere elaborato per costruire edifici simili per struttura ma diversi l'uno dall'altro.

Una prima distinzione può quindi essere effettuata tra classi di base, adattabili a molteplici situazioni, con funzionalità essenziali e semplici che possiamo definire dei modelli, e classi ereditate, che partono dalla classe di base per estenderne le capacità specializzandosi per gestire situazioni più specifiche.

Per tornare all'esempio del progetto di una casa, la classe base potrebbe essere il progetto di una parete prefabbricata. Da una parete base può essere derivata una parete con una porta, oppure una finestra che è una parete base con una caratteristica in più.

I concetti base della OOP
Addentriamoci ora un poco nei concetti che stanno alla base della OOP.
Questi concetti sono sintetizzati da quattro parole: Astrazione, Incapsulamento, Ereditarietà, Polimorfismo.
Parole che potrebbero sembrare un po' astruse, ma che cercheremo di rendere comprensibili per quanto ci è possibile.

Astrazione:
Si definisce astrazione il processo che porta alla creazione di modelli (classi) generici, senza entrare dall'inizio nella situazione specifica, a partire dai quali verranno costruiti oggetti via via più specializzati.

Torniamo al progetto della casa: se ci astraiamo dal pensare alle case reali che conosciamo, e cerchiamo una definizione generica della casa, possiamo stabilire che ogni casa è formata da muri che ne delimitano il perimetro ed è coperta da un tetto che ne delimita l'altezza, ha una porta di ingresso ed ha zero o più finestre.
Questa definizione di casa costituisce la classe casa.

Questo è un modello astratto in quanto si applica sia ad una capanna, che alla villetta con quattro stanze e allo stesso modo si applica all'Empire State Building o a Buckingham Palace. Da questa classe casa potranno discendere varie classi eredi:

Ciascuna di esse è una casa con muri che ne delimitano il perimetro, un tetto che ne delimita l'altezza, una porta di ingresso e zero o più finestre. Ma ognuna di esse ha in più una diversa caratteristica (proprietà): il materiale.
Inoltre, dalla classe casa di mattoni potranno derivare la classe villetta, la classe cottage, la classe palazzo, la classe castello, la classe grattacielo; dalla classe casa di legno potranno derivare la classe baracca, la classe baita, la classe chalet.
Ciascuna classe eredita le caratteristiche della classe genitrice e ne specializza l'astrazione in maggiori particolari.

Incapsulamento:
La definizione di incapsulamento, sui tomi di OOP, stabilisce che: la struttura alla base della classe deve essere protetta dall'accesso esterno, in modo tale che lo stato della classe in qualsiasi momento non possa essere modificato se non con l'uso della sua interfaccia.
Cerchiamo di dirlo con parole semplici: incapsulare significa fare in modo che l'interazione del programma che instanzia un oggetto di una classe con l'oggetto stesso sia gestita esclusivamente utilizzando la sua interfaccia pubblica, e non sia possibile dal di fuori di un oggetto modificare il suo stato se non in modo controllato.
In parole ancor più semplici, le variabili dichiarate a livello di classe devono essere possibilmente private, esposte tramite proprietà e manipolabili attraverso metodi.

Proviamo a fare un esempio pratico per scoraggiare i giovani programmatori dall'uso delle variabili pubbliche, «perché devo perdere tempo a scrivere 2 metodi per assegnare un valore anziché lasciarlo pubblico?» è infatti una domanda che tutti vi sarete posta al primo incontro con le property.
Abbiamo costruito una applicazione che abbiamo chiamato Incapsulamento, questa applicazione ha la semplice interfaccia che vi mostriamo qui sotto, una bella form colorata con un bottone, una checkbox e due etichette.
Le tre immagini ci mostrano l'applicazione in tre diversi stati:

Il cuore di questa applicazione è costituito da due classi:
Non Incapsulata Incapsulata
  public class ContaPublic
  {
    public int Contatore; /// <<<---

    public ContaPublic()
    {
      this.Contatore = 0;
    }

    public void Incrementa()
    {
      this.Contatore++;
    }

    public void Azzera()
    {
      this.Contatore = 0;
    }
  }





  public class ContaPrivate
  {
    private int mContatore; /// <<<---

    public ContaPrivate()
    {
      this.mContatore = 0;
    }

    public void Incrementa()
    {
      this.mContatore++;
    }

    public void Azzera()
    {
      this.mContatore = 0;
    }

    public int Contatore
    {
      get
      {
        return( this.mContatore );
      }
    }
  }

La differenza fra le due classi è che nella prima classe la variabile Contatore è pubblica, può quindi essere manipolata direttamente all'esterno della classe ed è modificabile anche utilizzando i metodi Incrementa e Azzera; mentre nella seconda Contatore è una proprietà di sola lettura (get) modificabile solo tramite i due metodi. Perché scrivere più codice è più corretto?
Nella form di interfaccia abbiamo inserito cinque controlli, un timer,un bottone , una checkbox, due label che visualizzano il campo contatore delle due classi che instanziamo. Il bottone fa partire o ferma il timer che ogni secondo incrementa i contatori e li visualizza utilizzando i metodi delle due classi.
La checkbox invece inserisce un «problema fuori contesto» ovvero va a scrivere in modo diretto la variabile pubblica Contatore della classe non incapsulata.
Visualizziamo il codice necessario:

  private void btnInizia_Click(object sender, System.EventArgs e)
  {
    if( !this.mStato )
    {
      this.btnInizia.Text = "Ferma e Azzera";
      this.tmrCambia.Interval = 1000;
      this.tmrCambia.Enabled = true;
      this.mStato = true;
    }
    else
    {
      this.btnInizia.Text = "Inizia";
      this.tmrCambia.Enabled = false;
      this.mStato = false;
      this.mCntPub.Azzera();
      this.mCntPri.Azzera();
    }
  }

Vediamo che il bottone esegue una serie di operazioni di base avviando o fermando il timer che attiva i contatori.

  private void tmrCambia_Tick(object sender, System.EventArgs e)
  {
    this.mCntPub.Incrementa();
    this.mCntPri.Incrementa();
    this.lblContaPub.Text = this.mCntPub.Contatore.ToString("00");
    this.lblContaPri.Text = this.mCntPri.Contatore.ToString("00");
  }

Il timer incrementa i contatori usando il metodo preposto e li visualizza

  private void checkBox1_CheckedChanged(object sender, System.EventArgs e)
  {
    this.mCntPub.Contatore = -100;
  }

La nostra checkbox quando cambia stato introduce una operazione anomala, fa una assegnazione direttamente al valore del contatore non incapsulato e ne modifica il contenuto. Introduce quindi una modifica non controllata allo stato della classe.

Intendiamoci, l'operazione compiuta dalla checkbox non è illegale, questo tipo di gestione dei dati in una classe però introduce possibili cause di anomalia difficilmente controllabili e rintracciabili all'interno di un programma. Infatti, se le sole funzioni che possono modificare un dato all'interno di una classe sono all'interno della classe stessa, in caso di anomalia ho un luogo preciso ove guardare per trovare il problema. In caso di variabile pubblica, devo andare a cercare in tutti i luoghi ove la variabile viene referenziata e scoprire quale è la funzione che agisce in modo anomalo. L'incapsulamento ha perciò scopi pratici, non filosofici.

Ereditarietà:
una classe può essere erede di una altra classe con un più alto livello di astrazione, in questo modo essa ha in sé le funzionalità della classe genitore, e può estenderle ulteriormente o se necessario sostituirle con funzionalità diverse.

Il grattacielo è un palazzo, che è una casa di mattoni, che è una casa. Ricordate?

Nell'ambiente .Net è prevista l'ereditarietà singola, ovvero è permesso ereditare una classe da una sola classe genitore, ma è permesso implementare diverse interfacce: le interfacce sono un particolare tipo di classi astratte, che definiscono metodi e proprietà senza implementarli.

Polimorfismo:
parola che può spaventare anche solo a pronunciarla, fa pensare subito a una brutta bestia viscida e tentacolata: il Polimorfo («brutto polimorfo che non sei altro!» se uno osasse dirmelo potrei offendermi). In realtà, il solito greco, da cui prendono origine tutte le parole complesse, ci spiega in modo letterale cosa vuol dire: Poli=Molte Morphos=Forme quindi polimorfismo significa assumere molte forme. E a noi che ce ne viene da tutto questo? Semplicemente che possiamo inserire in una classe un metodo oppure una proprietà pubblica ripetuto più volte ma con diverse «firme» ad esempio:

  public DateTime BuildDate( int pGg, int pMm, int pAA )
  public DateTime BuildDate( string pDate )
  public DateTime BuildDate( long pDate )
  public DeteTime BuildDate( string pDay, string pMonth, string pYear )

Abbiamo definito una serie di funzioni che ricevono diversi tipi di dati e li trasformano in una data, se le avessimo scritte in C o in VB6 avremmo dovuto dare a ciascuna un nome diverso, invece il polimorfismo ci permette di scrivere una funzione per ogni tipo di dato, ma tutte con lo stesso nome. Al momento della compilazione, sarà il compilatore ad utilizzare quella giusta in base ai dati che gli verranno passati da elaborare. Questo rende molto più semplice la stesura e poi l'uso delle classi e il loro riutilizzo oppure l'implementazione di un'aggiunta ad un metodo esistente magari con un parametro in più.

Un altro passo...
Definiti almeno in modo superficiale i fondamenti di OOP parliamo della stesura di un programma.
A prescindere dal tipo di linguaggio utilizzato, ma a maggior ragione se si tratta di un linguaggio OOP, prima di iniziare a scrivere codice, si deve fare un'accurata analisi del progetto da sviluppare, scriverla su un notes, o su word o su quel che si preferisce usare ed essere il più precisi possibile elencando l'obiettivo del programma e le funzionalità specifiche che si intendono dare allo stesso.
Facciamo un esempio, io voglio scrivere un programma per la gestione di un ricettario:

Il programma Ricettario ha l'obiettivo di memorizzare ricette di cucina;
il ricettario permetterà di inserire le ricette dando loro un titolo, gli ingredienti e le note di preparazione, permetterà di attribuire ad ogni ricetta una tipologia, che le collocherà all'interno di un pranzo o cena (quindi antipasto, primo, secondo, contorno, dessert, frutta) ed una categoria, che ne indicherà il gruppo di appartenenza (pesce, carne, vegetale, dolce);
il ricettario permetterà la modifica e consultazione delle ricette ricercandole per titolo, per tipologia titolo, per categoria titolo, per tipologia categoria titolo, per categoria tipologia titolo;
il ricettario permetterà di associare una immagine ad una ricetta;
il ricettario permetterà di stampare una singola ricetta, tutte le ricette secondo uno degli ordinamenti utilizzabili per la visualizzazione, permetterà di stampare un elenco delle ricette secondo uno degli ordinamenti predisposti.

Una volta descritto il nostro programma, una regola comunemente utilizzata dice che le classi sono i nomi che emergono dall'analisi, le proprietà sono gli aggettivi associati al nome, i metodi sono i verbi che ne descrivono le azioni.

Ma se fosse così facile, saremmo tutti programmatori...

Un'altra buona regola dice che ogni oggetto deve essere il più semplice possibile: meglio iniziare sviluppando oggetti con potenzialità più limitate, ma adatti a gestire situazioni generiche, poi sfruttare l'ereditarietà per estenderli e specializzarli in modo da renderli adatti allo specifico contesto in cui verranno utilizzati.

Casa -> casa di mattoni -> palazzo -> grattacielo...

Parliamo ora di classi all'interno di .NET.
Appurato che una classe è un progetto che definisce come è fatto un oggetto, un programma in OOP è composto da una o più classi.
Un programma scritto per .NET per poter essere eseguito in modo diretto deve avere all'interno di almeno una delle sue classi un metodo che si chiama «Main» questo metodo sarà il primo ad essere chiamato se richiederemo al sistema operativo di eseguire il programma. Vediamone un esempio:

  static void Main()
  {
    Application.Run(new frmMain());
  }

Questo è il Main dell'applicazione che abbiamo creato per la dimostrazione di incapsulamento, esegue una semplice chiamata al metodo Run della classe Application, e instanzia la classe Form che compone la nostra interfaccia utente.
La classe Application è una classe statica così come Main è una funzione statica, le classi statiche sono un particolare tipo di classe di cui discuteremo in seguito, hanno una peculiarità e un uso specifico.
Si tratti di un programma per visualizzare il classico "Hello World" sul video oppure di un programma per il rendering di oggetti tridimensionali il funzionamento è comunque sempre lo stesso.

Alcuni ragguagli tecnici e sintattici
Parliamo un attimo di convenzioni di scrittura:
In ambiente .Net si utilizza comunemente la notazione PascalCase, che prevede l'iniziale maiuscola per le parole semplici e per ogni singola parola che compone un nome composto per i seguenti «oggetti» (in questo caso intesi come parti di codice):
 
       namespace
       classi
       enumerazioni
       metodi
       eventi
       proprietà

Si utilizza invece la notazione camelCase, in cui la prima lettera è sempre minuscola ma l'iniziale dell'eventuale seconda parola che compone il termine composto è maiuscola, per i seguenti «oggetti»:
 
       nomi delle variabili member di una classe
       nomi delle variabili locali
       nomi dei parametri passati ad un metodo

  DrawString(drawString, drawFont, drawBrush, drawPoint);

Il namespace
Prima di iniziare a lavorare sul nostro esempio definiamo un concetto astratto che appartiene a .NET, il namespace, tradotto letteralmente come spazio dei nomi: definizione alquanto nebulosa, che almeno per chi scrive è stata fonte di incomprensioni e conseguenti attacchi di furia verso il povero PC che non aveva colpa per almeno i primi tre mesi da programmatore .NET. In realtà, un namespace è all'interno di un programma o una libreria di classi .NET una semplice etichettina colorata che permette di organizzare le classi secondo una tipologia logica. Un po' come le cartelle del file manager permettono di organizzare i propri progetti o i propri documenti. Utilizzando i namespace, Microsoft ci rende più semplice la ricerca delle classi che ci servono nell'enorme estensione del .NET Framework. Ogni classe infatti è definita all'interno di un namespace.

Ecco che quindi per cercare una Textbox userò:

  System.Windows.Forms.Textbox

Per cercare la funzione per leggere un'immagine:

  System.Drawing.Image

Per cercare il modo di aprire un file:

  System.IO.File

Da questi esempi possiamo comprendere come i namespace siano delle specie di «path» composti da nodi in cascata separati da punti; lo schema di rappresentazione dei namespace è, come sempre per i sistemi di organizzazione logica, un albero:

Così come per le librerie di sistema, le nostre librerie di oggetti potranno essere suddivise con namespace logici e coerenti:

  MyUserInterface

Potrebbe contenere tutto ciò che riguarda gli oggetti della mia interfaccia utente,

  MyUserInterface.Controls

gli User control,

  MyUserInterface.Components

le classi di supporto ai controlli o i componenti base,

  MyUserInterface.Static

le funzioni statiche (ad esempio la validazione o formattazione personalizzata di dati).

Come è fatta una classe:
Una classe è composta da dati e funzioni, i dati sono dei contenitori di valori, le funzioni sono il codice necessario a manipolare i dati.
    I Dati di una classe possono essere dei seguenti tipi:

       Variabili
       Costanti
       Eventi

    Lo stato di un oggetto (istanza di una classe) è definito dai valori contenuti nei suoi dati.
    Le Funzioni di una classe possono essere dei seguenti tipi:
 
       Costruttori
       Metodi
       Proprietà
       Event handler (li indichiamo anche se sono comunque metodi)
       Distruttori

Le funzioni di una classe ne descrivono il funzionamento e quindi il modo in cui essa si comporta in base all'ambiente in cui viene instanziata ed il mezzo con cui rivolgerle domande e ottenere risposte, siano esse valori in essa contenuti o dati elaborati.

Ancora un concetto specifico, l'Assembly
Un ultimo concetto da introdurre prima di iniziare a costruire una classe è il concetto di Assembly (tradotto come unità di distribuzione) un Assembly non è altro che un programma compilato, in grado di funzionare all'interno dell'ambiente .NET; un Assembly può essere un file Exe, oppure una DLL, nel primo caso può essere eseguito in modo diretto, nel secondo caso può essere referenziato ed utilizzato all'interno di un Exe oppure (se si tratta di una DLL ASP.NET) instanziato all'interno di una directory virtuale su server IIS.

Come iniziare il progetto esempio con Visual Studio .NET
Per creare la nostra prima classe possiamo utilizzare Visual Studio .NET o un altro IDE di sviluppo oppure il Notepad, creando un file di testo con estensione VB o CS in base al linguaggio che preferiamo usare (in questo articolo utilizziamo il C#, ma per i più pigri, se saremo in vena di generosità, potremmo tradurre in VB il codice esempio).

Prima di passare alla realizzazione pratica dell'esempio, utilizzando Visual Studio .NET costruiamo il progetto che ci permetterà di scrivere e testare il codice del nostro esempio.
Apriamo Visual Studio, clicchiamo il bottone Nuovo progetto della pagina di apertura dell'IDE, selezioniamo il ramo Progetti Visual C#, selezioniamo Windows Application, chiamiamo la nostra applicazione Persona e collochiamola su una cartella di nostro gusto sul disco del nostro PC.

Diamo l'OK e ci verrà creata la base per il nostro progetto, una applicazione windows con un singolo form funzionante.
Creiamo ora l'applicazione che useremo per effettuare i nostri test. Per prima cosa modifichiamo il progetto e definiamo il Namespace base in cui vogliamo che le nostre classi siano create, quindi facciamo click con il tasto destro del mouse sul progetto Persona e selezioniamo l'opzione Proprietà (Properties)

Selezioniamo la terza riga che conterrà il Namespace di default che automaticamente sarà stato inserito uguale al nome del progetto, perciò sarà Persona e sostituiamolo con MyNamespace.
A questo punto dobbiamo modificare la form1 che ci è stata generata e farla divenire la form base per il nostro progetto di Test

Inseriamo ora il codice per il menu di uscita applicazione ed avremo la nostra applicazione funzionante, per farlo, facciamo doppio click dal Form Designer sul MenuItem mnuExit e scriviamo il seguente codice:

  private void mnuExit_Click(object sender, System.EventArgs e)
  {
    Application.Exit();
  }
Compilando il programma e lanciandolo otterremo un programma con una form con il menu, e cliccando su Exit chiuderemo l'applicazione.
Questo sarà il contenitore per i nostri test.
A questo punto, per preparare il necessario alla stesura della nostra prima classe facciamo click con il tasto destro sul Solution Explorer (se non è aperto c'è il bottone per aprirlo in alto a destra sulla toolbar) sul nome del progetto e selezioniamo l'opzione Add (aggiungi) e poi Add Class (aggiungi classe) per creare la classe Persona; nella finestra di dialogo dove ci verrà proposto il nome class1.cs, lo sostituiamo con Persona.cs e sul Solution Explorer apparirà la nostra nuova classe.

A questo punto siamo pronti per lavorare, come avete notato siamo stati piuttosto stringati, nel descrivere come creare una applicazione e come lavorare con Visual Studio in quanto l'IDE di Visual Studio .NET non è argomento di questo articolo: assumiamo che chi legge, pur essendo un principiante, abbia provato ad usarlo e sia familiare con le sue operazioni di base.
  Successiva