Skip to content

6. Drielaagse architecturen

6.1. Introduction

Laten we nog eens terugkomen op de laatste versie van de belastingberekeningsapp:


using System;

namespace Chap3 {
    class Program {
        static void Main() {
            // interactief programma voor belastingberekening
            // de gebruiker voert drie gegevens in via het toetsenbord: gehuwd nbEnfants salaris
            // het programma geeft vervolgens de te betalen belasting weer
...

            // aanmaken van een object IImpot
            IImpot impot = null;
            try {
                // aanmaken van een object IImpot
                impot = new FileImpot("DataImpotInvalide.txt");
            } catch (FileImpotException e) {
                // foutmelding weergeven
...
                // programma stoppen
                Environment.Exit(1);
            }
            // oneindige lus
            while (true) {
                // de parameters voor de belastingberekening worden opgevraagd
                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();
...
                // de parameters zijn correct – de belasting wordt berekend
                Console.WriteLine("Impot=" + impot.calculer(marié == "o", nbEnfants, salaire) + " euros");
                // volgende belastingplichtige
            }//while
        }
    }
}

De vorige oplossing omvat klassieke programmeerprocessen:

  1. het ophalen van gegevens die zijn opgeslagen in bestanden, databases, ... regels 12-21
  2. de interactie met de gebruiker, regels 26 (invoer) en 29 (weergave)
  3. het gebruik van een vakspecifiek algoritme, regel 29

De praktijk heeft aangetoond dat het isoleren van deze verschillende bewerkingen in afzonderlijke klassen de onderhoudbaarheid van applicaties verbetert. De architectuur van een op deze manier gestructureerde applicatie is als volgt:

Deze architectuur wordt „drielagenarchitectuur“ genoemd, een vertaling van het Engelse „three-tier architecture“. De term „drielagen“ verwijst normaal gesproken naar een architectuur waarbij elke laag zich op een andere machine bevindt. Wanneer de lagen zich op dezelfde machine bevinden, wordt de architectuur een „drielagenarchitectuur“.

  • De laag [metier] bevat de bedrijfsregels van de applicatie. Voor onze belastingberekeningsapplicatie zijn dit de regels waarmee de belasting van een belastingplichtige kan worden berekend. Deze laag heeft gegevens nodig om te kunnen functioneren:
  • de belastingschijven, gegevens die elk jaar veranderen
  • het aantal kinderen, de burgerlijke staat en het jaarsalaris van de belastingplichtige

In het bovenstaande schema kunnen de gegevens uit twee bronnen afkomstig zijn:

  • de gegevenslaag of [dao] (DAO = Data Access Object) voor gegevens die al in bestanden of databases zijn opgeslagen. Dit zou hier het geval kunnen zijn voor de belastingschijven, zoals in de vorige versie van de applicatie is gedaan.
  • de gebruikersinterface-laag of [ui] (UI = User Interface) voor gegevens die door de gebruiker worden ingevoerd of aan de gebruiker worden getoond. Dit zou hier het geval kunnen zijn voor het aantal kinderen, de burgerlijke staat en het jaarsalaris van de belastingplichtige
  • In het algemeen zorgt de laag [dao] voor de toegang tot persistente gegevens (bestanden, databases) of niet-persistente gegevens (netwerk, sensoren, ...).
  • De laag [ui] zorgt op haar beurt voor de interacties met de gebruiker, indien er een is.
  • De drie lagen zijn onafhankelijk van elkaar gemaakt door het gebruik van interfaces.

We gaan de applicatie [Impots], die we al meerdere keren hebben bestudeerd, opnieuw bekijken om er een drielaagse architectuur aan te geven. Hiervoor gaan we de lagen [ui, metier, dao] een voor een bestuderen, te beginnen met de laag [dao], de laag die zich bezighoudt met de persistente gegevens.

Maar eerst moeten we de interfaces van de verschillende lagen van de applicatie [Impots] definiëren.

6.2. De interfaces van de applicatie [Impots]

Ter herinnering: een interface definieert een reeks methodesignaturen. De klassen die de interface implementeren, vullen deze methoden met inhoud.

Laten we terugkeren naar de drielaagse architectuur van onze applicatie:

In dit type architectuur neemt de gebruiker vaak het initiatief. Hij doet een verzoek in [1] en ontvangt een antwoord in [8]. Dit noemen we de verzoek-antwoordcyclus. Laten we het voorbeeld nemen van de berekening van de belasting van een belastingplichtige. Hiervoor zijn verschillende stappen nodig:

  1. de laag [ui] moet de gebruiker vragen naar het aantal kinderen, de burgerlijke staat en het jaarsalaris. Dit is de hierboven genoemde bewerking [1].
  2. Zodra dit is gebeurd, vraagt de laag [ui] aan de bedrijfslaag om de belasting te berekenen. Hiervoor stuurt zij de gegevens door die zij van de gebruiker heeft ontvangen. Dit is de bewerking [2].
  3. De laag [metier] heeft bepaalde informatie nodig om zijn taak uit te voeren: de belastingschijven. Deze laag zal deze informatie opvragen bij de laag [dao] via het pad [3, 4, 5, 6]. [3] is het oorspronkelijke verzoek en [6] het antwoord op dit verzoek.
  4. Nu de laag [metier] over alle benodigde gegevens beschikt, berekent deze de belasting.
  5. De laag [metier] kan nu reageren op het verzoek van de laag [ui] dat in (b) is gedaan. Dit is het pad [7].
  6. De laag [ui] zal deze resultaten opmaken en vervolgens aan de gebruiker presenteren. Dit is het pad [8].
  7. We kunnen ons voorstellen dat de gebruiker belastingsimulaties uitvoert en deze wil opslaan. Hiervoor gebruikt hij het pad [1-8].

Uit deze beschrijving blijkt dat een laag gebruikmaakt van de bronnen van de laag die rechts ervan ligt, nooit van die links ervan. Laten we twee aangrenzende lagen bekijken:

De laag [A] doet verzoeken aan de laag [B]. In de eenvoudigste gevallen wordt een laag geïmplementeerd door één enkele klasse. Een applicatie evolueert in de loop van de tijd. Zo kan de laag [B] verschillende implementatieklassen hebben, zoals [B1, B2, ...]. Als de laag [B] de laag [dao] is, kan deze een eerste implementatie hebben, [B1], die gegevens uit een bestand ophaalt. Enkele jaren later kan men de gegevens in een database willen opslaan. Dan wordt er een tweede implementatieklasse [B2] gebouwd. Als in de oorspronkelijke applicatie de laag [A] rechtstreeks met de klasse [B1] werkte, zijn we genoodzaakt de code van de laag [A] gedeeltelijk te herschrijven. Stel bijvoorbeeld dat we in de laag [A] iets als het volgende hebben geschreven:

1
2
3
B1 b1=new B1(...);
..
b1.getData(...);
  • regel 1: er wordt een instantie van de klasse [B1] aangemaakt
  • regel 3: er worden gegevens opgevraagd bij deze instantie

Als we aannemen dat de nieuwe implementatieklasse [B2] methoden gebruikt met dezelfde signatuur als die van de klasse [B1], dan moeten alle [B1] worden gewijzigd in [B2]. Dat is het zeer gunstige en vrij onwaarschijnlijke scenario als er geen aandacht is besteed aan deze methodesignaturen. In de praktijk komt het vaak voor dat de klassen [B1] en [B2] niet dezelfde methodesignaturen hebben, waardoor een groot deel van de laag [A] volledig herschreven moet worden.

Dit kan worden verbeterd door een interface in te voegen tussen de lagen [A] en [B]. Dit betekent dat we de methodesignaturen die de laag [B] aan de laag [A] aanbiedt, in een interface vastleggen. Het vorige schema ziet er dan als volgt uit:

De laag [A] richt zich nu niet meer rechtstreeks tot de laag [B], maar tot de interface [IB] daarvan. Zo verschijnt in de code van de laag [A] de implementatieklasse [Bi] van de laag [B] slechts één keer voor, namelijk bij de implementatie van de interface [IB]. Zodra dit is gebeurd, wordt in de code de interface [IB] gebruikt en niet de bijbehorende implementatieklasse. De vorige code wordt dan als volgt:

1
2
3
IB ib=new B1(...);
..
ib.getData(...);
  • regel 1: er wordt een instantie [ib] aangemaakt die de interface [IB] implementeert, door de klasse [B1] te instantiëren
  • regel 3: er worden gegevens opgevraagd bij de instantie [ib]

Als we nu de implementatie [B1] van de laag [B] vervangen door een implementatie [B2], en deze twee implementaties voldoen aan dezelfde interface [IB], dan hoeft alleen regel 1 van de laag [A] te worden gewijzigd en verder niets. Dit is een groot voordeel dat op zichzelf al het systematische gebruik van interfaces tussen twee lagen rechtvaardigt.

We kunnen nog een stap verder gaan en de laag [A] volledig onafhankelijk maken van de laag [B]. In de bovenstaande code vormt regel 1 een probleem omdat deze een harde verwijzing bevat naar de klasse [B1]. Het zou ideaal zijn als de laag [A] zou kunnen beschikken over een implementatie van de interface [IB] zonder een klasse te hoeven noemen. Dit zou in overeenstemming zijn met ons bovenstaande schema. We zien hier dat de laag [A] zich richt tot de interface [IB] en het is niet duidelijk waarom deze de naam zou moeten kennen van de klasse die deze interface implementeert. Dit detail is niet nuttig voor de laag [A].

Met het Spring-framework (http://www.springframework.org) kan dit resultaat worden bereikt. De voorgaande architectuur evolueert als volgt:

De transversale laag [Spring] stelt een laag in staat om via configuratie een verwijzing te verkrijgen naar de laag die zich rechts ervan bevindt, zonder dat de naam van de klasse die de laag implementeert bekend hoeft te zijn. Deze naam staat in de configuratiebestanden en niet in de C#-code. De C#-code van de laag [A] ziet er dan als volgt uit:

1
2
3
IB ib; // geïnitialiseerd door Spring
..
ib.getData(...);
  • regel 1: een instantie [ib] die de interface [IB] van de laag [B] implementeert. Deze instantie wordt door Spring aangemaakt op basis van informatie uit een configuratiebestand. Spring zorgt voor het aanmaken van:
    • de instantie [b] die de laag [B] implementeert
    • de instantie [a] die de laag [A] implementeert. Deze instantie wordt geïnitialiseerd. Het bovenstaande veld [ib] krijgt als waarde de referentie [b] van het object dat de laag [B] implementeert
  • regel 3: er worden gegevens opgevraagd bij de instantie [ib]

We zien nu dat de implementatieklasse [B1] van laag B nergens in de code van laag [A] voorkomt. Wanneer de implementatie [B1] wordt vervangen door een nieuwe implementatie [B2], verandert er niets in de code van de klasse [A]. We wijzigen simpelweg de Spring-configuratiebestanden om [B2] te instantiëren in plaats van [B1].

De combinatie van Spring en C#-interfaces zorgt voor een doorslaggevende verbetering van het onderhoud van applicaties door de lagen ervan onderling waterdicht te maken. Dit is de oplossing die we zullen gebruiken voor een nieuwe versie van de applicatie [Impots].

Laten we terugkeren naar de drielaagse architectuur van onze applicatie:

In eenvoudige gevallen kunnen we uitgaan van de laag [metier] om de interfaces van de applicatie te ontdekken. Om te kunnen werken, heeft de applicatie gegevens nodig:

  • die al beschikbaar zijn in bestanden, databases of via het netwerk. Deze worden geleverd door de laag [dao].
  • nog niet beschikbaar zijn. Deze worden dan geleverd door de laag [ui], die ze van de gebruiker van de applicatie ontvangt.

Welke interface moet de laag [dao] aanbieden aan de laag [metier]? Welke interacties zijn mogelijk tussen deze twee lagen? De laag [dao] moet de volgende gegevens leveren aan de laag [metier]:

  • de belastingschijven

In onze applicatie gebruikt de laag [dao] bestaande gegevens, maar creëert geen nieuwe. Een definitie van de interface van de laag [dao] zou als volgt kunnen luiden:


using Entites;

namespace Dao {
    public interface IImpotDao {
        // de belastingschijven
        TrancheImpot[] TranchesImpot{get;}
    }
}
  • regel 3: de laag [dao] wordt in de naamruimte [Dao] geplaatst
  • regel 6: de interface IImpotDao definieert de eigenschap TranchesImpot, die de belastingschijven aan de laag [métier] zal leveren.
  • regel 1: importeert de naamruimte waarin de structuur TrancheImpot is gedefinieerd:

namespace Entites {
    // een belastingschijf
    public struct TrancheImpot {
        public decimal Limite { get; set; }
        public decimal CoeffR { get; set; }
        public decimal CoeffN { get; set; }
    }
}

Laten we terugkeren naar de drielaagse architectuur van onze applicatie:

Welke interface moet de laag [metier] aanbieden aan de laag [ui]? Laten we de interacties tussen deze twee lagen nog eens op een rijtje zetten:

  1. de laag [ui] vraagt de gebruiker naar het aantal kinderen, de burgerlijke staat en het jaarsalaris. Dit is de hierboven genoemde bewerking [1].
  2. Zodra dit is gebeurd, vraagt de laag [ui] aan de bedrijfslaag om het aantal zetels te berekenen. Hiervoor stuurt zij de gegevens door die zij van de gebruiker heeft ontvangen. Dit is de bewerking [2].

Een definitie van de interface van de laag [metier] zou als volgt kunnen luiden:


namespace Metier {
    interface IImpotMetier {
        int CalculerImpot(bool marié, int nbEnfants, int salaire);
    }
}
  • regel 1: alles wat betrekking heeft op de laag [metier] wordt in de naamruimte [Metier] geplaatst.
  • regel 2: de interface IImpotMetier definieert slechts één methode: die waarmee de belasting van een belastingplichtige kan worden berekend op basis van zijn burgerlijke staat, het aantal kinderen en zijn jaarsalaris.

We bekijken een eerste implementatie van deze gelaagde architectuur.

6.3. Voorbeeldtoepassing – versie 4

6.3.1. Het Visual Studio-project

Het Visual Studio-project ziet er als volgt uit:

  • [1]: de map [Entites] bevat de objecten die in alle lagen voorkomen [ui, metier, dao]: de structuur TrancheImpot, de uitzondering FileImpotException.
  • [2]: de map [Dao] bevat de klassen en interfaces van de laag [dao]. We zullen twee implementaties van de interface IImpotDao gebruiken: de klasse HardwiredImpot die in paragraaf 4.10 is besproken en FileImpot die in paragraaf 5.8 is besproken.
  • [3]: de map [Metier] bevat de klassen en interfaces van de laag [metier]
  • [4]: de map [Ui] bevat de klassen van de laag [ui]
  • [5]: het bestand [DataImpot.txt] bevat de belastingschijven die worden gebruikt door de implementatie FileImpot van de laag [dao]. [6] is zo geconfigureerd dat het automatisch naar de uitvoermap van het project wordt gekopieerd.

6.3.2. De entiteiten van de applicatie

Laten we nog eens terugkomen op de drielaagse architectuur van onze applicatie:

We noemen entités de klassen die in alle lagen voorkomen. Dit geldt over het algemeen voor klassen en structuren die gegevens uit de laag [dao] inkapselen. Deze entiteiten reiken doorgaans terug tot de laag [ui].

De entiteiten van de applicatie zijn de volgende:

De structuur TrancheImpot


namespace Entites {
    // een belastingschijf
    public struct TrancheImpot {
        public decimal Limite { get; set; }
        public decimal CoeffR { get; set; }
        public decimal CoeffN { get; set; }
    }
}

L'-uitzondering FileImpotException


using System;

namespace Entites {
    public class FileImpotException : Exception {
        // foutcodes
        [Flags]
        public enum CodeErreurs { Acces = 1, Ligne = 2, Champ1 = 4, Champ2 = 8, Champ3 = 16 };

        // foutcode
        public CodeErreurs Code { get; set; }

        // fabrikanten
        public FileImpotException() {
        }
        public FileImpotException(string message)
            : base(message) {
        }
        public FileImpotException(string message, Exception e)
            : base(message, e) {
        }
    }
}

Opmerking: de klasse FileImpotException is alleen nuttig als de laag [dao] wordt geïmplementeerd door de klasse FileImpot.

6.3.3. De laag [dao]

Laten we de interface van de laag [dao] nog eens bekijken:


using Entites;

namespace Dao {
    public interface IImpotDao {
        // de belastingschijven
        TrancheImpot[] TranchesImpot{get;}
    }
}

We zullen deze interface op twee verschillende manieren implementeren.

Allereerst met de klasse HardwiredImpot die in paragraaf 4.10 is besproken:


using System;
using Entites;

namespace Dao {
    public class HardwiredImpot : IImpotDao {

        // tabellen met gegevens die nodig zijn voor de berekening van de belasting
        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 };
        // belastingschijven
        public TrancheImpot[] TranchesImpot { get; private set; }

        // constructor
        public HardwiredImpot() {
            // aanmaken van de tabel met belastingschijven
            TranchesImpot = new TrancheImpot[limites.Length];
            // invullen
            for (int i = 0; i < TranchesImpot.Length; i++) {
                TranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
            }
        }
    }// klasse
}// naamruimte
  • regel 5: de klasse HardwiredImpot implementeert de interface IImpotDao
  • regel 12: implementatie van de eigenschap TranchesImpot van de interface IImpotDao. Deze eigenschap is een automatische eigenschap. Ze implementeert de methode get van de eigenschap TranchesImpot van de interface IImpotDao. Bovendien is de methode set als privé gedefinieerd, dus intern voor de klasse, zodat de constructor op de regels 15-22 de array met belastingschijven kan initialiseren.

De interface IImpotDao zal eveneens worden geïmplementeerd door de klasse FileImpot die in paragraaf 5.8 wordt besproken:


using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using Entites;

namespace Dao {
    class FileImpot : IImpotDao {

        // gegevensbestand
        public string FileName { get; set; }

        // belastingschijven
        public TrancheImpot[] TranchesImpot { get; private set; }

        // constructeur
        public FileImpot(string fileName) {
            // de bestandsnaam wordt opgeslagen
            FileName = fileName;
            // gegevens
            List<TrancheImpot> listTranchesImpot = new List<TrancheImpot>();
            int numLigne = 1;
            // uitzondering
            FileImpotException fe = null;
            // de inhoud van het bestand fileName regel voor regel lezen
            Regex pattern = new Regex(@"s*:\s*");
            // in eerste instantie geen fout
            FileImpotException.CodeErreurs code = 0;
            try {
                using (StreamReader input = new StreamReader(FileName)) {
                    while (!input.EndOfStream && code == 0) {
                        // huidige regel
                        string ligne = input.ReadLine().Trim();
                        // lege regels worden genegeerd
                        if (ligne == "")
                            continue;
                        // regel opgesplitst in drie velden, gescheiden door:
                        string[] champsLigne = pattern.Split(ligne);
                        // zijn er 3 velden?
                        if (champsLigne.Length != 3) {
                            code = FileImpotException.CodeErreurs.Ligne;
                        }
                        // conversies van de 3 velden
                        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;
                            ;
                        }
                        // fout?
                        if (code != 0) {
                            // de fout wordt genoteerd
                            fe = new FileImpotException(String.Format("Ligne n° {0} incorrecte", numLigne)) { Code = code };
                        } else {
                            // de nieuwe belastingschijf wordt opgeslagen
                            listTranchesImpot.Add(new TrancheImpot() { Limite = limite, CoeffR = coeffR, CoeffN = coeffN });
                            // volgende regel
                            numLigne++;
                        }
                    }
                }
            } catch (Exception e) {
                // de fout wordt genoteerd
                fe = new FileImpotException(String.Format("Erreur lors de la lecture du fichier {0}", FileName), e) { Code = FileImpotException.CodeErreurs.Acces };
            }
            // moet de fout worden gemeld?
            if (fe != null) {
                // de uitzondering wordt gegenereerd
                throw fe;
            } else {
                // de lijst listImpot wordt teruggegeven in de tabel tranchesImpot
                TranchesImpot = listTranchesImpot.ToArray();
            }
        }
    }
}
  • deze code is al besproken in paragraaf 5.8.
  • regel 14: de methode TranchesImpot van de interface IImpotDao
  • regel 76: initialisatie van de belastingschijven in de constructor van de klasse, op basis van het bestand waarvan de constructor de naam heeft ontvangen in regel 17.

6.3.4. De laag [metier]

Laten we de interface van deze laag nog eens bekijken:


namespace Metier {
    public interface IImpotMetier {
        int CalculerImpot(bool marié, int nbEnfants, int salaire);
    }
}

De implementatie ImpotMetier van deze interface is als volgt:


using Entites;
using Dao;

namespace Metier {
    public class ImpotMetier : IImpotMetier {

        // laag [dao]
        private IImpotDao Dao { get; set; }

        // de belastingschijven
        private TrancheImpot[] tranchesImpot;

        // bouwer
        public ImpotMetier(IImpotDao dao) {
            // opslag
            Dao = dao;
            // belastingschijven
            tranchesImpot = dao.TranchesImpot;
        }

        // belastingberekening
        public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
            // berekening van het aantal aandelen
            decimal nbParts;
            if (marié)
                nbParts = (decimal)nbEnfants / 2 + 2;
            else
                nbParts = (decimal)nbEnfants / 2 + 1;
            if (nbEnfants >= 3)
                nbParts += 0.5M;
            // berekening belastbaar inkomen & gezinsquotiënt
            decimal revenu = 0.72M * salaire;
            decimal QF = revenu / nbParts;
            // berekening van de belasting
            tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
            int i = 0;
            while (QF > tranchesImpot[i].Limite)
                i++;
            // resultaat weergeven
            return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
        }//berekenen
    }//klasse

}
  • regel 5: de klasse [Metier] implementeert de interface [IImpotMetier].
  • regels 14-19: de laag [metier] moet samenwerken met de laag [dao]. Daarom moet deze een verwijzing hebben naar het object dat de interface IImpotDao implementeert. Daarom wordt deze verwijzing als parameter doorgegeven aan de constructor.
  • regel 16: de verwijzing naar de laag [dao] wordt opgeslagen in het privéveld van regel 8
  • regel 18: op basis van deze verwijzing vraagt de constructor de tabel met belastingschijven op en slaat een verwijzing hiernaar op in de privé-eigenschap van regel 8.
  • regels 22-41: implementatie van de methode CalculerImpot van de interface IImpotMetier. Deze implementatie maakt gebruik van de tabel met belastingschijven die door de constructor is geïnitialiseerd.

6.3.5. De laag [ui]

De klassen voor de gebruikersinteractie in versie 2 en 3 leken sterk op elkaar. Die van versie 2 zag er als volgt uit:


using System;

namespace Chap2 {
    public class Program {
        static void Main() {
...

            // aanmaken van een object IImpot
            IImpot impot = new HardwiredImpot();

            // oneindige lus
            while (true) {
...
            }//while
        }
    }
}

en die van versie 3:


using System;

namespace Chap3 {
    public class Program {
        static void Main() {
...

            // aanmaken van een object IImpot
            IImpot impot = null;
            try {
                // aanmaken van een object IImpot
                impot = new FileImpot("DataImpotInvalide.txt");
            } catch (FileImpotException e) {
                // foutmelding weergeven
                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);
                // programma beëindigd
                Environment.Exit(1);
            }
            // oneindige lus
            while (true) {
...
            }//while
        }
    }
}

Alleen de manier waarop het object van het type IImpot, waarmee de belasting wordt berekend, wordt geïnstantieerd, is anders. Dit object komt hier overeen met onze laag [métier].

Voor een implementatie van [dao] met de klasse HardwiredImpot is de dialoogklasse als volgt:


using System;
using Metier;
using Dao;
using Entites;

namespace Ui {
    public class Dialogue2 {
        static void Main() {
...

            // lagen worden aangemaakt [metier et dao]
            IImpotMetier metier = new ImpotMetier(new HardwiredImpot());

            // oneindige lus
            while (true) {
...
                // de parameters zijn correct – de belasting wordt berekend
                Console.WriteLine("Impot=" + metier.CalculerImpot(marié == "o", nbEnfants, salaire) + " euros");
                // volgende belastingplichtige
            }//while
        }
    }
}
  • regel 12: instantiëren van de lagen [dao] en [metier]. Ter herinnering: de laag [metier] heeft de laag [dao] nodig.
  • regel 18: gebruik van de laag [metier] om de belasting te berekenen

Voor een implementatie van [dao] met de klasse FileImpot is de dialoogklasse als volgt:


using System;
using Metier;
using Dao;
using Entites;

namespace Ui {
    public class Dialogue {
        static void Main() {
...
            // de lagen worden aangemaakt [metier et dao]
            IImpotMetier metier = null;
            try {
        // laag aanmaken [metier]
                metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
            } catch (FileImpotException e) {
                // foutmelding weergeven
                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);
                // programma stoppen
                Environment.Exit(1);
            }
            // oneindige lus
            while (true) {
...
                // de parameters zijn correct – de belasting wordt berekend
                Console.WriteLine("Impot=" + metier.CalculerImpot(marié == "o", nbEnfants, salaire) + " euros");
                // volgende belastingplichtige
            }//while
        }
    }
}
  • regel 11-21: instantiëren van de lagen [dao] en [metier]. Aangezien het instantiëren van de laag [dao] een uitzondering kan veroorzaken, wordt deze afgehandeld
  • regel 26: gebruik van de laag [metier] om de belasting te berekenen, net als in de vorige versie

6.3.6. Conclusie

De gelaagde architectuur en het gebruik van interfaces hebben onze applicatie een zekere flexibiliteit gegeven. Dit komt met name tot uiting in de manier waarop de laag [ui] de lagen [dao] en [métier] instantiëert:


        // de lagen worden aangemaakt [metier et dao]
IImpotMetier metier = new ImpotMetier(new HardwiredImpot());

in het ene geval en:


// de lagen worden aangemaakt [metier et dao]
            IImpotMetier metier = null;
            try {
        // laag [metier] wordt aangemaakt
                metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
            } catch (FileImpotException e) {
                // foutmelding weergeven
                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);
                // programma beëindigen
                Environment.Exit(1);
            }

in het andere geval. Afgezien van de uitzonderingsafhandeling in geval 2, verloopt het instantiëren van de lagen [dao] en [metier] in beide toepassingen op dezelfde manier. Zodra de lagen [dao] en [metier] zijn geïnstantieerd, is de code van de laag [ui] in beide gevallen identiek. Dit komt doordat de laag [métier] wordt aangestuurd via de bijbehorende interface IImpotMetier en niet via de implementatieklasse daarvan. Het wijzigen van de laag [metier] of de laag [dao] van de applicatie zonder hun interfaces te wijzigen, komt altijd neer op het wijzigen van alleen de voorgaande regels in de laag [ui].

Een ander voorbeeld van de flexibiliteit die deze architectuur biedt, is de implementatie van de laag [métier]:


using Entites;
using Dao;

namespace Metier {
    public class ImpotMetier : IImpotMetier {

        // laag [dao]
        private IImpotDao Dao { get; set; }

        // de belastingschijven
        private TrancheImpot[] tranchesImpot;

        // fabrikant
        public ImpotMetier(IImpotDao dao) {
            // opslag
            Dao = dao;
            // belastingschijven
            tranchesImpot = dao.TranchesImpot;
        }

        // belastingberekening
        public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
...
        }//berekenen
    }//klasse

}

Op regel 14 zien we dat de laag [métier] is opgebouwd op basis van een verwijzing naar de interface van de laag [dao]. Het wijzigen van de implementatie van deze laatste heeft dus geen invloed op de laag [métier]. Daarom kon onze enige implementatie van de laag [métier] zonder aanpassingen samenwerken met twee verschillende implementaties van de laag [dao].

6.4. Voorbeeldtoepassing – versie 5

Deze nieuwe versie bouwt voort op de vorige en bevat de volgende wijzigingen:

  • de lagen [métier] en [dao] zijn elk ingekapseld in een DLL en getest met het unit-testframework NUnit.
  • de integratie van de lagen wordt verzorgd door het Spring-framework

Bij grote projecten werken meerdere ontwikkelaars aan hetzelfde project. Gelaagde architecturen vergemakkelijken deze manier van werken: omdat de lagen onderling communiceren via duidelijk gedefinieerde interfaces, hoeft een ontwikkelaar die aan een laag werkt zich geen zorgen te maken over het werk van andere ontwikkelaars aan de andere lagen. Het volstaat dat iedereen zich aan de interfaces houdt.

Hierboven zal de ontwikkelaar van de laag [métier] bij het testen van zijn laag een implementatie van de laag [dao] nodig hebben. Zolang deze nog niet voltooid is, kan hij een dummy-implementatie van de laag [dao] gebruiken, mits deze voldoet aan de interface IImpotDao. Dit is ook een voordeel van de gelaagde architectuur: een vertraging in de laag [dao] belemmert het testen van de laag [métier] niet. De dummy-implementatie van de [dao]-laag heeft bovendien het voordeel dat deze vaak eenvoudiger te implementeren is dan de echte [dao]-laag, waarvoor het starten van een SGBD of netwerkverbindingen nodig kunnen zijn, ...

Zodra de laag [dao] voltooid en getest is, wordt deze aan de ontwikkelaars van de laag [métier] geleverd in de vorm van een DLL in plaats van broncode. Uiteindelijk wordt de applicatie vaak geleverd in de vorm van een uitvoerbaar .exe-bestand (dat van de laag [ui]) en .dll-klassenbibliotheken (de andere lagen).

6.4.1. NUnit

De tests die tot nu toe voor onze diverse applicaties zijn uitgevoerd, waren gebaseerd op een visuele controle. Er werd gecontroleerd of op het scherm werd weergegeven wat de bedoeling was. Deze methode is onbruikbaar wanneer er veel tests moeten worden uitgevoerd. De mens is namelijk onderhevig aan vermoeidheid en zijn vermogen om tests te controleren neemt in de loop van de dag af. De tests moeten daarom worden geautomatiseerd en ernaar streven dat er geen menselijke tussenkomst nodig is.

Een applicatie evolueert in de loop van de tijd. Bij elke wijziging moet worden gecontroleerd of de applicatie geen „regressie” vertoont, c.a.d, en of ze nog steeds voldoet aan de functionele tests die bij de oorspronkelijke ontwikkeling zijn uitgevoerd. Deze tests worden „non-regressietests” genoemd. Voor een applicatie van enige omvang kunnen honderden tests nodig zijn. Er wordt namelijk elke methode van elke klasse van de applicatie getest. Dit worden unit-tests genoemd. Deze kunnen veel ontwikkelaars in beslag nemen als ze niet zijn geautomatiseerd.

Er zijn tools ontwikkeld om de tests te automatiseren. Een daarvan heet NUnit. Deze is beschikbaar op de website [http://www.nunit.org]:

Voor dit document is versie 2.4.6 hierboven gebruikt (maart 2008). Bij de installatie wordt een pictogram [1] op het bureaublad geplaatst:

Een dubbelklik op het pictogram [1] start de grafische interface van NUnit [2]. Dit draagt op geen enkele manier bij aan de automatisering van de tests, aangezien we opnieuw terugvallen op een visuele controle: de tester controleert de testresultaten die in de grafische interface worden weergegeven. Toch kunnen de tests ook worden uitgevoerd met batchtools en kunnen de resultaten worden opgeslagen in XML-bestanden. Deze methode wordt door de ontwikkelteams gebruikt: de tests worden ’s nachts gestart en de ontwikkelaars beschikken de volgende ochtend over het resultaat.

Laten we aan de hand van een voorbeeld het principe van de NUnit-tests bekijken. Laten we eerst een nieuw C#-project van het type Console Application aanmaken:

In [1] zien we de références van het project. Deze verwijzingen zijn DLL die klassen en interfaces bevatten die door het project worden gebruikt. De referenties die in [1] worden weergegeven, worden standaard in elk nieuw C#-project opgenomen. Om de klassen en interfaces van het NUnit-framework te kunnen gebruiken, moeten we een nieuwe referentie aan het project toevoegen.

In het tabblad .NET hierboven kiezen we de component [nunit.framework]. De hierboven genoemde componenten [nunit.*] zijn geen componenten die standaard aanwezig zijn in de .NET-omgeving. Ze zijn daar terechtgekomen door de eerdere installatie van het framework NUnit. Zodra de toevoeging van de verwijzing is gevalideerd, verschijnt deze als [4] in de lijst met verwijzingen van het project.

Vóór het genereren van de applicatie is de map [bin/Release] van het project leeg. Na het genereren (F6) is te zien dat de map [bin/Release] niet langer leeg is:

In [6] zien we de aanwezigheid van DLL en [nunit.framework.dll]. Het is de toevoeging van de referentie [nunit.framework] die ervoor heeft gezorgd dat deze DLL naar de uitvoermap is gekopieerd. Dit is namelijk een van de mappen die door CLR (Common Language Runtime) .NET worden doorzocht om de klassen en interfaces te vinden waarnaar het project verwijst.

Laten we een eerste testklasse NUnit maken. Hiervoor verwijderen we de standaard gegenereerde klasse [Program.cs] en voegen we vervolgens een nieuwe klasse [Nunit1.cs] toe aan het project. We verwijderen ook de overbodige verwijzingen [7].

De testklasse NUnit1 ziet er als volgt uit:


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");
        }
    }
}
  • regel 6: de klasse NUnit1 moet openbaar zijn. Het sleutelwoord public wordt niet standaard door Visual Studio gegenereerd. Dit moet worden toegevoegd.
  • regel 5: het attribuut [TestFixture] is een NUnit-attribuut. Het geeft aan dat de klasse een testklasse is.
  • regels 7-9: de constructor. Deze wordt hier alleen gebruikt om een bericht op het scherm weer te geven. We willen zien wanneer deze wordt uitgevoerd.
  • regel 10: het attribuut [SetUp] definieert een methode die vóór elke unit-test wordt uitgevoerd.
  • regel 14: het attribuut [TearDown] definieert een methode die na elke unit-test wordt uitgevoerd.
  • regel 18: het attribuut [Test] definieert een testmethode. Voor elke methode die is geannoteerd met het attribuut [Test], wordt de geannoteerde methode [SetUp] vóór de test uitgevoerd en de geannoteerde methode [TearDown] na de test.
  • regel 21: een van de methoden [Assert.*] die zijn gedefinieerd door het framework NUnit. Er zijn de volgende methoden [Assert]:
    • [Assert.AreEqual(expression1, expression2)]: controleert of de waarden van de twee uitdrukkingen gelijk zijn. Er worden veel soorten uitdrukkingen geaccepteerd (int, string, float, double, decimal, ...). Als de twee uitdrukkingen niet gelijk zijn, wordt er een uitzondering gegenereerd.
    • [Assert.AreEqual(réel1, réel2, delta)]: controleert of twee reële getallen gelijk zijn op delta na, c.a.d abs(reëel1-reëel2)<=delta. Men kan bijvoorbeeld [Assert.AreEqual(réel1, réel2, 1E-6)] schrijven om te controleren of twee waarden gelijk zijn op 10-6 na.
    • [Assert.AreEqual(expression1, expression2, message)] en [Assert.AreEqual(réel1, réel2, delta, message)] zijn varianten waarmee de foutmelding kan worden gespecificeerd die moet worden gekoppeld aan de uitzondering die wordt gegenereerd wanneer de methode [Assert.AreEqual] mislukt.
    • [Assert.IsNotNull(object)] en [Assert.IsNotNull(object, message)]: controleert of object niet gelijk is aan null.
    • [Assert.IsNull(object)] en [Assert.IsNull(object, message)]: controleert of object gelijk is aan null.
    • [Assert.IsTrue(expression)] en [Assert.IsTrue(expression, message)]: controleert of de uitdrukking gelijk is aan true.
    • [Assert.IsFalse(expression)] en [Assert.IsFalse(expression, message)]: controleert of de uitdrukking gelijk is aan 'false'.
    • [Assert.AreSame(object1, object2)] en [Assert.AreSame(object1, object2, message)]: controleert of de verwijzingen object1 en object2 naar hetzelfde object verwijzen.
    • [Assert.AreNotSame(object1, object2)] en [Assert.AreNotSame(object1, object2, message)]: controleert of de verwijzingen object1 en object2 niet naar hetzelfde object verwijzen.
  • regel 21: de assertie moet slagen
  • regel 26: de assertie moet mislukken

Laten we het project zo configureren dat bij het genereren een DLL wordt geproduceerd in plaats van een uitvoerbaar .exe-bestand:

  • in [1]: projecteigenschappen
  • naar [2, 3]: als projecttype kiezen we [Class Library] (klassenbibliotheek)
  • in [4]: het genereren van het project levert een DLL (assembly) op met de naam [Nunit.dll]

Laten we nu NUnit gebruiken om de testklasse uit te voeren:

  • in [1]: een project NUnit openen
  • in [2, 3]: we laden de DLL bin/Release/Nunit.dll die is gegenereerd door het C#-project
  • naar [4]: DLL is geladen
  • in [5]: de testboom
  • in [6]: deze worden uitgevoerd
  • in [7]: de resultaten: t1 geslaagd, t2 mislukt
  • in [8]: een rode balk geeft aan dat de testklasse in zijn geheel is mislukt
  • in [9]: de foutmelding met betrekking tot de mislukte test
  • in [11]: de verschillende tabbladen van het resultatenvenster
  • in [12]: het tabblad [Console.Out]. Hierop is te zien dat:
    • de constructor slechts één keer is uitgevoerd
    • de methode [SetUp] vóór elk van de twee tests is uitgevoerd
    • de methode [TearDown] na elk van de twee tests is uitgevoerd

Het is mogelijk om aan te geven welke methoden getest moeten worden:

  • in [1]: er wordt gevraagd om een selectievakje naast elke test weer te geven
  • in [2]: de uit te voeren tests worden aangevinkt
  • in [3]: de tests worden uitgevoerd

Om fouten te corrigeren, volstaat het om het C#-project aan te passen en opnieuw te genereren. NUnit detecteert dat het geteste bestand DLL is gewijzigd en laadt het nieuwe bestand automatisch in. Vervolgens volstaat het om de tests opnieuw uit te voeren.

Laten we eens kijken naar de volgende nieuwe testklasse:


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");
        }
    }
}

Vanaf versie 2.4 van NUnit is er een nieuwe syntaxis beschikbaar gekomen, namelijk die in de regels 21 en 26. Hiervoor moet de testklasse afleiden van de klasse AssertionHelper (regel 6).

De (niet-uitputtende) overeenkomst tussen de oude en de nieuwe syntaxis is als volgt:

Assert.AreEqual(expression1, expression2,
message)
Expect(expression1,EqualTo(expression2),message)
Assert.AreEqual(réel1, réel2, delta, message)
Expect(expression1,EqualTo(expression2).Within(delta),
essage)
Assert.AreSame(objet1, objet2, message)
Expect(objet1,SameAs(objet2),message)
Assert.AreNotSame(objet1, objet2, message)
Expect(objet1,Not.SameAs(objet2),message)
Assert.IsNull(objet,message)
Expect(objet,Null,message)
Assert.IsNotNull(objet,message)
Expect(objet,Not.Null,message)
Assert.IsTrue(expression,message)
Expect(expression,True,message)
Assert.IsFalse(expression,message)
Expect(expression,False,message)

Laten we de volgende test toevoegen aan de klasse 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));
}

Als we (F6) de nieuwe DLL van het C#-project genereren, ziet het project NUnit er als volgt uit:

  • in [1]: de nieuwe testklasse [NUnit2] is automatisch gedetecteerd
  • in [2]: test t3 van NUnit2 wordt uitgevoerd
  • in [3]: de t3-test is geslaagd

Voor meer informatie over NUnit kunt u de helptekst van NUnit raadplegen:

6.4.2. De Visual Studio-oplossing

We gaan stap voor stap de volgende Visual Studio-oplossing bouwen:

  • in [1]: de oplossing ImpotsV5 bestaat uit drie projecten, één voor elk van de drie lagen van de applicatie
  • in [2]: het project [dao] van de laag [dao]
  • in [3]: het project [metier] van de laag [metier]
  • naar [4]: het project [ui] van de laag [ui]

De oplossing ImpotsV5 kan als volgt worden opgebouwd:

1
234
5
  • tot [1]: maak een nieuw project aan
  • in [2]: een console-applicatie kiezen
  • in [3]: het project [dao] openen
  • in [4]: het project aanmaken
  • in [5]: zodra het project is aangemaakt, opslaan
  • in [6]: de naam [dao] voor het project behouden
  • in [7]: een map opgeven om het project en de bijbehorende oplossing op te slaan
  • in [8]: geef de oplossing een naam
  • in [9]: aangeven dat de oplossing een eigen map moet hebben
  • in [10]: het project en de bijbehorende oplossing opslaan
  • in [11]: het project [dao] in de bijbehorende oplossing ImpotsV5
  • in [12]: de map van de oplossing ImpotsV5. Deze bevat de map [dao] uit de map [dao].
  • in [13]: de inhoud van de map [dao]
  • in [14]: er wordt een nieuw project toegevoegd aan de oplossing ImpotsV5
  • in [15]: het nieuwe project heet [metier]
  • in [16]: de oplossing met haar twee projecten
  • in [17]: de oplossing, nadat het derde project [ui] is toegevoegd
  • in [18]: de map van de oplossing en de mappen van de drie projecten
  • wanneer je een oplossing uitvoert via (Ctrl+F5), wordt het actieve project uitgevoerd. Hetzelfde geldt wanneer je de oplossing genereert (F6). De naam van het actieve project wordt in de oplossing vetgedrukt weergegeven: [19].
  • in [20]: om het actieve project van de oplossing te wijzigen
  • in [21]: het project [metier] is nu het actieve project van de oplossing

6.4.3. De laag [dao]

De projectreferenties (zie [1] in het project)

We voegen de referentie [nunit.framework] toe die nodig is voor de tests [NUnit]

De entiteiten (zie [2] in het project)

De klasse [TrancheImpot] is die van de vorige versies. De klasse [FileImpotException] uit de vorige versie wordt hernoemd naar [ImpotException] om deze algemener te maken en niet te koppelen aan een specifieke laag [dao]:


using System;

namespace Entites {
    public class ImpotException : Exception {

        // foutcode
        public int Code { get; set; }

        // fabrikanten
        public ImpotException() {
        }
        public ImpotException(string message)
            : base(message) {
        }
        public ImpotException(string message, Exception e)
            : base(message, e) {
        }
    }
}

De laag [dao] (zie [3] in het project)

De interface [IImpotDao] is die van de vorige versie. Hetzelfde geldt voor de klasse [HardwiredImpot]. De klasse [FileImpot] wordt aangepast om rekening te houden met de wijziging van de uitzondering [FileImpotException] in [ImpotException]:


...

namespace Dao {
    public class FileImpot : IImpotDao {

        // foutcodes
        [Flags]
        public enum CodeErreurs { Acces = 1, Ligne = 2, Champ1 = 4, Champ2 = 8, Champ3 = 16 };

...

        // fabrikant
        public FileImpot(string fileName) {
            // de bestandsnaam wordt opgeslagen
            FileName = fileName;
...
            // in eerste instantie geen fout
            CodeErreurs code = 0;
            try {
                using (StreamReader input = new StreamReader(FileName)) {
                    while (!input.EndOfStream && code == 0) {
...
                        // fout?
                        if (code != 0) {
                            // de fout wordt genoteerd
                            fe = new ImpotException(String.Format("Ligne n° {0} incorrecte", numLigne)) { Code = (int)code };
                        } else {
...
                        }
                    }
                }
            } catch (Exception e) {
                // de fout wordt genoteerd
                fe = new ImpotException(String.Format("Erreur lors de la lecture du fichier {0}", FileName), e) { Code = (int)CodeErreurs.Acces };
            }
            // moet de fout worden gemeld?
...
        }
    }
}
  • regel 8: de foutcodes die voorheen in de klasse [FileImpotException] stonden, zijn verplaatst naar de klasse [FileImpot]. Dit zijn namelijk foutcodes die specifiek zijn voor deze implementatie van de interface [IImpotDao].
  • regels 26 en 34: om een fout in te kapselen, wordt de klasse [ImpotException] gebruikt en niet langer de klasse [FileImpotException].

De test [Test1] (zie [4] in het project)

De klasse [Test1] geeft alleen de belastingschijven op het scherm weer:


using System;
using Dao;
using Entites;

namespace Tests {
    class Test1 {
        static void Main() {

            // de laag [dao] wordt aangemaakt
            IImpotDao dao = null;
            try {
                // laag [dao] aangemaakt
                dao = new FileImpot("DataImpot.txt");
            } catch (ImpotException e) {
                // foutmelding
                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);
                // programma beëindigd
                Environment.Exit(1);
            }
            // de belastingschijven worden weergegeven
            TrancheImpot[] tranchesImpot = dao.TranchesImpot;
            foreach (TrancheImpot t in tranchesImpot) {
                Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
            }
        }
    }
}
  • regel 13: de laag [dao] wordt geïmplementeerd door de klasse [FileImpot]
  • regel 14: de uitzondering van het type [ImpotException] die kan optreden, wordt afgehandeld.

Het bestand [DataImpot.txt] dat nodig is voor de tests, wordt automatisch gekopieerd naar de uitvoermap van het project (zie [5] in het project). Het project [dao] zal meerdere klassen bevatten met een methode [Main]. Daarom moet expliciet worden aangegeven welke klasse moet worden uitgevoerd wanneer de gebruiker het project met Ctrl-F5 laat uitvoeren:

  • in [1]: ga naar de projecteigenschappen
  • in [2]: aangeven dat het een console-applicatie is
  • in [3]: de uit te voeren klasse specificeren

De uitvoering van de vorige klasse [Test1] levert de volgende resultaten op:

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

De test [Test2] (zie [4] in het project)

De klasse [Test2] doet hetzelfde als de klasse [Test1] door de laag [dao] te implementeren met de klasse [HardwiredImpot]. Regel 13 van [Test1] wordt vervangen door het volgende:


                dao = new HardwiredImpot();

Het project is aangepast zodat nu de klasse [Test2] wordt uitgevoerd:

De schermresultaten zijn hetzelfde als voorheen.

De test NUnit [NUnit1] (zie [4] in het project)

De unit-test [NUnit1] is als volgt:


using System;
using Dao;
using Entites;
using NUnit.Framework;

namespace Tests {
    [TestFixture]
    public class NUnit1 : AssertionHelper{
        // laag [dao] te testen
        private IImpotDao dao;

        // fabrikant
        public NUnit1() {
            // initialisatie laag [dao]
            dao = new FileImpot("DataImpot.txt");
        }

        // test
        [Test]
        public void ShowTranchesImpot(){
            // de belastingschijven worden weergegeven
            TrancheImpot[] tranchesImpot = dao.TranchesImpot;
            foreach (TrancheImpot t in tranchesImpot) {
                Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
            }
            // enkele 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));
        }
    }
}
  • de testklasse is afgeleid van de klasse [AssertionHelper], waardoor de statische methode Expect kan worden gebruikt (regels 27-30).
  • regel 10: een verwijzing naar de laag [dao]
  • regels 13-16: de constructor instantiëert de laag [dao] met de klasse [FileImpot]
  • regels 19-20: de testmethode
  • regel 22: de tabel met belastingschijven wordt opgehaald uit de laag [dao]
  • regels 23-25: deze worden weergegeven zoals eerder. Deze weergave zou in een echte unit-test niet nodig zijn. Hier dient deze weergave een didactisch doel.
  • regel 27: er wordt gecontroleerd of er inderdaad 7 belastingschijven zijn
  • regels 28-30: we controleren de waarden van belastingschijf nr. 2

Om deze unit-test uit te voeren, moet het project van het type [Class Library] zijn:

  • in [1]: de aard van het project is gewijzigd
  • naar [2]: het gegenereerde DLL krijgt de naam [ImpotsV5-dao.dll]
  • in [3]: na het genereren (F6) van het project bevat de map [dao/bin/Release] de bestanden DLL en [ImpotsV5-dao.dll]

Vervolgens wordt het bestand DLL [ImpotsV5-dao.dll] in het framework NUnit geladen en uitgevoerd:

  • in [1]: de tests zijn geslaagd. We beschouwen de laag [dao] nu als operationeel. De bijbehorende DLL bevat alle klassen van het project, inclusief de testklassen. Deze zijn overbodig. We bouwen de DLL opnieuw op om de testklassen ervan uit te sluiten.
  • in [2]: de map [tests] wordt uit het project verwijderd
  • in [3]: het nieuwe project. Dit wordt opnieuw gegenereerd door F6 om een nieuwe DLL te genereren.

6.4.4. De laag [metier]

  • in [1] is het project [metier] het actieve project van de oplossing geworden
  • in [2]: de projectreferenties
  • in [3]: de laag [metier]
  • in [4]: de testklassen
  • in [5]: het bestand [DataImpot.txt] met de belastingschijven, geconfigureerd in [6] om automatisch te worden gekopieerd naar de uitvoermap van het project [7]

De projectreferenties (zie [2] in het project)

Net als bij het project [dao] wordt de referentie [nunit.framework] toegevoegd die nodig is voor de tests [NUnit]. De laag [metier] heeft de laag [dao] nodig. Daarom is een verwijzing naar de DLL van deze laag vereist. Dit gaat als volgt:

  • in [1]: voeg een nieuwe verwijzing toe aan de verwijzingen van het project [metier]
  • in [2]: selecteer het tabblad [Browse]
  • in [3]: selecteer de map [dao/bin/Release]
  • in [4]: selecteer de DLL en [ImpotsV5-dao.dll] die zijn gegenereerd in het project [dao]
  • in [5]: de nieuwe referentie

De laag [metier] (zie [3] in het project)

De interface [IImpotMetier] is die van de vorige versie. Hetzelfde geldt voor de klasse [ImpotMetier].

De test [Test1] (zie [4] in het project)

De klasse [Test1] voert slechts enkele salarisberekeningen uit:


using System;
using Dao;
using Entites;
using Metier;

namespace Tests {
    class Test1 {
        static void Main() {

            // de laag [metier] wordt aangemaakt
            IImpotMetier metier = null;
            try {
                // laag [metier] wordt aangemaakt
                metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
            } catch (ImpotException e) {
                // foutmelding weergeven
                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);
                // programma wordt gestopt
                Environment.Exit(1);
            }
            // er worden enkele belastingen berekend
            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)));
        }
    }
}
  • regel 14: aanmaken van de lagen [metier] en [dao]. De laag [dao] wordt geïmplementeerd met de klasse [FileImpot]
  • regels 12-21: afhandeling van een eventuele uitzondering van het type [ImpotException]
  • regels 23-26: herhaalde aanroepen van de enige methode CalculerImpot van de interface [IImpotMetier].

Het project [metier] is als volgt geconfigureerd:

  • [1]: het project is van het type console-applicatie
  • [2]: de uitgevoerde klasse is de klasse [Test1]
  • [3]: bij het genereren van het project wordt het uitvoerbare bestand [ImpotsV5-metier.exe] geproduceerd

De uitvoering van het project levert de volgende resultaten op:

1
2
3
4
Impot(true,2,60000)=4282 euros
Impot(false,3,60000)=4282 euros
Impot(false,3,60000)=0 euros
Impot(false,3,60000)=179275 euros

De test [NUnit1] (zie [4] in het project)

De unit-testklasse [NUnit1] neemt de vier voorgaande berekeningen over en controleert het resultaat ervan:


using Dao;
using Metier;
using NUnit.Framework;

namespace Tests {
    [TestFixture]
    public class NUnit1:AssertionHelper {
        // laag [metier] moet worden getest
        private IImpotMetier metier;

        // fabrikant
        public NUnit1() {
            // initialisatie laag [metier]
            metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
        }

        // test
        [Test]
        public void CalculsImpot(){
            // de belastingschijven worden weergegeven
            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));
        }
    }
}
  • regel 14: aanmaken van de lagen [metier] en [dao]. De laag [dao] wordt geïmplementeerd met de klasse [FileImpot]
  • regels 21-24: herhaalde aanroepen van de enige methode CalculerImpot van de interface [IImpotMetier] met controle van de resultaten.

Het project [metier] is nu als volgt geconfigureerd:

  • [1]: het project is van het type „klassenbibliotheek“
  • [2]: bij het genereren van het project wordt DLL en [ImpotsV5-metier.dll] gegenereerd

Het project is gegenereerd (F6). Vervolgens wordt de gegenereerde DLL [ImpotsV5-metier.dll] geladen in NUnit en getest:

 

Hierboven zijn de tests geslaagd. We beschouwen de laag [metier] nu als operationeel. De bijbehorende DLL bevat alle klassen van het project, inclusief de testklassen. Deze zijn overbodig. We bouwen de DLL opnieuw op om de testklassen ervan uit te sluiten.

  • naar [1]: de map [tests] wordt uitgesloten van het project
  • naar [2]: het nieuwe project. Dit wordt opnieuw gegenereerd door F6 om een nieuwe DLL te genereren.

6.4.5. De laag [ui]

  • in [1] is het project [ui] het actieve project van de oplossing geworden
  • in [2]: de projectreferenties
  • in [3]: de laag [ui]
  • in [4]: het bestand [DataImpot.txt] met de belastingschijven, geconfigureerd in [5] om automatisch te worden gekopieerd naar de uitvoermap van het project [6]

De projectreferenties (zie [2] in het project)

De laag [ui] heeft de lagen [metier] en [dao] nodig om de belastingberekeningen uit te voeren. Daarom heeft deze laag een verwijzing nodig naar de DLL van deze twee lagen. We gaan te werk zoals eerder is getoond voor de laag [metier]

De hoofdklasse [Dialogue.cs] (zie [3] in het project)

De klasse [Dialogue.cs] is die van de vorige versie.

Tests

Het project [ui] is als volgt geconfigureerd:

  • [1]: het project is van het type „console-applicatie“
  • [2]: bij het genereren van het project wordt het uitvoerbare bestand [ImpotsV5-ui.exe] geproduceerd
  • [3]: de klasse die zal worden uitgevoerd

Een voorbeeld van een uitvoering (Ctrl+F5) is als volgt:

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. De laag [Spring]

Laten we teruggaan naar de code in [Dialogue.cs] die de lagen [dao] en [metier] aanmaakt:


// lagen aanmaken [metier et dao]
            IImpotMetier metier = null;
            try {
        // laag aanmaken [metier]
                metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
            } catch (ImpotException e) {
                // foutmelding weergeven
...
                // programma beëindigen
                Environment.Exit(1);
            }

Regel 5 maakt de lagen [dao] en [metier] aan door de implementatieklassen van beide lagen expliciet te benoemen: FileImpot voor de laag [dao], ImpotMetier voor de laag [metier]. Als de implementatie van een van de lagen met een nieuwe klasse wordt uitgevoerd, wordt regel 5 aangepast. Bijvoorbeeld:


                metier = new ImpotMetier(new HardwiredImpot());

Afgezien van deze wijziging verandert er niets in de applicatie, aangezien elke laag via een interface met de volgende laag communiceert. Zolang deze interface niet verandert, verandert ook de communicatie tussen de lagen niet. Het Spring-framework stelt ons in staat om de onafhankelijkheid van de lagen nog iets verder door te voeren door de namen van de klassen die de verschillende lagen implementeren, in een configuratiebestand onder te brengen. Het wijzigen van de implementatie van een laag komt dan neer op het aanpassen van een configuratiebestand. Dit heeft geen enkele invloed op de code van de applicatie.

Hierboven zal de laag [ui] aan Spring vragen omde lagen [dao], [1], [metier] en [2] te instantiëren op basis van de informatie in een configuratiebestand. De laag [ui] zal vervolgens aan Spring [3] een verwijzing naar de laag [metier] vragen:


            // lagen worden aangemaakt [metier et dao]
            IImpotMetier metier = null;
            try {
                // Spring-context
                IApplicationContext ctx = ContextRegistry.GetContext();
                // er wordt een referentie opgevraagd voor de laag [metier]
                metier = (IImpotMetier)ctx.GetObject("metier");
            } catch (Exception e1) {
...
}
  • regel 5: instantiëren van de lagen [dao] en [metier] door Spring
  • regel 7: er wordt een verwijzing opgehaald naar de laag [metier]. Merk op dat de laag [ui] deze verwijzing had zonder de naam te vermelden van de klasse die de laag [metier] implementeert.

Het Spring-framework bestaat in twee versies: Java en .NET. De .NET-versie is beschikbaar via de url (maart 2008) [http://www.springframework.net/]:

  • in [1]: de website van [Spring.net]
  • en [2]: de downloadpagina
  • in [3]: Spring 1.1 downloaden (maart 2008)
  • naar [4]: de .exe-versie downloaden en vervolgens installeren
  • in [5]: de map die door de installatie wordt aangemaakt
  • in [6]: de map [bin/net/2.0/release] bevat de DLL-bestanden van Spring voor Visual Studio-projecten .NET 2.0 of hoger. Spring is een veelzijdig framework. Het onderdeel van Spring dat we hier gaan gebruiken om de integratie van de lagen in een applicatie te beheren, heet IoC: Inversion of Control of ook wel DI: Dependency Injection. Spring biedt bibliotheken voor toegang tot databases met NHibernate, het genereren en gebruiken van webservices, webapplicaties, ...
  • de DLL die nodig zijn om de integratie van de lagen in een applicatie te beheren, zijn de DLL, [7] en [8].

We slaan deze drie DLL op in een map [lib] van ons project:

  • [1]: de drie DLL-bestanden worden met Windows Verkenner in de map [lib] geplaatst
  • [2]: in het project [ui] worden alle bestanden weergegeven
  • [3]: de map [ui/lib] is nu zichtbaar. Deze wordt aan het project toegevoegd
  • [4]: de map [ui/lib] maakt deel uit van het project

Het aanmaken van de map [lib] is geenszins noodzakelijk. De verwijzingen konden rechtstreeks worden aangemaakt in de drie DLL-bestanden in de map [bin/net/2.0/release] van [Spring.net]. Het aanmaken van de map [lib] maakt het echter mogelijk om de applicatie te ontwikkelen op een werkstation dat niet over [Spring.net] beschikt, waardoor deze minder afhankelijk is van de beschikbare ontwikkelomgeving.

We voegen aan het project [ui] verwijzingen toe naar de drie nieuwe DLL-bestanden:

  • [1]: we maken verwijzingen aan naar de drie DLL-bestanden in de map [lib] [2]
  • [3]: de drie DLL-bestanden maken deel uit van de projectreferenties

Laten we terugkeren naar een overzicht van de architectuur van de applicatie:

Hierboven zal de laag [ui] aan Spring vragen om [0]de lagen [dao], [1], [metier] en [2] te instantiëren op basis van de informatie in een configuratiebestand. De laag [ui] zal vervolgens aan Spring [3] een verwijzing naar de laag [metier] opvragen. Dit vertaalt zich in de laag [ui] in de volgende code:


            // de lagen [metier et dao] worden aangemaakt
            IImpotMetier metier = null;
            try {
                // Spring-context
                IApplicationContext ctx = ContextRegistry.GetContext();
                // er wordt een referentie opgevraagd voor de laag [metier]
                metier = (IImpotMetier)ctx.GetObject("metier");
            } catch (Exception e1) {
...
}
  • regel 5: instantiëren van de lagen [dao] en [metier] door Spring
  • regel 7: er wordt een verwijzing opgehaald naar de laag [metier].

De bovenstaande regel [5] maakt gebruik van het configuratiebestand [App.config] van het Visual Studio-project. In een C#-project wordt dit bestand gebruikt om de applicatie te configureren. [App.config] is dus geen Spring-concept, maar een Visual Studio-concept dat door Spring wordt gebruikt. Spring kan ook andere configuratiebestanden gebruiken dan [App.config]. De hier gepresenteerde oplossing is dus niet de enige beschikbare.

Laten we het bestand [App.config] aanmaken met de Visual Studio-wizard:

  • in [1]: een nieuw element aan het project toevoegen
  • in [2]: selecteer "Application Configuration File"
  • in [3]: [App.config] is de standaardnaam van dit configuratiebestand
  • in [4]: het bestand [App.config] is aan het project toegevoegd

De inhoud van het bestand [App.config] is als volgt:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
</configuration>

[App.config] is een bestand XML. De configuratie van het project vindt plaats tussen de tags <configuration>. De benodigde configuratie voor Spring is als volgt:


<?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>
  • regels 11-23: het gedeelte dat wordt afgebakend door de tag <spring> wordt de sectiegroep <spring> genoemd. In [App.config] kunnen zoveel sectiegroepen worden aangemaakt als men wil.
  • Een sectiegroep bevat secties: dat is hier het geval:
    • regels 12-14: de sectie <spring/context>
    • regels 15-22: de sectie <spring/objects>
  • regels 4-9: het gebied <configSections> definieert de lijst met handlers van de sectiegroepen die in [App.config] aanwezig zijn.
  • regels 5-8: definieert de lijst met handlers voor de secties van de groep <spring> (name="spring").
  • regel 6: de handler van de sectie <context> van de groep <spring>:
    • name: naam van de beheerde sectie
    • type: naam van de klasse die de sectie beheert in de vorm NomClasse, NomDLL.
    • de sectie <context> van de groep <spring> wordt beheerd door de klasse [Spring.Context.Support.ContextHandler], die te vinden is in de DLL [Spring.Core.dll]
  • regel 7: de beheerder van de sectie <objects> van de groep <spring>

De regels 4-9 zijn standaard in een [App.config]-bestand met Spring. We kopiëren ze gewoon van het ene project naar het andere.

  • regels 12-14: definiëren de sectie <spring/context>.
  • regel 13: de tag <resource> dient om aan te geven waar het bestand zich bevindt dat de klassen definieert die Spring moet instantiëren. Deze kunnen zich in [App.config] bevinden, zoals hier, maar ze kunnen ook in een ander configuratiebestand staan. De locatie van deze klassen wordt opgegeven in het attribuut uri van de tag <resource>:
    • <resource uri="config://spring/objects"> geeft aan dat de lijst met te instantiëren klassen zich bevindt in het bestand [App.config] (config:), in de sectie //spring/objects, c.a.d, binnen de tag <objects> van de tag <spring>.
    • <resource uri="file://spring-config.xml"> zou aangeven dat de lijst met te instantiëren klassen zich in het bestand [spring-config.xml] bevindt. Dit bestand moet in de uitvoermappen (bin/Release of bin/Debug) van het project worden geplaatst. Het eenvoudigst is om het, net zoals bij het bestand [DataImpot.txt], in de hoofdmap van het project te plaatsen met de eigenschap [Copy to output directory=always].

De regels 12-14 zijn standaard in een [App.config]-bestand met Spring. We kopiëren ze gewoon van het ene project naar het andere.

  • regels 15-22: definiëren de klassen die moeten worden geïnstantieerd. In dit gedeelte vindt de specifieke configuratie van een applicatie plaats. De tag <objects> bakent het gedeelte af waarin de te instantiëren klassen worden gedefinieerd.
  • regels 16-18: definiëren de klasse die moet worden geïnstantieerd voor de laag [dao]
    • regel 16: elk door Spring geïnstantieerd object wordt omsloten door een <object>-tag. Deze tag heeft een name-attribuut dat de naam van het geïnstantieerde object aangeeft. Hiermee vraagt de applicatie een referentie aan bij Spring: „geef me een referentie naar het object met de naam dao”. Het type-attribuut definieert de klasse die moet worden geïnstantieerd in de vorm NomClasse, NomDLL. Zo definieert regel 16 een object met de naam „dao”, een instantie van de klasse „Dao.FileImpot” die zich bevindt in de „DLL” „ImpotsV5-dao.dll”. Merk op dat de volledige naam van de klasse wordt opgegeven (inclusief de naamruimte) en dat het achtervoegsel .dll niet wordt vermeld in de naam van de DLL.

Een klasse kan op twee manieren worden geïnstantieerd met Spring:

  1. via een specifieke constructor waaraan parameters worden doorgegeven: dit gebeurt in de regels 16-18.
  2. via de standaardconstructor zonder parameters. Het object wordt dan geïnitialiseerd via zijn openbare eigenschappen: de tag <object> bevat dan <property>-subtags om deze verschillende eigenschappen te initialiseren. We hebben hier geen voorbeeld van dit geval.
  • (vervolg)
    • regel 16: de geïnstantieerde klasse is de klasse FileImpot. Deze heeft de volgende constructor:

        public FileImpot(string fileName);

De parameters van de constructor worden gedefinieerd met behulp van <constructor-arg>-tags.

  • regel 17: definieert de eerste en enige parameter van de constructor. Het attribuut index is het nummer van de constructorparameter, het attribuut value is de waarde ervan: <constructor-arg index="i" value="valuei"/>
  • regels 19-21: definiëren de klasse die moet worden geïnstantieerd voor de laag [metier]: de klasse [Metier.ImpotMetier], die zich bevindt in de DLL [ImpotsV5-metier.dll].
    • regel 19: de geïnstantieerde klasse is de klasse ImpotMetier. Deze heeft de volgende constructor:

        public ImpotMetier(IImpotDao dao);
  • (vervolg)
    • regel 20: definieert de eerste en enige parameter van de constructor. Hierboven is de parameter dao van de constructor een objectreferentie. In dit geval wordt in de tag <constructor-arg> het attribuut ref gebruikt in plaats van het attribuut value dat werd gebruikt voor de laag [dao]: <constructor-arg index="i" ref="refi"/>. In de bovenstaande constructor vertegenwoordigt de parameter dao een instantie op de laag [dao]. Deze instantie is gedefinieerd in de regels 16-18 van het configuratiebestand. Zo staat in regel 20:

                <constructor-arg index="0" ref="dao"/>

ref="dao" staat voor het Spring-object "dao" dat in de regels 16-18 is gedefinieerd.

Samenvattend: het bestand [App.config]:

  • maakt een instantie aan van de laag [dao] met de klasse FileImpot, die DataImpot.txt als parameter ontvangt (regels 16-18). Het resulterende object wordt "dao" genoemd
  • maakt een instantie aan van de laag [metier] met de klasse ImpotMetier, die het voorgaande "dao"-object als parameter ontvangt (regels 19-21).

Nu hoeven we alleen nog maar dit Spring-configuratiebestand te gebruiken in de laag [ui]. Hiervoor dupliceren we de klasse [Dialogue.cs] naar [Dialogue2.cs] en maken we van deze laatste de hoofdklasse van het project [ui]:

  • in [1]: kopie van [Dialogue.cs]
  • naar [2]: samenvoeging
  • naar [3]: de kopie van [Dialogue.cs]
  • in [4]: hernoemd naar [Dialogue2.cs]
  • naar [6]: we maken van [Dialogue2.cs] de hoofdklasse van het project [ui].

De volgende code uit [Dialogue.cs]:


            // de lagen [metier et dao] worden aangemaakt
            IImpotMetier metier = null;
            try {
        // laag [metier] aangemaakt
                metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
            } catch (ImpotException e) {
                // foutmelding weergeven
                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);
                // programma stoppen
                Environment.Exit(1);
            }
            // oneindige lus
            while (true) {
...

wordt in [Dialogue2.cs] als volgt:


            // lagen worden aangemaakt [metier et dao]
            IApplicationContext ctx = null;
            try {
                // Spring-context
                ctx = ContextRegistry.GetContext();
            } catch (Exception e1) {
                // foutmelding
                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;
                }
                // programma afsluiten
                Environment.Exit(1);
            }
            // er wordt een referentie opgevraagd op de laag [metier]
            IImpotMetier metier = (IImpotMetier)ctx.GetObject("metier");
            // oneindige lus
            while (true) {
....................................
  • regel 2: IApplicationContext biedt toegang tot alle objecten die door Spring zijn geïnstantieerd. Dit object wordt de Spring-context van de applicatie genoemd, of kortweg de applicatiecontext. Op dit moment is deze context nog niet geïnitialiseerd. Dat gebeurt door de volgende try/catch-blok.
  • regel 5: de Spring-configuratie in [App.config] wordt ingelezen en verwerkt. Als er na deze bewerking geen uitzondering is opgetreden, zijn alle objecten uit de sectie <objects> geïnstantieerd:
  • het Spring-object „dao” is een instantie op de laag [dao]
  • het Spring-object "metier" is een instantie op de laag [metier]
  • regel 19: de klasse [Dialogue2.cs] heeft een verwijzing nodig naar de laag [metier]. Deze wordt opgevraagd bij de applicatiecontext. Het object IApplicationContext biedt toegang tot de Spring-objecten via hun naam (het attribuut `name` van de tag `<object>` in de Spring-configuratie). De geretourneerde verwijzing is een verwijzing naar het generieke type Object. We moeten de geretourneerde referentie omzetten naar het juiste type, in dit geval het type van de interface van de laag [metier]: IImpotMetier.

Als alles goed is gegaan, heeft [Dialogue2.cs] na regel 19 een verwijzing naar de laag [metier]. De code vanaf regel 21 is die van de klasse [Dialogue.cs] die we al hebben besproken.

  • regels 6-17: afhandeling van de uitzondering die optreedt wanneer het verwerken van het Spring-configuratiebestand niet kan worden voltooid. Hiervoor kunnen verschillende redenen zijn: onjuiste syntaxis van het configuratiebestand zelf of het onvermogen om een van de geconfigureerde objecten te instantiëren. In ons voorbeeld zou dit laatste het geval zijn als het bestand DataImpot.txt uit regel 17 van [App.config] niet in de uitvoermap van het project zou worden gevonden.

De uitzondering die op regel 6 wordt doorgegeven, is een reeks uitzonderingen waarbij elke uitzondering twee eigenschappen heeft:

  • Message: het foutbericht dat bij de uitzondering hoort
  • InnerException: de voorgaande uitzondering in de uitzonderingsketen

De lus in de regels 10-14 geeft alle uitzonderingen in de keten weer in de vorm: uitzonderingsklasse en bijbehorend bericht.

Wanneer het project [ui] met een geldig configuratiebestand wordt uitgevoerd, krijgt men de gebruikelijke resultaten:

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

Wanneer het project [ui] wordt uitgevoerd met een niet-bestaand bestand [DataImpotInexistant.txt],


            <object name="dao" type="Dao.FileImpot, ImpotsV5-dao">
                <constructor-arg index="0" value="DataImpotInexistant.txt"/>
            </object>

krijgt men de volgende resultaten:

Chaîne des exceptions :
----------------------------------------
System.Configuration.ConfigurationErrorsException: Error creating context 'spring.root': Could not find file 'C:\data\2007-2008\c# 2008\poly\Chap4\ImpotsV5\ui\bin\Release\DataImpotInexistant.txt'.
----------------------------------------
Spring.Util.FatalReflectionException: Cannot instantiate Type [Spring.Context.Support.XmlApplicationContext] using ctor [Void .ctor(System.String, Boolean, System.String[])] : 'Exception has been thrown by the target of an invocation.'
----------------------------------------
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
----------------------------------------
Spring.Objects.Factory.ObjectCreationException: Error creating object with name'dao' defined in 'config [spring/objects]' : Initialization of object failed : Cannot instantiate Type [Dao.FileImpot] using ctor [Void .ctor(System.String)] :'Exception has been thrown by the target of an invocation.'
----------------------------------------
Spring.Util.FatalReflectionException: Cannot instantiate Type [Dao.FileImpot] using ctor [Void .ctor(System.String)] : 'Exception has been thrown by the targetof an invocation.'
----------------------------------------
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
----------------------------------------
Entites.ImpotException: Erreur lors de la lecture du fichier DataImpotInexistant.txt
----------------------------------------
System.IO.FileNotFoundException: Could not find file 'C:\data\2007-2008\c# 2008\poly\Chap4\ImpotsV5\ui\bin\Release\DataImpotInexistant.txt'.
  • regel 17: de oorspronkelijke uitzondering van het type [FileNotFoundException]
  • regel 15: de laag [dao] verpakt deze uitzondering in een type [Entites.ImpotException]
  • regel 9: de uitzondering die door Spring is gegenereerd omdat het niet gelukt is het object met de naam "dao" te instantiëren. Tijdens het aanmaken van dit object zijn er eerder twee andere uitzonderingen opgetreden: die op regel 11 en 13.
  • Omdat het object "dao" niet kon worden aangemaakt, kon de applicatiecontext niet worden aangemaakt. Dit is de betekenis van de uitzondering op regel 5. Eerder was er al een andere uitzondering opgetreden, namelijk die op regel 7.
  • regel 3: de uitzondering op het hoogste niveau, de laatste in de keten: er wordt een configuratiefout gemeld.

Uit dit alles kunnen we concluderen dat de diepste uitzondering, in dit geval die op regel 17, vaak de meest veelzeggende is. Merk echter op dat Spring de foutmelding van regel 17 heeft behouden om deze door te geven aan de uitzondering op het hoogste niveau (regel 3), zodat de oorspronkelijke oorzaak van de fout op het hoogste niveau bekend is.

Spring verdient op zichzelf al een heel boek. We hebben het onderwerp hier slechts oppervlakkig behandeld. Je kunt je er verder in verdiepen met het document [spring-net-reference.pdf], dat je kunt vinden in de installatiemap van Spring:

 

Je kunt ook [http://tahe.developpez.com/dotnet/springioc] lezen, een Spring-tutorial die wordt gepresenteerd in de context van VB.NET.