6. Architetture a 3 livelli
6.1. Introduzione
Diamo un'occhiata all'ultima versione dell'applicazione per il calcolo delle imposte:
using System;
namespace Chap3 {
class Program {
static void Main() {
// interactive tax calculator
// the user enters three data points on the keyboard: married nbEnfants salary
// the program then displays Tax payable
...
// creation of a IImpot object
IImpot impot = null;
try {
// creation of a IImpot object
impot = new FileImpot("DataImpotInvalide.txt");
} catch (FileImpotException e) {
// error display
...
// program stop
Environment.Exit(1);
}
// infinite loop
while (true) {
// tax calculation parameters are requested
Console.Write("Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :");
string paramètres = Console.ReadLine().Trim();
...
// parameters are correct - Impot is calculated
Console.WriteLine("Impot=" + impot.calculer(marié == "o", nbEnfants, salaire) + " euros");
// next taxpayer
}//while
}
}
}
La soluzione precedente include il classico :
- recupero dei dati memorizzati in file, database, ecc. righe 12-21
- dialogo con l'utente, righe 26 (input) e 29 (output)
- l'uso di un algoritmo aziendale, riga 29
L'esperienza pratica ha dimostrato che isolare questi diversi processi in classi separate migliora la manutenibilità delle applicazioni. L'architettura di un'applicazione strutturata in questo modo è la seguente:
![]() |
Questa architettura è denominata "architettura a tre livelli". Il termine "a tre livelli" si riferisce normalmente a un'architettura in cui ogni livello si trova su una macchina diversa. Quando i livelli si trovano sulla stessa macchina, l'architettura diventa un'architettura "a tre livelli".
- Il livello [aziendale] contiene le regole aziendali dell'applicazione. Per la nostra applicazione di calcolo delle imposte, si tratta delle regole utilizzate per calcolare l'imposta di un contribuente. Questo livello necessita di dati per funzionare:
- scaglioni fiscali, che cambiano ogni anno
- numero di figli, stato civile e stipendio annuale del contribuente
Nel diagramma sopra, i dati possono provenire da due fonti:
- il livello di accesso ai dati o [dao] (DAO = Data Access Object) per i dati già memorizzati in file o database. Questo potrebbe essere il caso, ad esempio, delle fasce di imposta, come avveniva nella versione precedente dell’applicazione.
- il livello dell'interfaccia utente o [ui] (UI = User Interface) per i dati inseriti dall'utente o visualizzati all'utente. Questo potrebbe essere il caso qui per il numero di figli, lo stato civile e lo stipendio annuo del contribuente
- in generale, il livello [dao] gestisce l'accesso ai dati persistenti (file, database) o ai dati non persistenti (rete, sensori, ecc.).
- Il livello [ui] gestisce le interazioni con l'utente, se presenti.
- I tre livelli sono resi indipendenti attraverso l'uso di interfacce.
Prenderemo l'applicazione [Impots] che abbiamo già studiato più volte e le daremo un'architettura a 3 livelli. Per farlo, studieremo i livelli [ui, metier, dao] uno dopo l'altro, iniziando dal livello [dao], che gestisce i dati persistenti.
Per prima cosa, dobbiamo definire le interfacce dei vari livelli dell'applicazione [Impots].
6.2. Interfacce dell'applicazione [Impots]
Ricordiamo che un'interfaccia definisce un insieme di firme di metodi. Le classi che implementano l'interfaccia danno contenuto a questi metodi.
Torniamo all'architettura a 3 livelli della nostra applicazione:
![]() |
In questo tipo di architettura, spesso è l'utente a prendere l'iniziativa. Egli effettua una richiesta in [1] e riceve una risposta in [8]. Questo è noto come ciclo richiesta-risposta. Prendiamo l'esempio del calcolo delle imposte di un contribuente. Ciò richiederà diversi passaggi:
- il livello [ui] dovrà chiedere all'utente il numero di figli, lo stato civile e lo stipendio annuo. Questa è l'operazione [1] sopra indicata.
- una volta fatto ciò, il livello [ui] chiederà al livello business di calcolare l’imposta. Per farlo, trasmetterà i dati ricevuti dall’utente. Questa è l’operazione [2].
- Il livello [metier] ha bisogno di alcune informazioni per svolgere il proprio lavoro: le fasce di imposta. Richiederà queste informazioni al livello [dao] con il percorso [3, 4, 5, 6]. [3] è la richiesta iniziale e [6] la risposta a questa richiesta.
- Con tutti i dati necessari, il livello [metier] calcola l'imposta.
- Il livello [metier] può ora rispondere alla richiesta effettuata dal livello [ui] in (b). Questo è il percorso [7].
- Il livello [ui] formatterà questi risultati e li presenterà all'utente. Questo è il percorso [8].
- potremmo immaginare che l'utente esegua simulazioni fiscali e voglia salvarle. Per farlo, utilizzerebbe il percorso [1-8].
Questa descrizione mostra che un livello utilizzerà le risorse del livello alla sua destra, ma mai quelle del livello alla sua sinistra. Consideriamo due livelli contigui:
![]() |
Il livello [A] invia richieste al livello [B]. Nei casi più semplici, un livello è implementato da una singola classe. Un'applicazione si evolve nel tempo. Quindi il livello [B] può avere diverse classi di implementazione [B1, B2, ...]. Se il livello [B] è il livello [dao], può avere una prima implementazione [B1] che recupera i dati da un file. Qualche anno dopo, potresti voler inserire i dati in un database. A quel punto creiamo una seconda classe di implementazione [B2]. Se, nell'applicazione iniziale, il livello [A] funzionava direttamente con la classe [B1], siamo costretti a riscrivere parzialmente il codice del livello [A]. Ad esempio, supponiamo che il livello [A] sia stato scritto come segue:
- riga 1: viene creata un'istanza della classe [B1]
- riga 3: vengono richiesti i dati da questa istanza
Se supponiamo che la nuova classe di implementazione [B2] utilizzi metodi con la stessa firma di quelli della classe [B1], dovremo sostituire tutte le [B1] con [B2]. Questo è un caso molto favorevole, e piuttosto improbabile se non avete prestato attenzione a queste firme di metodo. In pratica, le classi [B1] e [B2] spesso non hanno le stesse firme di metodo, quindi gran parte del livello [A] deve essere completamente riscritta.
Questo può essere migliorato creando un'interfaccia tra i livelli [A] e [B]. Ciò significa fissare in un'interfaccia le firme dei metodi presentati dal livello [B] al livello [A]. Il diagramma precedente diventa quindi il seguente:
![]() |
Il livello [A] non si rivolge più direttamente al livello [B], bensì alla sua interfaccia [IB]. Pertanto, nel codice del livello [A], la classe di implementazione [Bi] del livello [B] compare una sola volta, al momento dell'implementazione dell'interfaccia [IB]. Una volta fatto ciò, nel codice viene utilizzata l'interfaccia [IB] e non la sua classe di implementazione. Il codice precedente diventa il seguente:
- riga 1: viene creata un'istanza [ib] che implementa l'interfaccia [IB] istanziando la classe [B1]
- riga 3: vengono richiesti i dati dall'istanza [ib]
Ora, se sostituiamo l'implementazione [B1] del livello [B] con un'implementazione [B2], e entrambe le implementazioni rispettano la stessa interfaccia [IB], allora solo la riga 1 del livello [A] deve essere modificata e nessun'altra. Questo è un grande vantaggio e di per sé giustifica l'uso sistematico delle interfacce tra due livelli.
Possiamo spingerci ancora oltre e rendere il livello [A] totalmente indipendente dal livello [B]. Nel codice sopra riportato, la riga 1 è problematica perché fa riferimento alla classe [B1]. Idealmente, il livello [A] dovrebbe avere un'implementazione dell'interfaccia [IB] senza dover specificare il nome di una classe. Ciò sarebbe coerente con il nostro diagramma sopra riportato. Possiamo vedere che il livello [A] si rivolge all'interfaccia [IB] e non vediamo perché dovrebbe conoscere il nome della classe che implementa questa interfaccia. Questo dettaglio non è di alcuna utilità per il livello [A].
Il framework Spring (http://www.springframework.org) raggiunge questo obiettivo. L'architettura precedente si evolve come segue:
![]() |
Il livello trasversale [Spring] consentirà a un livello di ottenere un riferimento al livello alla sua destra tramite configurazione, senza dover conoscere il nome della classe di implementazione del livello. Questo nome sarà presente nei file di configurazione e non nel codice C#. Il codice C# per il livello [A] assumerà quindi la seguente forma:
- riga 1: un'istanza [ib] che implementa l'interfaccia [IB] del livello [B]. Questa istanza viene creata da Spring sulla base delle informazioni presenti in un file di configurazione. Spring creerà:
- l'istanza [b] che implementa il livello [B]
- l'istanza [a] che implementa il livello [A]. Questa istanza verrà inizializzata. Il campo [ib] sopra riportato verrà impostato sul riferimento [b] dell'oggetto che implementa il livello [B]
- riga 3: i dati vengono richiesti all'istanza [ib]
Ora possiamo vedere che la classe di implementazione [B1] del livello B non compare in nessuna parte del codice del livello [A]. Quando l'implementazione [B1] viene sostituita da una nuova implementazione [B2], nulla cambierà nel codice della classe [A]. Basta semplicemente modificare i file di configurazione di Spring per istanziare [B2] invece di [B1].
La combinazione di Spring e delle interfacce C# apporta un miglioramento decisivo alla manutenzione dell'applicazione rendendo i livelli dell'applicazione a tenuta stagna. Questa è la soluzione che useremo per una nuova versione dell'applicazione [Impots].
Torniamo all'architettura a tre livelli della nostra applicazione:
![]() |
In casi semplici, possiamo partire dal livello [business] per individuare le interfacce dell'applicazione. Per funzionare, ha bisogno di:
- già disponibili in file, database o tramite la rete. Sono fornite dal livello [dao].
- non ancora disponibili. Sono fornite dal livello [ui], che le ottiene dall'utente dell'applicazione.
Quale interfaccia dovrebbe offrire il livello [dao] al livello [metier]? Quali sono le possibili interazioni tra questi due livelli? Il livello [dao] deve fornire al livello [metier] i seguenti dati:
- scaglioni fiscali
Nella nostra applicazione, il livello [dao] utilizza dati esistenti ma non ne crea di nuovi. Una definizione dell'interfaccia per il livello [dao] potrebbe essere la seguente:
using Entites;
namespace Dao {
public interface IImpotDao {
// tax brackets
TrancheImpot[] TranchesImpot{get;}
}
}
- riga 3: il livello [dao] verrà inserito nello spazio dei nomi [Dao]
- riga 6: l'interfaccia IImpotDao definisce la proprietà TranchesImpot che fornirà le fasce di imposta al livello [business].
- riga 1: importa lo spazio dei nomi in cui è definita la struttura TrancheImpot :
namespace Entites {
// a tax bracket
public struct TrancheImpot {
public decimal Limite { get; set; }
public decimal CoeffR { get; set; }
public decimal CoeffN { get; set; }
}
}
Torniamo all'architettura a tre livelli della nostra applicazione:
![]() |
Quale interfaccia dovrebbe presentare il livello [metier] al livello [ui]? Ricordiamo le interazioni tra questi due livelli:
- il livello [ui] chiede all’utente il numero di figli, lo stato civile e lo stipendio annuo. Questa è l’operazione [1] sopra indicata.
- Una volta fatto ciò, il livello [ui] chiederà al livello business di calcolare i posti a sedere. Per farlo, trasmetterà i dati ricevuti dall'utente. Questa è l'operazione [2].
Una definizione dell'interfaccia per il livello [metier] potrebbe essere la seguente:
namespace Metier {
interface IImpotMetier {
int CalculerImpot(bool marié, int nbEnfants, int salaire);
}
}
- riga 1: inseriremo tutto ciò che riguarda il livello [metier] nello spazio dei nomi [Metier].
- riga 2: l'interfaccia IImpotMetier definisce un solo metodo: quello di calcolare l'imposta dovuta da un contribuente in base allo stato civile, al numero di figli e allo stipendio annuo.
Esaminiamo un'implementazione iniziale di questa architettura a livelli.
6.3. Applicazione di esempio - versione 4
6.3.1. Il progetto Visual Studio
Il progetto Visual Studio sarà il seguente:
![]() |
- [1]: la cartella [Entities] contiene oggetti che attraversano i livelli [ui, metier, dao]: la struttura TrancheImpot, l'eccezione FileImpotException.
- [2]: la cartella [Dao] contiene le classi e le interfacce del livello [dao]. Utilizzeremo due implementazioni di IImpotDao: la classe HardwiredImpot discussa nel paragrafo 4.10 e FileImpot discussa nel paragrafo 5.8.
- [3]: la cartella [Metier] contiene classi e interfacce per il livello [metier]
- [4]: la cartella [Ui] contiene le classi del livello [ui]
- [5]: il file [DataImpot.txt] contiene le fasce di imposta utilizzate dall'implementazione FileImpot del livello [dao]. È configurato [6] per essere copiato automaticamente nella cartella di esecuzione del progetto.
6.3.2. Entità dell'applicazione
Torniamo all'architettura a 3 livelli della nostra applicazione:
![]() |
Le chiamiamo "classi trasversali ai livelli". Si tratta in genere di classi e strutture che incapsulano i dati provenienti dal livello [dao]. Queste entità risalgono solitamente fino al livello [ui].
Le entità dell'applicazione sono le seguenti:
La struttura TrancheImpot
namespace Entites {
// a tax bracket
public struct TrancheImpot {
public decimal Limite { get; set; }
public decimal CoeffR { get; set; }
public decimal CoeffN { get; set; }
}
}
L'eccezione FileImportException
using System;
namespace Entites {
public class FileImpotException : Exception {
// error codes
[Flags]
public enum CodeErreurs { Acces = 1, Ligne = 2, Champ1 = 4, Champ2 = 8, Champ3 = 16 };
// error code
public CodeErreurs Code { get; set; }
// manufacturers
public FileImpotException() {
}
public FileImpotException(string message)
: base(message) {
}
public FileImpotException(string message, Exception e)
: base(message, e) {
}
}
}
Nota: l'eccezione FileImportException è utile solo se il livello [dao] è implementato da FileImport.
6.3.3. Il livello [dao]
![]() |
Ricordiamo l'interfaccia del livello [dao]:
using Entites;
namespace Dao {
public interface IImpotDao {
// tax brackets
TrancheImpot[] TranchesImpot{get;}
}
}
Implementeremo questa interfaccia in due modi diversi.
Innanzitutto con HardwiredImpot, descritto nel paragrafo 4.10:
using System;
using Entites;
namespace Dao {
public class HardwiredImpot : IImpotDao {
// data tables required to calculate the
decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };
// ranges
public TrancheImpot[] TranchesImpot { get; private set; }
// manufacturer
public HardwiredImpot() {
// creation of a table of
TranchesImpot = new TrancheImpot[limites.Length];
// filling
for (int i = 0; i < TranchesImpot.Length; i++) {
TranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
}
}
}// class
}// namespace
- riga 5: la classe HardwiredImpot implementa l'interfaccia IImpotDao
- riga 12: implementazione dell'interfaccia TranchesImpot IImpotDao. Questa proprietà è una proprietà automatica. Implementa la proprietà get dell'interfaccia TranchesImpot IImpotDao. Abbiamo anche dichiarato un metodo set che è interno alla classe, in modo che il costruttore delle righe 15-22 possa inizializzare la tabella delle fasce di imposta.
L'interfaccia IImpotDao sarà implementata anche dalla classe FileImpot discussa nel paragrafo 5.8:
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using Entites;
namespace Dao {
class FileImpot : IImpotDao {
// data file
public string FileName { get; set; }
// tax brackets
public TrancheImpot[] TranchesImpot { get; private set; }
// manufacturer
public FileImpot(string fileName) {
// save the file name
FileName = fileName;
// data
List<TrancheImpot> listTranchesImpot = new List<TrancheImpot>();
int numLigne = 1;
// exception
FileImpotException fe = null;
// read the contents of the fileName file, line by line
Regex pattern = new Regex(@"s*:\s*");
// initially no error
FileImpotException.CodeErreurs code = 0;
try {
using (StreamReader input = new StreamReader(FileName)) {
while (!input.EndOfStream && code == 0) {
// current line
string ligne = input.ReadLine().Trim();
// ignore empty lines
if (ligne == "")
continue;
// line broken down into three fields separated by :
string[] champsLigne = pattern.Split(ligne);
// do we have 3 fields?
if (champsLigne.Length != 3) {
code = FileImpotException.CodeErreurs.Ligne;
}
// 3-field conversions
decimal limite = 0, coeffR = 0, coeffN = 0;
if (code == 0) {
if (!Decimal.TryParse(champsLigne[0], out limite))
code = FileImpotException.CodeErreurs.Champ1;
if (!Decimal.TryParse(champsLigne[1], out coeffR))
code |= FileImpotException.CodeErreurs.Champ2;
if (!Decimal.TryParse(champsLigne[2], out coeffN))
code |= FileImpotException.CodeErreurs.Champ3;
;
}
// mistake?
if (code != 0) {
// on note l'erreur
fe = new FileImpotException(String.Format("Ligne n° {0} incorrecte", numLigne)) { Code = code };
} else {
// the new tax bracket is memorized
listTranchesImpot.Add(new TrancheImpot() { Limite = limite, CoeffR = coeffR, CoeffN = coeffN });
// next line
numLigne++;
}
}
}
} catch (Exception e) {
// on note l'erreur
fe = new FileImpotException(String.Format("Erreur lors de la lecture du fichier {0}", FileName), e) { Code = FileImpotException.CodeErreurs.Acces };
}
// error to report?
if (fe != null) {
// on lance l'exception
throw fe;
} else {
// return the listImpot list in the tranchesImpot array
TranchesImpot = listTranchesImpot.ToArray();
}
}
}
}
- questo codice è già stato studiato nel paragrafo 5.8.
- riga 14: il metodo TranchesImpot dell'interfaccia IImpotDao
- riga 76: inizializzazione delle fasce di imposta nel costruttore della classe, dal file passato al costruttore alla riga 17.
6.3.4. Il pannolino [metier]
![]() |
Ricordiamo l'interfaccia di questo livello:
namespace Metier {
public interface IImpotMetier {
int CalculerImpot(bool marié, int nbEnfants, int salaire);
}
}
L'implementazione ImpotMetier di questa interfaccia è la seguente:
using Entites;
using Dao;
namespace Metier {
public class ImpotMetier : IImpotMetier {
// layer [dao]
private IImpotDao Dao { get; set; }
// tax brackets
private TrancheImpot[] tranchesImpot;
// manufacturer
public ImpotMetier(IImpotDao dao) {
// memorization
Dao = dao;
// tax brackets
tranchesImpot = dao.TranchesImpot;
}
// tAX CALCULATION
public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
// calculating the number of shares
decimal nbParts;
if (marié)
nbParts = (decimal)nbEnfants / 2 + 2;
else
nbParts = (decimal)nbEnfants / 2 + 1;
if (nbEnfants >= 3)
nbParts += 0.5M;
// calculation of taxable income & family quota
decimal revenu = 0.72M * salaire;
decimal QF = revenu / nbParts;
// tAX CALCULATION
tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
int i = 0;
while (QF > tranchesImpot[i].Limite)
i++;
// return result
return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
}//calculate
}//class
}
- riga 5: la classe [Metier] implementa l'interfaccia [IImpotMetier].
- righe 14-19: il livello [metier] deve collaborare con il livello [dao]. Deve quindi avere un riferimento all'oggetto che implementa l'interfaccia IImpotDao. Questo è il motivo per cui tale riferimento viene passato come parametro al costruttore.
- riga 16: il riferimento al livello [dao] è memorizzato nel campo privato della riga 8
- riga 18: a partire da questo riferimento, il builder richiede la tabella delle fasce di imposta e memorizza un riferimento nella proprietà privata alla riga 8.
- righe 22-41: implementazione del metodo CalculerImpot dell'interfaccia IImpotMetier. Questa implementazione utilizza la tabella delle fasce di imposta inizializzata dal costruttore.
6.3.5. Il livello [ui]
![]() |
Le classi delle finestre di dialogo utente nelle versioni 2 e 3 erano molto simili. Quella per la versione 2 era la seguente:
using System;
namespace Chap2 {
public class Program {
static void Main() {
...
// creation of
IImpot impot = new HardwiredImpot();
// infinite loop
while (true) {
...
}//while
}
}
}
e versione 3 :
using System;
namespace Chap3 {
public class Program {
static void Main() {
...
// creation of a IImpot object
IImpot impot = null;
try {
// creation of a IImpot object
impot = new FileImpot("DataImpotInvalide.txt");
} catch (FileImpotException e) {
// error display
string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
// program stop
Environment.Exit(1);
}
// infinite loop
while (true) {
...
}//while
}
}
}
L'unica cosa che cambia è il modo in cui si istanzia l'interfaccia IImpot, utilizzata per calcolare l'imposta. Questo oggetto corrisponde qui al nostro livello [aziendale].
Per un'implementazione [dao] con la classe HardwiredImpot, la classe di dialogo è la seguente:
using System;
using Metier;
using Dao;
using Entites;
namespace Ui {
public class Dialogue2 {
static void Main() {
...
// we create the layers [metier and dao]
IImpotMetier metier = new ImpotMetier(new HardwiredImpot());
// infinite loop
while (true) {
...
// the parameters are correct - the
Console.WriteLine("Impot=" + metier.CalculerImpot(marié == "o", nbEnfants, salaire) + " euros");
// next taxpayer
}//while
}
}
}
- riga 12: istanziazione dei livelli [dao] e [metier]. Ricordare che il livello [metier] richiede il livello [dao].
- riga 18: utilizzo del livello [metier] per calcolare l'imposta
Per un'implementazione [dao] con la classe FileImpot, la classe di dialogo è la seguente:
using System;
using Metier;
using Dao;
using Entites;
namespace Ui {
public class Dialogue {
static void Main() {
...
// we create the layers [metier and dao]
IImpotMetier metier = null;
try {
// layer creation [job]
metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
} catch (FileImpotException e) {
// error display
string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
// program stop
Environment.Exit(1);
}
// infinite loop
while (true) {
...
// parameters are correct - Impot is calculated
Console.WriteLine("Impot=" + metier.CalculerImpot(marié == "o", nbEnfants, salaire) + " euros");
// next taxpayer
}//while
}
}
}
- righe 11-21: istanziamento dei livelli [dao] e [metier]. L'istanziamento del livello [dao] può generare un'eccezione, che viene gestita da
- riga 26: utilizzo del livello [metier] per calcolare l'imposta, come nella versione precedente
6.3.6. Conclusione
L'architettura a livelli e l'uso delle interfacce hanno apportato una certa flessibilità alla nostra applicazione. Ciò è particolarmente evidente nel modo in cui il livello [ui] istanzia i livelli [dao] e [business]:
// on crée les couches [metier et dao]
IImpotMetier metier = new ImpotMetier(new HardwiredImpot());
in un caso e :
// we create the layers [metier and dao]
IImpotMetier metier = null;
try {
// layer creation [job]
metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
} catch (FileImpotException e) {
// error display
string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
// program stop
Environment.Exit(1);
}
nell'altro. Ad eccezione della gestione delle eccezioni nel caso 2, l'istanziazione dei livelli [dao] e [metier] è simile in entrambe le applicazioni. Una volta istanziati i livelli [dao] e [metier], il codice per il livello [ui] è identico in entrambi i casi. Ciò è dovuto al fatto che il livello [metier] viene manipolato tramite la sua interfaccia IImpotMetier e non tramite la sua classe di implementazione. Modificare il livello [metier] o il livello [dao] dell'applicazione senza modificare le loro interfacce comporterà sempre la modifica solo delle righe precedenti nel livello [ui].
Un altro esempio della flessibilità offerta da questa architettura è l'implementazione del livello [business]:
using Entites;
using Dao;
namespace Metier {
public class ImpotMetier : IImpotMetier {
// layer [dao]
private IImpotDao Dao { get; set; }
// tax brackets
private TrancheImpot[] tranchesImpot;
// manufacturer
public ImpotMetier(IImpotDao dao) {
// memorization
Dao = dao;
// tax brackets
tranchesImpot = dao.TranchesImpot;
}
// tAX CALCULATION
public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
...
}//calculate
}//class
}
La riga 14 mostra che il livello [business] è costruito a partire da un riferimento all'interfaccia del livello [dao]. Modificare l'implementazione di quest'ultimo non ha quindi alcun impatto sul livello [business]. Questo è il motivo per cui la nostra singola implementazione del livello [business] è stata in grado di funzionare senza modifiche con due diverse implementazioni del livello [dao].
6.4. Esempio di applicazione - versione 5
![]() |
Questa nuova versione si basa su quella precedente e include le seguenti modifiche:
- i livelli [business] e [dao] sono incapsulati ciascuno in una DLL e testati con il framework di test unitari NUnit.
- L'integrazione dei livelli è garantita dal framework Spring
Nei progetti di grandi dimensioni, diversi sviluppatori lavorano allo stesso progetto. Le architetture a livelli facilitano questo modo di lavorare: poiché i livelli comunicano tra loro tramite interfacce ben definite, uno sviluppatore che lavora su un livello non deve preoccuparsi del lavoro degli altri sviluppatori sugli altri livelli. Tutto ciò che serve è che tutti rispettino le interfacce.
Nell'esempio sopra riportato, lo sviluppatore del livello [business] avrà bisogno di un'implementazione del livello [dao] per testare il proprio livello. Finché questa non sarà completata, potrà utilizzare un'implementazione fittizia del livello [dao], purché sia conforme all'interfaccia [dao] IImpotDao. Questo è un altro vantaggio dell'architettura a livelli: un ritardo nel livello [dao] non impedisce il test del livello [business]. L'implementazione fittizia del livello [dao] ha anche il vantaggio di essere spesso più facile da implementare rispetto al vero livello [dao], che potrebbe richiedere l'avvio di un SGBD, connessioni di rete, ...
Una volta completato e testato, il livello [dao] verrà consegnato agli sviluppatori del livello [business] sotto forma di DLL anziché di codice sorgente. Alla fine, l'applicazione viene spesso distribuita sotto forma di file eseguibile .exe (per il livello [ui]) e librerie di classi .dll (per gli altri livelli).
6.4.1. NUnit
Fino ad ora, i test effettuati per le nostre varie applicazioni si basavano sulla verifica visiva. Verificavamo che sullo schermo apparisse ciò che ci aspettavamo. Questo metodo è inutilizzabile quando ci sono molti test da effettuare. Gli esseri umani sono soggetti alla stanchezza e la loro capacità di verificare i test si affievolisce con il passare delle ore. I test devono quindi essere automatizzati e mirare a zero intervento umano.
Un'applicazione si evolve nel tempo. Ogni volta che si evolve, dobbiamo verificare che l'applicazione non subisca una "regressione", ovvero che continui a superare i test funzionali che sono stati eseguiti quando è stata scritta per la prima volta. Questi test sono chiamati test di "non regressione". Un'applicazione di grandi dimensioni può richiedere centinaia di test. Viene testato ogni metodo di ogni classe dell'applicazione. Questi sono chiamati test unitari. Questi possono coinvolgere molti sviluppatori se non sono stati automatizzati.
Sono stati sviluppati strumenti per automatizzare i test. Uno di questi si chiama NUnit. È disponibile su [http://www.nunit.org] :
![]() | ![]() |
Per questo documento è stata utilizzata la versione 2.4.6 o successive (marzo 2008). L'installazione crea un'icona [1] sul desktop:
![]() |
Un doppio clic sull'icona [1] avvia l'interfaccia grafica di NUnit [2]. Ciò non contribuisce in alcun modo all'automazione dei test, poiché ci si riduce ancora una volta alla verifica visiva: il tester controlla i risultati dei test visualizzati nell'interfaccia grafica. Tuttavia, i test possono anche essere eseguiti tramite strumenti batch e i relativi risultati salvati in file XML. Questo è il metodo utilizzato dai team di sviluppo: i test vengono eseguiti durante la notte e gli sviluppatori dispongono dei risultati la mattina seguente.
Diamo un'occhiata a un esempio di test NUnit. Per prima cosa, creiamo un nuovo progetto C# di tipo Application Console :
![]() |
In [1] vediamo i riferimenti del progetto. Questi riferimenti sono DLL contenenti classi e interfacce utilizzate dal progetto. Quelle presentate in [1] sono incluse di default in ogni nuovo progetto C#. Per poter utilizzare le classi e le interfacce del framework NUnit, dobbiamo aggiungere [2] un nuovo riferimento al progetto.
![]() |
Nella scheda .NET in alto, selezioniamo il componente [nunit.framework]. I componenti [nunit.*] sopra indicati non sono componenti presenti di default nell'ambiente .NET. Sono stati inseriti lì dalla precedente installazione del framework NUnit. Una volta aggiunto il riferimento, questo appare [4] nell'elenco dei riferimenti del progetto.
Prima di generare l'applicazione, la cartella [bin/Release] del progetto è vuota. Dopo la generazione (F6), la cartella [bin/Release] non è più vuota:
![]() |
In [6], vediamo la presenza della DLL [nunit.framework.dll]. È stata l'aggiunta del riferimento [nunit.framework] a far sì che questa DLL venisse copiata nella cartella di esecuzione. Questa è infatti una delle cartelle che verrà esplorata dal CLR (Common Language Runtime) .NET per trovare le classi e le interfacce a cui fa riferimento il progetto.
Creiamo una prima classe di test NUnit. Per farlo, eliminiamo la classe predefinita [Program.cs] e aggiungiamo una nuova classe [Nunit1.cs] al progetto. Eliminiamo anche i riferimenti non necessari [7].
La classe di test NUnit1 sarà la seguente:
using System;
using NUnit.Framework;
namespace NUnit {
[TestFixture]
public class NUnit1 {
public NUnit1() {
Console.WriteLine("constructeur");
}
[SetUp]
public void avant() {
Console.WriteLine("Setup");
}
[TearDown]
public void après() {
Console.WriteLine("TearDown");
}
[Test]
public void t1() {
Console.WriteLine("test1");
Assert.AreEqual(1, 1);
}
[Test]
public void t2() {
Console.WriteLine("test2");
Assert.AreEqual(1, 2, "1 n'est pas égal à 2");
}
}
}
- riga 6: la classe NUnit1 deve essere pubblica. La parola chiave public non viene generata da Visual Studio per impostazione predefinita. Deve essere aggiunta.
- riga 5: [TestFixture] è un attributo NUnit. Indica che la classe è una classe di test.
- righe 7-9: il costruttore. Qui viene utilizzato solo per scrivere un messaggio sullo schermo. Vogliamo vedere quando viene eseguito.
- riga 10: [SetUp] definisce un metodo eseguito prima di ogni test unitario.
- riga 14: [TearDown] definisce un metodo da eseguire dopo ogni test unitario.
- riga 18: l'attributo [Test] definisce un metodo di test. Per ogni metodo annotato con [Test], il [SetUp] annotato verrà eseguito prima del test e il [TearDown] verrà eseguito dopo il test.
- riga 21: uno degli [Assert.*] definiti dal framework NUnit. Sono disponibili i seguenti metodi [Assert]:
- [Assert.AreEqual(espressione1, espressione2)] : verifica che i valori delle due espressioni siano uguali. Sono supportati molti tipi di espressioni (int, string, float, double, decimal, ...). Se le due espressioni non sono uguali, viene generata un'eccezione.
- [Assert.AreEqual(real1, real2, delta)] : verifica che due numeri reali siano vicini a delta, ovvero abs(real1-real2)<=delta. Ad esempio, possiamo scrivere [Assert.AreEqual(real1, real2, 1E-6)] per verificare che due valori siano vicini a 10-6.
- [Assert.AreEqual(expression1, expression2, message)] e [Assert.AreEqual(real1, real2, delta, message)] sono varianti utilizzate per specificare il messaggio di errore da associare all'eccezione generata quando [Assert.AreEqual] fallisce.
- [Assert.IsNotNull(object)] e [Assert.IsNotNull(object, message)] : verifica che object non sia uguale a null.
- [Assert.IsNull(object)] e [Assert.IsNull(object, message)] : verifica che l'oggetto sia uguale a null.
- [Assert.IsTrue(espressione)] e [Assert.IsTrue(espressione, messaggio)]: verifica che l'espressione sia vera.
- [Assert.IsFalse(espressione)] e [Assert.IsFalse(espressione, messaggio)] : verifica che l'espressione sia falsa.
- [Assert.AreSame(object1, object2)] e [Assert.AreSame(object1, object2, message)] : verifica che i riferimenti object1 e object2 si riferiscano allo stesso oggetto.
- [Assert.AreNotSame(object1, object2)] e [Assert.AreNotSame(object1, object2, message)] : verifica che i riferimenti object1 e object2 non designino lo stesso oggetto.
- riga 21: l'asserzione deve avere esito positivo
- riga 26: l'asserzione deve fallire
Configuriamo il progetto in modo che la sua generazione produca una DLL anziché un eseguibile .exe:
![]() |
- in [1]: proprietà del progetto
- in [2, 3]: selezionare [Libreria di classi] come tipo di progetto
- in [4]: la generazione del progetto produrrà una DLL (assembly) denominata [Nunit.dll]
Ora utilizziamo NUnit per eseguire la classe di test:
![]() |
- in [1]: aprire un progetto NUnit
- in [2, 3]: caricare la DLL bin/Release/Nunit.dll generata dal progetto C#
- in [4]: la DLL è stata caricata
- in [5]: l'albero dei test
- in [6]: vengono eseguiti
![]() |
- in [7]: risultati: t1 ha avuto esito positivo, t2 ha fallito
- in [8]: una barra rossa indica il fallimento complessivo della classe di test
- in [9]: il messaggio di errore del test fallito
![]() |
- in [11]: le diverse schede nella finestra dei risultati
- in [12]: la scheda [Console.Out]. Qui vediamo che:
- il builder è stato eseguito una sola volta
- il metodo [SetUp] è stato eseguito prima di ciascuno dei due test
- il metodo [TearDown] è stato eseguito dopo ciascuno dei due test
È possibile specificare i metodi da testare:
![]() |
- in [1]: viene visualizzata una casella di controllo accanto a ciascun test
- in [2]: selezionare i test da eseguire
- in [3]: vengono eseguiti
Per correggere gli errori, è sufficiente correggere il progetto C# e rigenerarlo. NUnit rileva che la DLL che sta testando è stata modificata e carica automaticamente quella nuova. Tutto ciò che devi fare è eseguire nuovamente i test.
Si consideri la seguente nuova classe di test:
using System;
using NUnit.Framework;
namespace NUnit {
[TestFixture]
public class NUnit2 : AssertionHelper {
public NUnit2() {
Console.WriteLine("constructeur");
}
[SetUp]
public void avant() {
Console.WriteLine("Setup");
}
[TearDown]
public void après() {
Console.WriteLine("TearDown");
}
[Test]
public void t1() {
Console.WriteLine("test1");
Expect(1, EqualTo(1));
}
[Test]
public void t2() {
Console.WriteLine("test2");
Expect(1, EqualTo(2), "1 n'est pas égal à 2");
}
}
}
A partire dalla versione 2.4 di NUnit, è disponibile una nuova sintassi, quella delle righe 21 e 26. Per questo, la classe di test deve derivare da AssertionHelper (riga 6).
La corrispondenza (non esaustiva) tra la vecchia e la nuova sintassi è la seguente:
Aggiungiamo il seguente test alla classe NUnit2:
[Test]
public void t3() {
bool vrai = true, faux = false;
Expect(vrai, True);
Expect(faux, False);
Object obj1 = new Object(), obj2 = null, obj3=obj1;
Expect(obj1, Not.Null);
Expect(obj2, Null);
Expect(obj3, SameAs(obj1));
double d1 = 4.1, d2 = 6.4, d3 = d1;
Expect(d1, EqualTo(d3).Within(1e-6));
Expect(d1, Not.EqualTo(d2));
}
Se generiamo (F6) la nuova DLL del progetto C#, il progetto NUnit diventa il seguente:
![]() |
- in [1]: la nuova classe di test [NUnit2] è stata rilevata automaticamente
- in [2]: esegui il test t3 di NUnit2
- in [3]: il test t3 è stato superato
Per saperne di più su NUnit, leggi la guida di NUnit :
![]() | ![]() |
6.4.2. La soluzione Visual Studio
![]() |
Realizzeremo gradualmente la seguente soluzione Visual Studio:
![]() |
- in [1]: la soluzione ImpotsV5 è composta da tre progetti, uno per ciascuno dei tre livelli dell'applicazione
- in [2]: il progetto [dao] del livello [dao]
- in [3]: il progetto [metier] per il livello [metier]
- in [4]: progetto [ui] del livello [ui]
La soluzione ImpotsV5 può essere costruita come segue:
1 ![]() | 234 ![]() | 5 ![]() |
- it [1]: crea un nuovo progetto
- en [2]: selezionare un'applicazione console
- in [3]: chiamare il progetto [dao]
- in [4]: creare il progetto
- in [5]: una volta creato il progetto, salvarlo
![]() |
- in [6]: mantenere il nome [dao] per il progetto
- in [7]: specificare una cartella in cui salvare il progetto e la relativa soluzione
- in [8]: assegnare un nome alla soluzione
- in [9]: indicare che la soluzione deve avere un proprio file
- in [10]: salvare il progetto e la sua soluzione
- in [11]: il progetto [dao] nella sua soluzione ImpotsV5
![]() |
- in [12]: il file della soluzione ImpotsV5. Contiene la cartella [dao] dalla cartella [dao].
- in [13]: il contenuto della cartella [dao]
- in [14]: un nuovo progetto viene aggiunto alla soluzione ImpotsV5
![]() |
- in [15]: il nuovo progetto si chiama [metier]
- in [16]: la soluzione con i suoi due progetti
- in [17]: la soluzione, una volta aggiunto il terzo progetto [ui]
![]() |
- in [18]: il file della soluzione e i file dei tre progetti
- quando una soluzione viene eseguita utilizzando (Ctrl+F5), è il progetto attivo che viene eseguito. Lo stesso vale quando si genera (F6) la soluzione. Il nome del progetto attivo è in grassetto [19] nella soluzione.
- in [20]: per cambiare il progetto attivo della soluzione
- in [21]: il progetto [metier] è ora il progetto attivo nella soluzione
6.4.3. Il [layer dao]
![]() |
![]() |
Riferimenti del progetto (vedi [1] nel progetto)
Aggiungiamo il riferimento [nunit.framework] necessario per i test [NUnit]
Le entità (vedi [2] nel progetto)
La classe [TrancheImport] è la stessa delle versioni precedenti. La classe [FileImportException] della versione precedente è stata rinominata [ImportException] per renderla più generica e non collegarla a un particolare livello [dao]:
using System;
namespace Entites {
public class ImpotException : Exception {
// error code
public int Code { get; set; }
// manufacturers
public ImpotException() {
}
public ImpotException(string message)
: base(message) {
}
public ImpotException(string message, Exception e)
: base(message, e) {
}
}
}
Il livello [dao] (vedi [3] nel progetto)
L'interfaccia [IImpotDao] è la stessa della versione precedente. Lo stesso vale per la classe [HardwiredImpot]. La classe [FileImpot] è stata modificata per tenere conto del cambiamento dell'eccezione [FileImpotException] in [ImpotException]:
...
namespace Dao {
public class FileImpot : IImpotDao {
// error codes
[Flags]
public enum CodeErreurs { Acces = 1, Ligne = 2, Champ1 = 4, Champ2 = 8, Champ3 = 16 };
...
// manufacturer
public FileImpot(string fileName) {
// save the file name
FileName = fileName;
...
// initially no error
CodeErreurs code = 0;
try {
using (StreamReader input = new StreamReader(FileName)) {
while (!input.EndOfStream && code == 0) {
...
// mistake?
if (code != 0) {
// on note l'erreur
fe = new ImpotException(String.Format("Ligne n° {0} incorrecte", numLigne)) { Code = (int)code };
} else {
...
}
}
}
} catch (Exception e) {
// on note l'erreur
fe = new ImpotException(String.Format("Erreur lors de la lecture du fichier {0}", FileName), e) { Code = (int)CodeErreurs.Acces };
}
// error to report?
...
}
}
}
- riga 8: i codici di errore precedentemente presenti nella classe [FileImpotException] sono stati spostati nella classe [FileImpot]. Si tratta di codici di errore specifici per questa implementazione dell'interfaccia [IImpotDao].
- righe 26 e 34: per incapsulare un errore, viene utilizzata la classe [ImpotException] invece della classe [FileImpotException].
Il test [Test1] (vedere [4] nel progetto)
La classe [Test1] visualizza semplicemente le fasce di imposta sullo schermo:
using System;
using Dao;
using Entites;
namespace Tests {
class Test1 {
static void Main() {
// create the [dao] layer
IImpotDao dao = null;
try {
// layer creation [dao]
dao = new FileImpot("DataImpot.txt");
} catch (ImpotException e) {
// error display
string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
// program stop
Environment.Exit(1);
}
// display tax brackets
TrancheImpot[] tranchesImpot = dao.TranchesImpot;
foreach (TrancheImpot t in tranchesImpot) {
Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
}
}
}
}
- riga 13: il livello [dao] è implementato dalla classe [FileImpot]
- riga 14: gestisce l'eccezione [ImpotException] che potrebbe verificarsi.
Il file [DataImpot.txt] necessario per il test viene copiato automaticamente nella cartella di esecuzione del progetto (vedere [5] nel progetto). Il progetto [dao] avrà diverse classi contenenti un metodo [Main]. In questo caso, è necessario indicare esplicitamente la classe da eseguire quando l'utente richiede l'esecuzione del progetto premendo Ctrl-F5 :
![]() |
- en [1]: accedere alle proprietà del progetto
- en [2]: specificare che si tratta di un'applicazione console
- in [3]: specificare la classe da eseguire
L'esecuzione della precedente classe [Test1] fornisce i seguenti risultati:
4962:0:0
8382:0,068:291,09
14753:0,191:1322,92
23888:0,283:2668,39
38868:0,374:4846,98
47932:0,426:6883,66
0:0,481:9505,54
Il test [Test2] (vedi [4] nel progetto)
La classe [Test2] fa la stessa cosa della classe [Test1], implementando il livello [dao] con la classe [HardwiredImpot]. La riga 13 di [Test1] viene sostituita dal seguente:
dao = new HardwiredImpot();
Il progetto viene modificato per eseguire la classe [Test2]:
![]() |
I risultati sullo schermo sono gli stessi di prima.
Il test NUnit [NUnit1] (vedere [4] nel progetto)
Il test unitario [NUnit1] è il seguente:
using System;
using Dao;
using Entites;
using NUnit.Framework;
namespace Tests {
[TestFixture]
public class NUnit1 : AssertionHelper{
// layer [dao] to be tested
private IImpotDao dao;
// manufacturer
public NUnit1() {
// dao] layer initialization
dao = new FileImpot("DataImpot.txt");
}
// test
[Test]
public void ShowTranchesImpot(){
// display tax brackets
TrancheImpot[] tranchesImpot = dao.TranchesImpot;
foreach (TrancheImpot t in tranchesImpot) {
Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
}
// some tests
Expect(tranchesImpot.Length,EqualTo(7));
Expect(tranchesImpot[2].Limite,EqualTo(14753));
Expect(tranchesImpot[2].CoeffR, EqualTo(0.191));
Expect(tranchesImpot[2].CoeffN, EqualTo(1322.92));
}
}
}
- la classe di test deriva dalla classe [AssertionHelper], consentendo l'uso del metodo statico Expect (righe 27-30).
- riga 10: un riferimento al livello [dao]
- righe 13-16: il costruttore istanzia il livello [dao] con la classe [FileImport]
- righe 19-20: il metodo di test
- riga 22: recupera la tabella delle fasce di imposta dal livello [dao]
- righe 23-25: visualizzate come in precedenza. Questa visualizzazione non sarebbe necessaria in un vero test unitario. Qui, questa visualizzazione ha uno scopo didattico.
- riga 27: verificare che ci siano 7 scaglioni fiscali
- righe 28-30: controllare i valori relativi alla fascia di imposta n. 2
Per eseguire questo test unitario, il progetto deve essere di tipo [Class Library] :
![]() |
- in [1]: la natura del progetto è stata modificata
- in [2]: la DLL generata si chiamerà [ ImpotsV5-dao.dll]
- in [3]: dopo aver generato (F6) il progetto, la cartella [dao/bin/Release] contiene la DLL [ImpotsV5-dao.dll]
La DLL [ImpotsV5-dao.dll] viene quindi caricata nel framework NUnit ed eseguita:
![]() |
- in [1]: test superati. Consideriamo ora operativo il livello [dao]. La sua DLL contiene tutte le classi del progetto, comprese le classi di test. Queste non sono più necessarie. Ricostruiamo la DLL per escludere le classi di test.
- in [2]: la cartella [tests] viene esclusa dal progetto
- in [3]: il nuovo progetto. Questo viene rigenerato premendo F6 per generare una nuova DLL.
6.4.4. Il [lavoro " " del livello]
![]() |
![]() |
- in [1], il progetto [metier] è diventato il progetto attivo della soluzione
- in [2]: riferimenti al progetto
- en [3]: il livello [metier]
- in [4]: classi di test
- in [5]: il file delle fasce di imposta [DataImpot.txt] configurato [6] per essere copiato automaticamente nella cartella di esecuzione del progetto [7]
Riferimenti del progetto (vedi [2] nel progetto)
Come per il progetto [dao], aggiungiamo il riferimento [nunit.framework] necessario per i test [NUnit]. Il livello [metier] necessita del livello [dao]. Ha quindi bisogno di un riferimento alla DLL di questo livello. Procedere come segue:
![]() |
- in [1]: viene aggiunto un nuovo riferimento ai riferimenti del progetto [metier]
- in [2]: selezionare la scheda [Sfoglia]
- in [3]: selezionare la cartella [dao/bin/Release]
- in [4]: selezionare la DLL [ImpotsV5-dao.dll] generata nel progetto [dao]
- in [5]: il nuovo riferimento
Il pannolino [metier] (vedi [3] nel progetto)
L'interfaccia [IImpotMetier] è la stessa della versione precedente. Lo stesso vale per la classe [ImpotMetier].
Il test [Test1] (vedi [4] nel progetto)
La classe [Test1] esegue semplicemente alcuni calcoli relativi allo stipendio:
using System;
using Dao;
using Entites;
using Metier;
namespace Tests {
class Test1 {
static void Main() {
// we create the [metier] layer
IImpotMetier metier = null;
try {
// layer creation [job]
metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
} catch (ImpotException e) {
// error display
string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
// program stop
Environment.Exit(1);
}
// on calcule qqs impots
Console.WriteLine(String.Format("Impot(true,2,60000)={0} euros", metier.CalculerImpot(true, 2, 60000)));
Console.WriteLine(String.Format("Impot(false,3,60000)={0} euros", metier.CalculerImpot(false, 3, 60000)));
Console.WriteLine(String.Format("Impot(false,3,60000)={0} euros", metier.CalculerImpot(false, 3, 6000)));
Console.WriteLine(String.Format("Impot(false,3,60000)={0} euros", metier.CalculerImpot(false, 3, 600000)));
}
}
}
- riga 14: creazione dei livelli [metier] e [dao]. Il livello [dao] è implementato con la classe [FileImpot]
- righe 12-21: gestione di una possibile eccezione [ImpotException]
- righe 23-26: chiamate ripetute al metodo unico dell'interfaccia [IImpotMetier] di CalculerImpot.
Il progetto [metier] è configurato come segue:
![]() |
- [1]: il progetto è un'applicazione console
- [2]: la classe eseguita è [Test1]
- [3]: la generazione del progetto produrrà l'eseguibile [ImpotsV5-metier.exe]
I risultati del progetto sono i seguenti:
Il test [NUnit1] (vedi [4] nel progetto)
La classe di test unitario [NUnit1] ripete i quattro calcoli precedenti e verifica i risultati:
using Dao;
using Metier;
using NUnit.Framework;
namespace Tests {
[TestFixture]
public class NUnit1:AssertionHelper {
// layer [metier] to test
private IImpotMetier metier;
// manufacturer
public NUnit1() {
// initialization layer [metier]
metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
}
// test
[Test]
public void CalculsImpot(){
// display tax brackets
Expect(metier.CalculerImpot(true, 2, 60000), EqualTo(4282));
Expect(metier.CalculerImpot(false, 3, 60000), EqualTo(4282));
Expect(metier.CalculerImpot(false, 3, 6000), EqualTo(0));
Expect(metier.CalculerImpot(false, 3, 600000), EqualTo(179275));
}
}
}
- riga 14: creazione dei livelli [metier] e [dao]. Il livello [dao] è implementato con la classe [FileImpot]
- righe 21-24: chiamate ripetute al singolo metodo dell'interfaccia [IImpotMetier] di CalculerImpot con verifica dei risultati.
Il progetto [metier] è ora configurato come segue:
![]() |
- [1]: il progetto è di tipo "libreria di classi"
- [2]: la generazione del progetto produrrà una DLL [ImpotsV5-metier.dll]
Il progetto viene generato (F6). Quindi la DLL [ ImpotsV5-metier.dll generata viene caricata in NUnit e testata:
![]() |
I test sopra riportati hanno avuto esito positivo. Consideriamo ora operativo il livello [metier]. La sua DLL contiene tutte le classi del progetto, comprese le classi di test. Queste ultime non sono più necessarie. Ricompiliamo la DLL per escludere le classi di test.
![]() |
- in [1]: la cartella [tests] è esclusa dal progetto
- in [2]: il nuovo progetto. Questo viene rigenerato premendo F6 per generare una nuova DLL.
6.4.5. Il livello [ui]
![]() |
![]() |
- in [1], il progetto [ui] è diventato il progetto attivo per la soluzione
- in [2]: riferimenti al progetto
- in [3]: il livello [ui]
- in [4]: il file delle fasce di imposta [DataImport.txt], configurato [5] per essere copiato automaticamente nella cartella di esecuzione del progetto [6]
Riferimenti al progetto (vedi [2] nel progetto)
Il livello [ui] necessita dei livelli [metier] e [dao] per eseguire i propri calcoli fiscali. Ha quindi bisogno di un riferimento alla DLL di questi due livelli. Procedere come indicato per il livello [metier]
La classe principale [Dialogue.cs] (vedere [3] nel progetto)
La classe [Dialogue.cs] è la stessa della versione precedente.
Test
Il progetto [ui] è configurato come segue:
![]() |
- [1]: il progetto è di tipo "console applicativa"
- [2]: la generazione del progetto produrrà l'eseguibile [ImpotsV5-ui.exe]
- [3]: la classe da eseguire
Di seguito è riportato un esempio di esecuzione (Ctrl+F5):
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 60000
Impot=4282 euros
6.4.6. Il [livello Spring]
Torniamo al codice in [Dialogue.cs] che crea i livelli [dao] e [metier]:
// on crée les couches [metier et dao]
IImpotMetier metier = null;
try {
// création couche [metier]
metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
} catch (ImpotException e) {
// affichage erreur
...
// arrêt programme
Environment.Exit(1);
}
La riga 5 crea i livelli [dao] e [metier], specificando esplicitamente i nomi delle classi di implementazione per entrambi i livelli: FileImpot per il livello [dao], ImpotMetier per il livello [metier]. Se uno dei livelli viene implementato con una nuova classe, la riga 5 verrà modificata. Ad esempio:
metier = new ImpotMetier(new HardwiredImpot());
A parte questa modifica, nulla cambierà nell'applicazione, poiché ogni livello comunica con quello successivo tramite un'interfaccia. Finché l'interfaccia rimane invariata, la comunicazione tra i livelli rimane invariata. Il framework Spring ci permette di portare l'indipendenza dei livelli un passo avanti, esternalizzando i nomi delle classi che implementano i vari livelli in un file di configurazione. Modificare l'implementazione di un livello equivale a modificare un file di configurazione. Non vi è alcun impatto sul codice dell'applicazione.
![]() |
In precedenza, il livello [ui] chiederà a Spring [0] di istanziare i livelli [dao] [1] e [metier] [2] in base alle informazioni contenute in un file di configurazione. Il livello [ui] chiederà quindi a Spring [3] un riferimento al livello [metier]:
// we create the layers [metier and dao]
IImpotMetier metier = null;
try {
// spring context
IApplicationContext ctx = ContextRegistry.GetContext();
// a reference is requested on the [metier] layer
metier = (IImpotMetier)ctx.GetObject("metier");
} catch (Exception e1) {
...
}
- riga 5: istanziazione dei livelli [dao] e [metier] da parte di Spring
- riga 7: viene recuperato un riferimento al livello [metier]. Si noti che il livello [ui] ha ottenuto questo riferimento senza specificare il nome della classe che implementa il livello [metier].
Il framework Spring è disponibile in due versioni: Java e .NET. La versione .NET è disponibile all'indirizzo (marzo 2008) [http://www.springframework.net/]:
![]() |
- in [1]: il sito [Spring.net]
- in [2]: pagina dei download
![]() |
- in [3]: scarica Spring 1.1 (marzo 2008)
![]() |
- in [4]: scaricare e installare la versione .exe
- in [5]: la cartella generata dall'installazione
- in [6]: la cartella [bin/net/2.0/release] contiene le DLL di Spring per progetti Visual Studio .NET 2.0 o superiori. Spring è un framework ricco. L'aspetto di Spring che useremo qui per gestire l'integrazione dei livelli in un'applicazione si chiama IoC: Inversion of Control o DI: Dependence Injection. Spring fornisce librerie per l'accesso al database con NHibernate, la generazione e la gestione di servizi web, applicazioni web, ...
- le DLL necessarie per gestire l'integrazione dei livelli in un'applicazione sono le DLL [7] e [8].
Mettiamo queste tre DLL in una cartella [lib] nel nostro progetto:
![]() |
- [1]: le tre DLL vengono inserite nella cartella [lib] tramite Esplora risorse
- [2]: nel progetto [ui], visualizza tutti i file
- [3]: la cartella [ui/lib] è ora visibile. La includiamo nel
- [4]: la cartella [ui/lib] fa parte del progetto
L'operazione di creazione della cartella [lib] non è affatto essenziale. I riferimenti potrebbero essere creati direttamente sulle tre DLL nella cartella [bin/net/2.0/release] di [Spring.net]. Tuttavia, creando la cartella [lib], l'applicazione può essere sviluppata su una workstation senza [Spring.net], rendendola meno dipendente dall'ambiente di sviluppo disponibile.
Stiamo aggiungendo i riferimenti alle tre nuove DLL al progetto [ui]:
![]() |
- [1]: creare riferimenti alle tre DLL nella cartella [lib] [2]
- [3]: le tre DLL fanno parte dei riferimenti del progetto
Torniamo a una panoramica dell'architettura dell'applicazione:
![]() |
Come illustrato sopra, il livello [ui] chiederà a Spring [0] di istanziare i livelli [dao] [1] e [metier] [2] in base alle informazioni contenute in un file di configurazione. Il livello [ui] chiederà quindi a Spring [3] un riferimento al livello [metier]. Ciò comporterà il seguente codice nel livello [ui]:
// we create the layers [metier and dao]
IImpotMetier metier = null;
try {
// spring context
IApplicationContext ctx = ContextRegistry.GetContext();
// a reference is requested on the [metier] layer
metier = (IImpotMetier)ctx.GetObject("metier");
} catch (Exception e1) {
...
}
- riga 5: istanziamento dei livelli [dao] e [metier] da parte di Spring
- riga 7: recupera un riferimento sul livello [metier].
La riga [5] sopra riportata utilizza il file di configurazione [App.config] nel progetto Visual Studio. In un progetto C#, questo file viene utilizzato per configurare l'applicazione. [App.config] non è quindi un concetto proprio di Spring, ma un concetto di Visual Studio che Spring sfrutta. Spring sa come utilizzare file di configurazione diversi da [App.config]. La soluzione qui presentata non è quindi l'unica disponibile.
Creiamo il file [App.config] con la procedura guidata di Visual Studio:
![]() |
- in [1]: aggiungi un nuovo elemento al progetto
- in [2]: selezionare "File di configurazione dell'applicazione"
- in [3]: [App.config] è il nome predefinito di questo file di configurazione
- in [4]: il file [App.config] è stato aggiunto al progetto
Il contenuto del file [App.config] è il seguente:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
</configuration>
[ App.config] è un file XML. La configurazione del progetto è racchiusa tra i tag <configuration>. La configurazione richiesta per Spring è la seguente:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object name="dao" type="Dao.FileImpot, ImpotsV5-dao">
<constructor-arg index="0" value="DataImpot.txt"/>
</object>
<object name="metier" type="Metier.ImpotMetier, ImpotsV5-metier">
<constructor-arg index="0" ref="dao"/>
</object>
</objects>
</spring>
</configuration>
- righe 11-23: la sezione delimitata dal tag <spring> è denominata gruppo di sezioni <spring>. È possibile creare tutti i gruppi di sezioni che si desidera in [App.config].
- un gruppo di sezioni contiene sezioni: è il caso qui:
- righe 12-14: la sezione <spring/context>
- righe 15-22: la sezione <spring/objects>
- righe 4-9: la regione <configSections> definisce l'elenco dei gestori di gruppi di sezioni presenti in [App.config].
- righe 5-8: definisce l'elenco dei gestori di sezione nel gruppo <spring> (name="spring").
- riga 6: il gestore della sezione <context> del gruppo <spring>:
- nome: nome della sezione gestita
- type : nome della classe che gestisce la sezione nel formato NomClasse, NomDLL.
- la sezione <context> del gruppo <spring> è gestita da [Spring.Context.Support.ContextHandler] che si trova nella DLL [Spring.Core.dll]
- riga 7: il gestore della sezione <objects> del gruppo <spring>
Le righe 4-9 sono standard in un file [App.config] con Spring. Le copiamo semplicemente da un progetto all'altro.
- righe 12-14: definiscono la sezione <spring/context>.
- riga 13: il tag <resource> indica la posizione del file che definisce le classi che Spring deve istanziare. Queste possono trovarsi in [App.config] come qui, ma possono anche trovarsi in un altro file di configurazione. La posizione di queste classi è indicata nell'attributo uri del tag <resource>:
- <resource uri="config://spring/objects> indica che l'elenco delle classi da istanziare si trova nel file [App.config] (configuring:), nella directory //spring/objects, ovvero nel tag <objects> del tag <spring>.
- <resource uri="file://spring-config.xml"> indicherebbe che l'elenco delle classi da istanziare si trova nel file [spring-config.xml]. Questo file dovrebbe essere collocato nelle cartelle di runtime del progetto (bin/Release o bin/Debug). Il modo più semplice è collocarlo, come è stato fatto per il file [DataImport.txt], nella radice del progetto con la proprietà [Copy to output directory=always].
Le righe 12-14 sono standard in un file [App.config] con Spring. Le copiamo semplicemente da un progetto all'altro.
- righe 15-22: definiscono le classi da istanziare. È qui che avviene la configurazione specifica di un'applicazione. L'elemento <objects> delimita la sezione di definizione delle classi da istanziare.
- righe 16-18: definiscono la classe da istanziare per il livello [dao]
- riga 16: ogni oggetto istanziato da Spring è oggetto di un tag <object>. Questo ha un attributo name che è il nome dell'oggetto istanziato. È così che l'applicazione chiede a Spring un riferimento: "dammi un riferimento all'oggetto chiamato dao". L'attributo type definisce la classe da istanziare come NomClasse, NomDLL. La riga 16 definisce un oggetto chiamato "dao", istanza di "Dao.FileImport", che si trova nella DLL "ImportsV5-dao.dll". Si noti che viene fornito il nome completo della classe (incluso lo spazio dei nomi) e che il suffisso .dll non è specificato nel nome della DLL.
Una classe può essere istanziata in due modi con Spring:
- tramite un costruttore speciale a cui vengono passati dei parametri: ciò avviene nelle righe 16-18.
- tramite il costruttore predefinito senza parametri. L'oggetto viene quindi inizializzato tramite la sua proprietà pubblica: il tag <object> ha quindi dei sottotag <property> per inizializzare queste proprietà. Non abbiamo alcun esempio di questo caso qui.
- (continua)
- riga 16: la classe istanziata è FileImport. Ha il seguente builder:
public FileImpot(string fileName);
I parametri del costruttore sono definiti utilizzando <constructor-arg>.
- riga 17: definisce il primo e unico parametro del costruttore. L'indice dell'attributo è il numero del parametro del costruttore, il valore dell'attributo è il suo valore: <constructor-arg index="i" value="valuei"/>
- righe 19-21: definiscono la classe da istanziare per il livello [metier]: classe [Metier.ImpotMetier], che si trova nella DLL [ImpotsV5-metier.dll].
- riga 19: la classe istanziata è ImpotMetier. Ha il seguente builder:
public ImpotMetier(IImpotDao dao);
- (continua)
- riga 20: definisce il primo e unico parametro del costruttore. Sopra, il parametro dao del costruttore è un riferimento a un oggetto. In questo caso, nel tag <constructor-arg>, utilizziamo l'attributo ref al posto di value utilizzato per il livello [dao]: <constructor-arg index="i" ref="refi"/>. Nel costruttore sopra riportato, il parametro dao rappresenta un'istanza sul livello [dao]. Questa istanza è stata definita dalle righe 16-18 del file di configurazione. Pertanto, alla riga 20:
<constructor-arg index="0" ref="dao"/>
ref="dao" rappresenta l'oggetto Spring "dao" definito dalle righe 16-18.
Riassumendo, il file [App.config]:
- istanzia il livello [dao] con la classe FileImpot che riceve come parametro DataImpot.txt (righe 16-18). L'oggetto risultante viene chiamato "dao"
- istanzia il livello [metier] con la classe ImpotMetier che riceve come parametro il precedente oggetto "dao" (righe 19-21).
Non resta che utilizzare questo file di configurazione Spring nel livello [ui]. Per farlo, duplichiamo la classe [Dialogue.cs] in [Dialogue2.cs] e rendiamo quest'ultima la classe principale del progetto [ui]:
![]() |
- in [1]: copia di [Dialogue.cs]
- in [2]: incollaggio
- in [3]: la copia di [Dialogue.cs]
- in [4]: rinominato [Dialogue2.cs]
![]() |
- in [6]: rendiamo [Dialogue2.cs] la classe principale del progetto [ui].
Il seguente codice da [Dialogue.cs] :
// we create the layers [metier and dao]
IImpotMetier metier = null;
try {
// layer creation [job]
metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
} catch (ImpotException e) {
// error display
string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
// program stop
Environment.Exit(1);
}
// infinite loop
while (true) {
...
diventa quanto segue in [Dialogue2.cs] :
// we create the layers [metier and dao]
IApplicationContext ctx = null;
try {
// spring context
ctx = ContextRegistry.GetContext();
} catch (Exception e1) {
// error display
Console.WriteLine("Chaîne des exceptions : \n{0}", "".PadLeft(40, '-'));
Exception e = e1;
while (e != null) {
Console.WriteLine("{0}: {1}", e.GetType().FullName, e.Message);
Console.WriteLine("".PadLeft(40, '-'));
e = e.InnerException;
}
// program stop
Environment.Exit(1);
}
// a reference is requested on the [metier] layer
IImpotMetier metier = (IImpotMetier)ctx.GetObject("metier");
// infinite loop
while (true) {
....................................
- riga 2: IApplicationContext dà accesso all'insieme di oggetti istanziati da Spring. Chiamiamo questo oggetto contesto Spring dell'applicazione, o semplicemente contesto dell'applicazione. Per il momento, questo contesto non è stato inizializzato. Il try / catch che segue lo fa.
- riga 5: viene letta e utilizzata la configurazione di Spring in [App.config]. Dopo questa operazione, se non è stata generata alcuna eccezione, tutti gli oggetti nella sezione <objects> saranno stati letti e istanziati:
- l'oggetto Spring "dao" è un'istanza del livello [dao]
- l'oggetto Spring "metier" è un'istanza del livello [metier]
- riga 19: la classe [Dialogue2.cs] necessita di un riferimento al livello [metier]. Questo viene richiesto al contesto dell'applicazione. L'oggetto IApplicationContext consente di accedere agli oggetti Spring tramite il loro nome (attributo name tag <object> della configurazione Spring). Il riferimento restituito è un riferimento al tipo generico Object. Dobbiamo convertire il riferimento restituito nel tipo corretto, in questo caso il tipo dell'interfaccia del livello [metier]: IImpotMetier.
Se tutto è andato bene, dopo la riga 19, [Dialogue2.cs] ha un riferimento al livello [metier]. Il codice dalle righe 21 in poi è quello della classe [Dialogue.cs] già studiata.
- righe 6-17: gestione dell'eccezione che si verifica quando il file di configurazione Spring non può essere elaborato. Ci possono essere varie ragioni per questo: sintassi errata nel file di configurazione stesso, o impossibilità di istanziare uno degli oggetti configurati. Nel nostro esempio, quest'ultimo caso si verificherebbe se il file DataImpot.txt della riga 17 di [App.config] non fosse stato trovato nel file di esecuzione del progetto.
L'eccezione alla riga 6 fa parte di una catena di eccezioni in cui ogni eccezione presenta due proprietà:
- Message: messaggio di errore dell'eccezione
- InnerException: l'eccezione precedente nella catena di eccezioni
Il ciclo alle righe 10-14 visualizza tutte le eccezioni della catena nella forma: classe dell'eccezione e messaggio associato.
Quando il progetto [ui] viene eseguito con un file di configurazione valido, si ottengono i risultati usuali:
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 60000
Impot=4282 euros
Quando si esegue il progetto [ui] con un file [DataImpotInexistant.txt] inesistente,
<object name="dao" type="Dao.FileImpot, ImpotsV5-dao">
<constructor-arg index="0" value="DataImpotInexistant.txt"/>
</object>
otteniamo i seguenti risultati:
- riga 17: l'eccezione originale di tipo [FileNotFoundException]
- riga 15: il livello [dao] incapsula questa eccezione in un tipo [Entites.ImpotException]
- riga 9: l'eccezione generata da Spring perché non è riuscito a istanziare l'oggetto denominato "dao". Nel processo di creazione di questo oggetto, si sono verificate in precedenza altre due eccezioni: quelle alle righe 11 e 13.
- Poiché non è stato possibile creare l'oggetto "dao", non è stato possibile creare il contesto dell'applicazione. Questo è il significato dell'eccezione alla riga 5. In precedenza si era verificata un'altra eccezione, quella della riga 7.
- riga 3: l'eccezione di livello più alto, l'ultima della catena: viene segnalato un errore di configurazione.
Da tutto ciò, ricorderemo che è l'eccezione più profonda, in questo caso quella della riga 17, che spesso è la più significativa. Si noti, tuttavia, che Spring ha conservato il messaggio di errore della riga 17 e lo ha trasmesso all'eccezione di livello più alto alla riga 3, al fine di avere la causa originale dell'errore al livello più alto.
Spring da solo meriterebbe un libro. Qui ne abbiamo solo accennato. Può essere approfondito con il documento [spring-net-reference.pdf] che si trova nella cartella di installazione di Spring:
![]() |
Vedi anche [http://tahe.developpez.com/dotnet/springioc], un tutorial su Spring presentato in un contesto VB.NET.






























































