Skip to content

6. Architektury trójwarstwowe

6.1. Introduction

Wróćmy do najnowszej wersji aplikacji do obliczania podatku:


using System;

namespace Chap3 {
    class Program {
        static void Main() {
            // interaktywny program do obliczania podatku
            // użytkownik wprowadza trzy dane za pomocą klawiatury: żonaty nbEnfants wynagrodzenie
            // następnie program wyświetla kwotę podatku do zapłaty
...

            // utworzenie obiektu IImpot
            IImpot impot = null;
            try {
                // utworzenie obiektu IImpot
                impot = new FileImpot("DataImpotInvalide.txt");
            } catch (FileImpotException e) {
                // wyświetlenie błędu
...
                // zatrzymanie programu
                Environment.Exit(1);
            }
            // pętla nieskończona
            while (true) {
                // wymagane są parametry obliczenia podatku
                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();
...
                // parametry są poprawne – obliczany jest podatek
                Console.WriteLine("Impot=" + impot.calculer(marié == "o", nbEnfants, salaire) + " euros");
                // kolejny podatnik
            }//while
        }
    }
}

Poprzednie rozwiązanie obejmowało klasyczne operacje programistyczne:

  1. pobieranie danych zapisanych w plikach, bazach danych itp. – linie 12–21
  2. interakcja z użytkownikiem, wiersze 26 (wprowadzanie danych) i 29 (wyświetlanie)
  3. wykorzystanie algorytmu biznesowego, wiersz 29

Praktyka wykazała, że wyodrębnienie tych różnych operacji do oddzielnych klas poprawia łatwość konserwacji aplikacji. Architektura tak zorganizowanej aplikacji wygląda następująco:

Architekturę tę nazywa się „architekturą trójwarstwową”, co jest tłumaczeniem angielskiego terminu „three-tier architecture”. Termin „trójwarstwowa” zazwyczaj odnosi się do architektury, w której każda warstwa znajduje się na innym komputerze. Gdy warstwy znajdują się na tym samym komputerze, architektura staje się architekturą „trójwarstwową”.

  • Warstwa [metier] zawiera reguły biznesowe aplikacji. W przypadku naszej aplikacji do obliczania podatku są to reguły umożliwiające obliczenie podatku podatnika. Warstwa ta potrzebuje danych do działania:
  • progi podatkowe, które zmieniają się co roku
  • liczba dzieci, stan cywilny oraz roczne wynagrodzenie podatnika

Na powyższym schemacie dane mogą pochodzić z dwóch źródeł:

  • warstwa dostępu do danych lub [dao] (DAO = Data Access Object) w przypadku danych już zapisanych w plikach lub bazach danych. W tym przypadku mogłoby to dotyczyć przedziałów podatkowych, tak jak miało to miejsce w poprzedniej wersji aplikacji.
  • warstwa interfejsu użytkownika lub [ui] (UI = User Interface) w przypadku danych wprowadzanych przez użytkownika lub wyświetlanych użytkownikowi. W tym przypadku mogłaby to dotyczyć liczby dzieci, stanu cywilnego oraz rocznego wynagrodzenia podatnika
  • Ogólnie rzecz biorąc, warstwa [dao] odpowiada za dostęp do danych trwałych (pliki, bazy danych) lub nietrwałych (sieć, czujniki itp.).
  • Warstwa [ui] natomiast odpowiada za interakcje z użytkownikiem, o ile taki istnieje.
  • Wszystkie trzy warstwy są od siebie niezależne dzięki zastosowaniu interfejsów.

Wrócimy do aplikacji [Impots], którą już kilkakrotnie omawialiśmy, aby nadać jej architekturę trójwarstwową. W tym celu przeanalizujemy warstwy [ui, metier, dao] po kolei, zaczynając od warstwy [dao], która odpowiada za dane trwałe.

Wcześniej musimy zdefiniować interfejsy poszczególnych warstw aplikacji [Impots].

6.2. Interfejsy aplikacji [Impots]

Przypomnijmy, że interfejs definiuje zbiór sygnatur metod. Klasy implementujące interfejs nadają tym metodom konkretną treść.

Wróćmy do trójwarstwowej architektury naszej aplikacji:

W tego typu architekturze to często użytkownik podejmuje inicjatywę. Wysyła żądanie w [1] i otrzymuje odpowiedź w [8]. Nazywa się to cyklem żądanie–odpowiedź. Weźmy na przykład obliczenie podatku podatnika. Będzie to wymagało kilku etapów:

  1. warstwa [ui] będzie musiała zapytać użytkownika o liczbę dzieci, stan cywilny i roczne wynagrodzenie. Jest to operacja [1] opisana powyżej.
  2. Po wykonaniu tej czynności warstwa [ui] zwróci się do warstwy biznesowej o obliczenie podatku. W tym celu przekaże jej dane otrzymane od użytkownika. Jest to operacja [2].
  3. Warstwa [metier] potrzebuje pewnych informacji, aby wykonać swoje zadanie: przedziały podatkowe. Zwróci się o te informacje do warstwy [dao], korzystając ze ścieżki [3, 4, 5, 6]. [3] to żądanie początkowe, a [6] to odpowiedź na to żądanie.
  4. Mając wszystkie potrzebne dane, warstwa [metier] oblicza podatek.
  5. Warstwa [metier] może teraz odpowiedzieć na żądanie warstwy [ui] wysłane w punkcie (b). Jest to ścieżka [7].
  6. Warstwa [ui] sformatuje te wyniki, a następnie przedstawi je użytkownikowi. Jest to ścieżka [8].
  7. Można sobie wyobrazić, że użytkownik przeprowadza symulacje podatkowe i chce je zapisać. W tym celu skorzysta ze ścieżki [1-8].

Z opisu tego wynika, że dana warstwa korzysta z zasobów warstwy znajdującej się po jej prawej stronie, nigdy z warstwy po lewej. Rozważmy dwie sąsiadujące warstwy:

Warstwa [A] wysyła żądania do warstwy [B]. W najprostszych przypadkach warstwa jest implementowana przez jedną klasę. Aplikacja ewoluuje z biegiem czasu. W związku z tym warstwa [B] może mieć różne klasy implementacyjne, np. [B1, B2, ...]. Jeśli warstwa [B] jest warstwą [dao], ta ostatnia może mieć pierwszą implementację [B1], która pobiera dane z pliku. Kilka lat później może zaistnieć potrzeba umieszczenia danych w bazie danych. Wówczas stworzymy drugą klasę implementacyjną o nazwie [B2]. Jeśli w pierwotnej aplikacji warstwa [A] współpracowała bezpośrednio z klasą [B1], konieczne jest częściowe przepisanie kodu warstwy [A]. Załóżmy na przykład, że w warstwie [A] napisaliśmy coś takiego:

1
2
3
B1 b1=new B1(...);
..
b1.getData(...);
  • wiersz 1: tworzona jest instancja klasy [B1]
  • wiersz 3: od tej instancji żądane są dane

Jeśli założymy, że nowa klasa implementacyjna [B2] wykorzystuje metody o tej samej sygnaturze co klasa [B1], konieczne będzie zastąpienie wszystkich wystąpień [B1] na [B2]. Jest to bardzo korzystny scenariusz, który jednak jest raczej mało prawdopodobny, jeśli nie zwrócono uwagi na te sygnatury metod. W praktyce często zdarza się, że klasy [B1] i [B2] nie mają takich samych sygnatur metod, w związku z czym znaczna część warstwy [A] musi zostać całkowicie przepisana.

Sytuację można poprawić, wprowadzając interfejs pomiędzy warstwami [A] i [B]. Oznacza to, że w interfejsie ustalamy sygnatury metod przekazywanych przez warstwę [B] do warstwy [A]. Wówczas poprzedni schemat przyjmuje następujący kształt:

Warstwa [A] nie zwraca się już bezpośrednio do warstwy [B], lecz do jej interfejsu [IB]. W ten sposób w kodzie warstwy [A] klasa implementacyjna [Bi] warstwy [B] pojawia się tylko raz, podczas implementacji interfejsu [IB]. W związku z tym w kodzie wykorzystywany jest interfejs [IB], a nie jego klasa implementacyjna. Poprzedni kod zmienia się na następujący:

1
2
3
IB ib=new B1(...);
..
ib.getData(...);
  • wiersz 1: instancja [ib] implementująca interfejs [IB] jest tworzona poprzez instancjonowanie klasy [B1]
  • wiersz 3: dane są pobierane z instancji [ib]

Jeśli teraz zastąpimy implementację [B1] warstwy [B] implementacją [B2], a obie te implementacje będą realizować ten sam interfejs [IB], wówczas należy zmodyfikować jedynie wiersz 1 warstwy [A], a żadnego innego. Jest to ogromna zaleta, która sama w sobie uzasadnia systematyczne stosowanie interfejsów między dwiema warstwami.

Można pójść jeszcze dalej i uczynić warstwę [A] całkowicie niezależną od warstwy [B]. W powyższym kodzie wiersz 1 stanowi problem, ponieważ zawiera stałe odwołanie do klasy [B1]. Idealnym rozwiązaniem byłoby, gdyby warstwa [A] mogła korzystać z implementacji interfejsu [IB] bez konieczności podawania nazwy klasy. Byłoby to zgodne z naszym schematem przedstawionym powyżej. Widać na nim, że warstwa [A] odwołuje się do interfejsu [IB] i nie widać powodu, dla którego musiałaby znać nazwę klasy implementującej ten interfejs. Ten szczegół nie jest przydatny dla warstwy [A].

Framework Spring (http://www.springframework.org) pozwala osiągnąć ten rezultat. Poprzednia architektura zmienia się w następujący sposób:

Warstwa przekrojowa [Spring] umożliwi warstwie uzyskanie poprzez konfigurację odwołania do warstwy znajdującej się po jej prawej stronie bez konieczności znajomości nazwy klasy implementującej tę warstwę. Nazwa ta będzie znajdować się w plikach konfiguracyjnych, a nie w kodzie C#. Kod C# warstwy [A] przyjmuje zatem następującą postać:

1
2
3
IB ib; // zainicjowany przez Spring
..
ib.getData(...);
  • wiersz 1: instancja [ib] implementująca interfejs [IB] warstwy [B]. Instancja ta jest tworzona przez Spring na podstawie informacji znalezionych w pliku konfiguracyjnym. Spring zajmie się utworzeniem:
    • instancję [b] implementującą warstwę [B]
    • instancję [a] implementującą warstwę [A]. Instancja ta zostanie zainicjowana. Powyższe pole [ib] otrzyma jako wartość odwołanie [b] do obiektu implementującego warstwę [B]
  • wiersz 3: żądane są dane z instancji [ib]

Widać teraz, że klasa implementacyjna [B1] warstwy B nie pojawia się nigdzie w kodzie warstwy [A]. Gdy implementacja [B1] zostanie zastąpiona nową implementacją [B2], w kodzie klasy [A] nic się nie zmieni. Wystarczy po prostu zmienić pliki konfiguracyjne Springa, aby instancjonować [B2] zamiast [B1].

Połączenie Springa z interfejsami C# stanowi decydującą poprawę w zakresie utrzymania aplikacji, zapewniając wzajemną izolację ich warstw. To właśnie to rozwiązanie wykorzystamy w nowej wersji aplikacji [Impots].

Wróćmy do trójwarstwowej architektury naszej aplikacji:

W prostych przypadkach można zacząć od warstwy [metier], aby poznać interfejsy aplikacji. Do działania aplikacja potrzebuje danych:

  • już dostępne w plikach, bazach danych lub w sieci. Dane te są dostarczane przez warstwę [dao].
  • które nie są jeszcze dostępne. W takim przypadku są one dostarczane przez warstwę [ui], która pozyskuje je od użytkownika aplikacji.

Jakie interfejsy powinna udostępniać warstwa [dao] warstwie [metier]? Jakie są możliwe interakcje między tymi dwiema warstwami? Warstwa [dao] musi dostarczać następujące dane do warstwy [metier]:

  • przedziały podatkowe

W naszej aplikacji warstwa [dao] wykorzystuje istniejące dane, ale nie tworzy nowych. Definicja interfejsu warstwy [dao] mogłaby wyglądać następująco:


using Entites;

namespace Dao {
    public interface IImpotDao {
        // progi podatkowe
        TrancheImpot[] TranchesImpot{get;}
    }
}
  • wiersz 3: warstwa [dao] zostanie umieszczona w przestrzeni nazw [Dao]
  • wiersz 6: interfejs IImpotDao definiuje właściwość TranchesImpot, która dostarczy przedziały podatkowe do warstwy [métier].
  • wiersz 1: importuje przestrzeń nazw, w której zdefiniowana jest struktura TrancheImpot:

namespace Entites {
    // przedział podatkowy
    public struct TrancheImpot {
        public decimal Limite { get; set; }
        public decimal CoeffR { get; set; }
        public decimal CoeffN { get; set; }
    }
}

Wróćmy do trójwarstwowej architektury naszej aplikacji:

Jaki interfejs powinna warstwa [metier] udostępniać warstwie [ui]? Przypomnijmy sobie interakcje między tymi dwiema warstwami:

  1. warstwa [ui] pyta użytkownika o liczbę dzieci, stan cywilny i roczne wynagrodzenie. Jest to operacja [1] opisana powyżej.
  2. Po wykonaniu tej czynności warstwa [ui] zwróci się do warstwy biznesowej o obliczenie liczby miejsc. W tym celu przekaże jej dane otrzymane od użytkownika. Jest to operacja [2].

Definicja interfejsu warstwy [metier] mogłaby wyglądać następująco:


namespace Metier {
    interface IImpotMetier {
        int CalculerImpot(bool marié, int nbEnfants, int salaire);
    }
}
  • wiersz 1: wszystko, co dotyczy warstwy [metier], zostanie umieszczone w przestrzeni nazw [Metier].
  • wiersz 2: interfejs IImpotMetier definiuje tylko jedną metodę: tę, która pozwala obliczyć podatek podatnika na podstawie jego stanu cywilnego, liczby dzieci i rocznego wynagrodzenia.

Analizujemy pierwszą implementację tej architektury warstwowej.

6.3. Przykładowa aplikacja – wersja 4

6.3.1. Projekt w Visual Studio

Projekt w Visual Studio będzie wyglądał następująco:

  • [1]: folder [Entites] zawiera obiekty wspólne dla wszystkich warstw [ui, metier, dao]: strukturę TrancheImpot, wyjątek FileImpotException.
  • [2]: folder [Dao] zawiera klasy i interfejsy warstwy [dao]. Wykorzystamy dwie implementacje interfejsu IImpotDao: klasę HardwiredImpot omówioną w punkcie 4.10 oraz klasę FileImpot omówioną w punkcie 5.8.
  • [3]: folder [Metier] zawiera klasy i interfejsy warstwy [metier]
  • [4]: folder [Ui] zawiera klasy warstwy [ui]
  • [5]: plik [DataImpot.txt] zawiera przedziały podatkowe wykorzystywane przez implementację FileImpot warstwy [dao]. Plik [6] jest skonfigurowany tak, aby był automatycznie kopiowany do folderu uruchomieniowego projektu.

6.3.2. Elementy aplikacji

Wróćmy do trójwarstwowej architektury naszej aplikacji:

Klasy występujące we wszystkich warstwach nazywamy entités. Zazwyczaj są to klasy i struktury, które hermetyzują dane z warstwy [dao]. Elementy te sięgają zazwyczaj aż do warstwy [ui].

Elementy aplikacji są następujące:

Struktura TrancheImpot


namespace Entites {
    // przedział podatkowy
    public struct TrancheImpot {
        public decimal Limite { get; set; }
        public decimal CoeffR { get; set; }
        public decimal CoeffN { get; set; }
    }
}

L' wyjątek FileImpotException


using System;

namespace Entites {
    public class FileImpotException : Exception {
        // kody błędów
        [Flags]
        public enum CodeErreurs { Acces = 1, Ligne = 2, Champ1 = 4, Champ2 = 8, Champ3 = 16 };

        // kod błędu
        public CodeErreurs Code { get; set; }

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

Uwaga: klasa FileImpotException jest przydatna tylko wtedy, gdy warstwa [dao] jest zaimplementowana przez klasę FileImpot.

6.3.3. Warstwa [dao]

Przypomnijmy interfejs warstwy [dao]:


using Entites;

namespace Dao {
    public interface IImpotDao {
        // przedziały podatkowe
        TrancheImpot[] TranchesImpot{get;}
    }
}

Zaimplementujemy ten interfejs na dwa różne sposoby.

Po pierwsze za pomocą klasy HardwiredImpot omówionej w punkcie 4.10:


using System;
using Entites;

namespace Dao {
    public class HardwiredImpot : IImpotDao {

        // tabele danych niezbędnych do obliczenia podatku
        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 };
        // przedziały podatkowe
        public TrancheImpot[] TranchesImpot { get; private set; }

        // konstruktor
        public HardwiredImpot() {
            // tworzenie tabeli przedziałów podatkowych
            TranchesImpot = new TrancheImpot[limites.Length];
            // wypełnianie
            for (int i = 0; i < TranchesImpot.Length; i++) {
                TranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
            }
        }
    }// klasa
}// przestrzeń nazw
  • wiersz 5: klasa HardwiredImpot implementuje interfejs IImpotDao
  • wiersz 12: implementacja właściwości TranchesImpot interfejsu IImpotDao. Ta właściwość jest właściwością automatyczną. Implementuje ona metodę get właściwości TranchesImpot interfejsu IImpotDao. Ponadto zadeklarowano metodę set jako prywatną, a więc wewnętrzną dla klasy, aby konstruktor z wierszy 15–22 mógł zainicjować tablicę przedziałów podatkowych.

Interfejs IImpotDao zostanie również zaimplementowany przez klasę FileImpot omówioną w paragrafie 5.8:


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

namespace Dao {
    class FileImpot : IImpotDao {

        // plik danych
        public string FileName { get; set; }

        // przedziały podatkowe
        public TrancheImpot[] TranchesImpot { get; private set; }

        // konstruktor
        public FileImpot(string fileName) {
            // zapisujemy nazwę pliku
            FileName = fileName;
            // dane
            List<TrancheImpot> listTranchesImpot = new List<TrancheImpot>();
            int numLigne = 1;
            // wyjątek
            FileImpotException fe = null;
            // odczyt zawartości pliku fileName, wiersz po wierszu
            Regex pattern = new Regex(@"s*:\s*");
            // na początku brak błędów
            FileImpotException.CodeErreurs code = 0;
            try {
                using (StreamReader input = new StreamReader(FileName)) {
                    while (!input.EndOfStream && code == 0) {
                        // bieżący wiersz
                        string ligne = input.ReadLine().Trim();
                        // puste wiersze są pomijane
                        if (ligne == "")
                            continue;
                        // wiersz podzielony na trzy pola oddzielone znakiem:
                        string[] champsLigne = pattern.Split(ligne);
                        // czy mamy 3 pola?
                        if (champsLigne.Length != 3) {
                            code = FileImpotException.CodeErreurs.Ligne;
                        }
                        // konwersje trzech pól
                        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;
                            ;
                        }
                        // błąd?
                        if (code != 0) {
                            // odnotowujemy błąd
                            fe = new FileImpotException(String.Format("Ligne n° {0} incorrecte", numLigne)) { Code = code };
                        } else {
                            // zapisujemy nowy przedział podatkowy
                            listTranchesImpot.Add(new TrancheImpot() { Limite = limite, CoeffR = coeffR, CoeffN = coeffN });
                            // następny wiersz
                            numLigne++;
                        }
                    }
                }
            } catch (Exception e) {
                // odnotowano błąd
                fe = new FileImpotException(String.Format("Erreur lors de la lecture du fichier {0}", FileName), e) { Code = FileImpotException.CodeErreurs.Acces };
            }
            // czy należy zgłosić błąd?
            if (fe != null) {
                // wyzwalany jest wyjątek
                throw fe;
            } else {
                // zwracamy listę listImpot do tablicy tranchesImpot
                TranchesImpot = listTranchesImpot.ToArray();
            }
        }
    }
}
  • kod ten został już omówiony w punkcie 5.8.
  • wiersz 14: metoda TranchesImpot interfejsu IImpotDao
  • wiersz 76: inicjalizacja przedziałów podatkowych w konstruktorze klasy na podstawie pliku, którego nazwę konstruktor otrzymał w wierszu 17.

6.3.4. Warstwa [metier]

Przypomnijmy interfejs tej warstwy:


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

Implementacja tego interfejsu o nazwie ImpotMetier wygląda następująco:


using Entites;
using Dao;

namespace Metier {
    public class ImpotMetier : IImpotMetier {

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

        // przedziały podatkowe
        private TrancheImpot[] tranchesImpot;

        // wykonawca
        public ImpotMetier(IImpotDao dao) {
            // zapisywanie
            Dao = dao;
            // przedziały podatkowe
            tranchesImpot = dao.TranchesImpot;
        }

        // obliczanie podatku
        public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
            // obliczanie liczby udziałów
            decimal nbParts;
            if (marié)
                nbParts = (decimal)nbEnfants / 2 + 2;
            else
                nbParts = (decimal)nbEnfants / 2 + 1;
            if (nbEnfants >= 3)
                nbParts += 0.5M;
            // obliczanie dochodu podlegającego opodatkowaniu i współczynnika rodzinnego
            decimal revenu = 0.72M * salaire;
            decimal QF = revenu / nbParts;
            // obliczenie podatku
            tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
            int i = 0;
            while (QF > tranchesImpot[i].Limite)
                i++;
            // zwrot wyniku
            return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
        }//obliczyć
    }//klasyfikacja

}
  • wiersz 5: klasa [Metier] implementuje interfejs [IImpotMetier].
  • wiersze 14–19: warstwa [metier] musi współpracować z warstwą [dao]. Musi zatem posiadać odwołanie do obiektu implementującego interfejs IImpotDao. Dlatego właśnie to odwołanie jest przekazywane jako parametr do konstruktora.
  • wiersz 16: odwołanie do warstwy [dao] jest zapisywane w polu prywatnym wiersza 8
  • wiersz 18: na podstawie tego odwołania konstruktor pobiera tabelę przedziałów podatkowych i zapisuje do niej odwołanie w prywatnej właściwości wiersza 8.
  • wiersze 22–41: implementacja metody CalculerImpot interfejsu IImpotMetier. Implementacja ta wykorzystuje tabelę przedziałów podatkowych zainicjowaną przez konstruktor.

6.3.5. Warstwa [ui]

Klasy odpowiedzialne za interakcję z użytkownikiem w wersjach 2 i 3 były bardzo podobne. Klasa z wersji 2 wyglądała następująco:


using System;

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

            // utworzenie obiektu IImpot
            IImpot impot = new HardwiredImpot();

            // pętla nieskończona
            while (true) {
...
            }//while
        }
    }
}

a w wersji 3:


using System;

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

            // utworzenie obiektu IImpot
            IImpot impot = null;
            try {
                // utworzenie obiektu IImpot
                impot = new FileImpot("DataImpotInvalide.txt");
            } catch (FileImpotException e) {
                // wyświetlenie błędu
                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);
                // zatrzymanie programu
                Environment.Exit(1);
            }
            // pętla nieskończona
            while (true) {
...
            }//while
        }
    }
}

Zmienia się jedynie sposób instancjonowania obiektu typu IImpot, który umożliwia obliczenie podatku. Obiekt ten odpowiada tutaj naszej warstwie [métier].

W przypadku implementacji [dao] z klasą HardwiredImpot klasa okna dialogowego wygląda następująco:


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

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

            // tworzenie warstw [metier et dao]
            IImpotMetier metier = new ImpotMetier(new HardwiredImpot());

            // pętla nieskończona
            while (true) {
...
                // parametry są poprawne – obliczamy podatek
                Console.WriteLine("Impot=" + metier.CalculerImpot(marié == "o", nbEnfants, salaire) + " euros");
                // kolejny podatnik
            }//while
        }
    }
}
  • wiersz 12: instancjonowanie warstw [dao] i [metier]. Przypominamy, że warstwa [metier] wymaga warstwy [dao].
  • wiersz 18: wykorzystanie warstwy [metier] do obliczenia podatku

W przypadku implementacji [dao] z klasą FileImpot klasa okna dialogowego wygląda następująco:


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

namespace Ui {
    public class Dialogue {
        static void Main() {
...
            // tworzymy warstwy [metier et dao]
            IImpotMetier metier = null;
            try {
        // tworzenie warstwy [metier]
                metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
            } catch (FileImpotException e) {
                // wyświetlenie błędu
                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);
                // zatrzymanie programu
                Environment.Exit(1);
            }
            // pętla nieskończona
            while (true) {
...
                // parametry są poprawne – obliczamy podatek
                Console.WriteLine("Impot=" + metier.CalculerImpot(marié == "o", nbEnfants, salaire) + " euros");
                // kolejny podatnik
            }//while
        }
    }
}
  • wiersze 11–21: instancjonowanie warstw [dao] i [metier]. Ponieważ instancjonowanie warstwy [dao] może spowodować wyjątek, jest on obsługiwany
  • wiersz 26: wykorzystanie warstwy [metier] do obliczenia podatku, tak jak w poprzedniej wersji

6.3.6. Wnioski

Architektura warstwowa i wykorzystanie interfejsów zapewniły naszej aplikacji pewną elastyczność. Widać to zwłaszcza w sposobie, w jaki warstwa [ui] instancjonuje warstwy [dao] i [métier]:


        // tworzymy warstwy [metier et dao]
IImpotMetier metier = new ImpotMetier(new HardwiredImpot());

w jednym przypadku oraz:


// tworzy się warstwy [metier et dao]
            IImpotMetier metier = null;
            try {
        // tworzenie warstwy [metier]
                metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
            } catch (FileImpotException e) {
                // wyświetlenie błędu
                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);
                // zatrzymanie programu
                Environment.Exit(1);
            }

w drugim. Pomijając obsługę wyjątku w przypadku 2, instancjonowanie warstw [dao] i [metier] przebiega podobnie w obu aplikacjach. Po utworzeniu instancji warstw [dao] i [metier] kod warstwy [ui] jest identyczny w obu przypadkach. Wynika to z faktu, że warstwa [métier] jest obsługiwana za pośrednictwem jej interfejsu IImpotMetier, a nie poprzez jej klasę implementacyjną. Zmiana warstwy [metier] lub warstwy [dao] w aplikacji bez zmiany ich interfejsów będzie zawsze sprowadzać się do zmiany wyłącznie poprzednich wierszy w warstwie [ui].

Innym przykładem elastyczności zapewnianej przez tę architekturę jest implementacja warstwy [métier]:


using Entites;
using Dao;

namespace Metier {
    public class ImpotMetier : IImpotMetier {

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

        // przedziały podatkowe
        private TrancheImpot[] tranchesImpot;

        // producent
        public ImpotMetier(IImpotDao dao) {
            // zapis
            Dao = dao;
            // przedziały podatkowe
            tranchesImpot = dao.TranchesImpot;
        }

        // obliczanie podatku
        public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
...
        }//obliczyć
    }//klasa

}

W wierszu 14 widać, że warstwa [métier] jest zbudowana na podstawie odwołania do interfejsu warstwy [dao]. Zmiana implementacji tej ostatniej nie ma zatem żadnego wpływu na warstwę [métier]. Dlatego właśnie nasza jedyna implementacja warstwy [métier] mogła działać bez zmian z dwiema różnymi implementacjami warstwy [dao].

6.4. Przykładowa aplikacja – wersja 5

Ta nowa wersja opiera się na poprzedniej, wprowadzając następujące zmiany:

  • warstwy [métier] i [dao] są każdorazowo zamknięte w warstwie DLL i testowane za pomocą frameworka testów jednostkowych NUnit.
  • integrację warstw zapewnia framework Spring

W dużych projektach nad tym samym projektem pracuje wielu programistów. Architektury warstwowe ułatwiają taki sposób pracy: ponieważ warstwy komunikują się między sobą za pomocą ściśle zdefiniowanych interfejsów, programista pracujący nad daną warstwą nie musi martwić się o pracę innych programistów nad pozostałymi warstwami. Wystarczy tylko, aby wszyscy przestrzegali tych interfejsów.

W powyższym przykładzie programista warstwy [métier] będzie potrzebował podczas testowania swojej warstwy implementacji warstwy [dao]. Dopóki ta implementacja nie zostanie ukończona, może on korzystać z fikcyjnej implementacji warstwy [dao], o ile jest ona zgodna z interfejsem IImpotDao. Jest to również zaleta architektury warstwowej: opóźnienie w warstwie [dao] nie uniemożliwia testowania warstwy [métier]. Fikcyjna implementacja warstwy [dao] ma również tę zaletę, że często jest łatwiejsza do wdrożenia niż prawdziwa warstwa [dao], która może wymagać uruchomienia SGBD, posiadania połączeń sieciowych itp.

Gdy warstwa [dao] zostanie ukończona i przetestowana, zostanie udostępniona programistom warstwy [métier] w postaci pliku DLL, a nie kodu źródłowego. Ostatecznie aplikacja jest często dostarczana w postaci pliku wykonywalnego .exe (należącego do warstwy [ui]) oraz bibliotek klas .dll (pozostałe warstwy).

6.4.1. NUnit

Dotychczasowe testy naszych różnych aplikacji opierały się na weryfikacji wizualnej. Sprawdzaliśmy, czy na ekranie pojawia się to, czego oczekiwaliśmy. Jest to metoda nie do zastosowania, gdy trzeba przeprowadzić wiele testów. Człowiek jest bowiem podatny na zmęczenie, a jego zdolność do sprawdzania testów słabnie w miarę upływu dnia. Testy muszą zatem zostać zautomatyzowane i mieć na celu wyeliminowanie jakiejkolwiek interwencji człowieka.

Aplikacja ewoluuje z biegiem czasu. Przy każdej zmianie należy sprawdzić, czy aplikacja nie uległa „regresji”, c.a.d, oraz czy nadal przechodzi testy poprawności działania, które przeprowadzono podczas jej pierwotnego tworzenia. Testy te nazywane są testami „nie-regresji”. Nieco większa aplikacja może wymagać setek testów. Testuje się bowiem każdą metodę każdej klasy aplikacji. Nazywa się to testami jednostkowymi. Mogą one wymagać zaangażowania wielu programistów, jeśli nie zostały zautomatyzowane.

Opracowano narzędzia służące do automatyzacji testów. Jedno z nich nosi nazwę NUnit. Jest ono dostępne na stronie [http://www.nunit.org]:

Do przygotowania niniejszego dokumentu wykorzystano powyższą wersję 2.4.6 (marzec 2008 r.). Po instalacji na pulpicie pojawia się ikona [1]:

Podwójne kliknięcie ikony [1] uruchamia interfejs graficzny programu NUnit [2]. Nie przyczynia się to w żaden sposób do automatyzacji testów, ponieważ ponownie sprowadza nas to do weryfikacji wizualnej: tester sprawdza wyniki testów wyświetlane w interfejsie graficznym. Niemniej jednak testy mogą być również uruchamiane za pomocą narzędzi wsadowych, a ich wyniki zapisywane w plikach XML. Właśnie ta metoda jest stosowana przez zespoły programistów: testy są uruchamiane w nocy, a programiści otrzymują wyniki następnego dnia rano.

Przyjrzyjmy się na przykładzie zasadzie działania testów NUnit. Najpierw utwórzmy nowy projekt C# typu Console Application:

W pliku [1] widoczne są odniesienia do projektu. Odniesienia te to pliki références zawierające klasy i interfejsy wykorzystywane przez projekt. Te przedstawione w [1] są domyślnie dołączane do każdego nowego projektu w języku C#. Aby móc korzystać z klas i interfejsów frameworka NUnit, musimy dodać [2] nowe odwołanie do projektu.

W powyższej zakładce .NET wybieramy komponent [nunit.framework]. Powyższe komponenty [nunit.*] nie są komponentami domyślnie obecnymi w środowisku .NET. Zostały one tam przeniesione w wyniku wcześniejszej instalacji frameworka NUnit. Po zatwierdzeniu dodania odwołania pojawia się ono jako [4] na liście odwołań projektu.

Przed wygenerowaniem aplikacji folder [bin/Release] projektu jest pusty. Po wygenerowaniu (F6) można zauważyć, że folder [bin/Release] nie jest już pusty:

W folderze [6] widoczne są foldery DLL i [nunit.framework.dll]. To właśnie dodanie odniesienia [nunit.framework] spowodowało skopiowanie tego pliku DLL do folderu uruchomieniowego. Jest to bowiem jeden z katalogów, które zostaną przeszukane przez CLR (Common Language Runtime) oraz NET w celu znalezienia klas i interfejsów, do których odwołuje się projekt.

Stwórzmy pierwszą klasę testową NUnit. W tym celu usuwamy klasę [Program.cs] wygenerowaną domyślnie, a następnie dodajemy do projektu nową klasę [Nunit1.cs]. Usuwamy również zbędne odwołania [7].

Klasa testowa NUnit1 będzie wyglądać następująco:


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");
        }
    }
}
  • wiersz 6: klasa NUnit1 musi być publiczna. Słowo kluczowe public nie jest generowane domyślnie przez Visual Studio. Należy je dodać.
  • wiersz 5: atrybut [TestFixture] jest atrybutem NUnit. Wskazuje on, że klasa jest klasą testową.
  • wiersze 7–9: konstruktor. Jest on tutaj używany wyłącznie do wyświetlenia komunikatu na ekranie. Chcemy zobaczyć, kiedy jest on wykonywany.
  • wiersz 10: atrybut [SetUp] definiuje metodę wykonywaną przed każdym testem jednostkowym.
  • wiersz 14: atrybut [TearDown] definiuje metodę wykonywaną po każdym teście jednostkowym.
  • wiersz 18: atrybut [Test] definiuje metodę testową. Dla każdej metody opatrzonej atrybutem [Test] metoda opatrzona atrybutem [SetUp] zostanie wykonana przed testem, a metoda opatrzona atrybutem [TearDown] zostanie wykonana po teście.
  • wiersz 21: jedna z metod [Assert.*] zdefiniowanych przez framework NUnit. Występują następujące metody [Assert]:
    • [Assert.AreEqual(expression1, expression2)]: sprawdza, czy wartości obu wyrażeń są równe. Akceptowanych jest wiele typów wyrażeń (int, string, float, double, decimal itp.). Jeśli oba wyrażenia nie są równe, zgłaszany jest wyjątek.
    • [Assert.AreEqual(réel1, réel2, delta)]: sprawdza, czy dwie liczby rzeczywiste są równe z dokładnością do delta, c.a.d abs(liczba1-liczba2)<=delta. Można na przykład napisać [Assert.AreEqual(réel1, réel2, 1E-6)], aby sprawdzić, czy dwie wartości są równe z dokładnością do 10⁻⁶.
    • [Assert.AreEqual(expression1, expression2, message)] i [Assert.AreEqual(réel1, réel2, delta, message)] to warianty umożliwiające określenie komunikatu o błędzie, który ma być powiązany z wyjątkiem zgłoszonym w przypadku niepowodzenia metody [Assert.AreEqual].
    • [Assert.IsNotNull(object)] i [Assert.IsNotNull(object, message)]: sprawdzają, czy object nie jest równe null.
    • [Assert.IsNull(object)] i [Assert.IsNull(object, message)]: sprawdzają, czy obiekt jest równy null.
    • [Assert.IsTrue(expression)] i [Assert.IsTrue(expression, message)]: sprawdza, czy wyrażenie jest równe true.
    • [Assert.IsFalse(expression)] i [Assert.IsFalse(expression, message)]: sprawdza, czy wyrażenie jest równe false.
    • [Assert.AreSame(object1, object2)] i [Assert.AreSame(object1, object2, message)]: sprawdza, czy odwołania object1 i object2 wskazują ten sam obiekt.
    • [Assert.AreNotSame(object1, object2)] i [Assert.AreNotSame(object1, object2, message)]: sprawdza, czy odwołania object1 i object2 nie wskazują na ten sam obiekt.
  • wiersz 21: asercja musi zakończyć się powodzeniem
  • wiersz 26: asercja musi zakończyć się niepowodzeniem

Skonfigurujmy projekt tak, aby podczas generowania powstawał plik DLL zamiast pliku wykonywalnego .exe:

  • w [1]: właściwości projektu
  • na [2, 3]: jako typ projektu wybieramy [Class Library] (Biblioteka klas)
  • w [4]: generowanie projektu spowoduje utworzenie pliku DLL (zestawu) o nazwie [Nunit.dll]

Teraz użyjmy pliku NUnit do uruchomienia klasy testowej:

  • w pliku [1]: otwarcie projektu NUnit
  • w [2, 3]: ładujemy plik DLL bin/Release/Nunit.dll wygenerowany podczas tworzenia projektu C#
  • do [4]: plik DLL został załadowany
  • do pliku [5]: drzewo testów
  • w pliku [6]: są one uruchamiane
  • w [7]: wyniki: t1 zakończył się powodzeniem, t2 zakończył się niepowodzeniem
  • w [8]: czerwony pasek wskazuje na ogólną porażkę klasy testów
  • w [9]: komunikat o błędzie związany z nieudanym testem
  • w [11]: różne zakładki okna wyników
  • w [12]: zakładka [Console.Out]. Widać na niej, że:
    • konstruktor został wykonany tylko raz
    • metoda [SetUp] została wykonana przed każdym z dwóch testów
    • metoda [TearDown] została wykonana po każdym z dwóch testów

Można określić metody, które mają zostać przetestowane:

  • w [1]: żądamy wyświetlenia pola wyboru obok każdego testu
  • w metodzie [2]: zaznacza się testy do wykonania
  • w [3]: są one wykonywane

Aby skorygować błędy, wystarczy poprawić projekt C# i ponownie go wygenerować. NUnit wykrywa, że testowany plik DLL został zmieniony i automatycznie ładuje nową wersję. Wystarczy wtedy ponownie uruchomić testy.

Rozważmy następującą nową klasę testową:


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

Począwszy od wersji 2.4 NUnit dostępna jest nowa składnia, widoczna w wierszach 21 i 26. W związku z tym klasa testowa musi dziedziczyć po klasie AssertionHelper (wiersz 6).

Poniżej przedstawiono (niewyczerpujące) zestawienie zgodności między starą a nową składnią:

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)

Dodajmy następujący test do klasy 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));
}

Jeśli wygenerujemy (F6) nowy plik DLL z projektu C#, projekt NUnit przyjmie następujący wygląd:

  • w [1]: nowa klasa testowa [NUnit2] została automatycznie wykryta
  • w [2]: uruchamiany jest test t3 z NUnit2
  • w [3]: test t3 zakończył się powodzeniem

Aby uzyskać więcej informacji na temat NUnit, należy zapoznać się z pomocą dotyczącą NUnit:

6.4.2. Rozwiązanie w Visual Studio

Będziemy stopniowo tworzyć następujące rozwiązanie w Visual Studio:

  • w [1]: rozwiązanie ImpotsV5 składa się z trzech projektów, po jednym dla każdej z trzech warstw aplikacji
  • w [2]: projekt [dao] warstwy [dao]
  • w [3]: projekt [metier] warstwy [metier]
  • w [4]: projekt [ui] z warstwy [ui]

Rozwiązanie ImpotsV5 można skonstruować w następujący sposób:

1
234
5
  • w [1]: utworzyć nowy projekt
  • w [2]: wybierz aplikację konsolową
  • w [3]: wywołaj projekt [dao]
  • w [4]: utworzyć projekt
  • w [5]: po utworzeniu projektu zapisać go
  • w [6]: zachować nazwę [dao] dla projektu
  • w [7]: określić folder, w którym zostanie zapisany projekt i jego rozwiązanie
  • w [8]: nadać nazwę rozwiązaniu
  • w [9]: zaznaczyć, że rozwiązanie musi mieć własny folder
  • w [10]: zapisać projekt i jego rozwiązanie
  • w [11]: projekt [dao] w ramach rozwiązania ImpotsV5
  • w [12]: folder rozwiązania ImpotsV5. Zawiera on folder [dao] z folderu [dao].
  • w [13]: zawartość folderu [dao]
  • do [14]: dodajemy nowy projekt do rozwiązania ImpotsV5
  • w [15]: nowy projekt nosi nazwę [metier]
  • w [16]: rozwiązanie wraz z dwoma projektami
  • w [17]: rozwiązanie po dodaniu trzeciego projektu [ui]
  • w [18]: folder rozwiązania oraz foldery trzech projektów
  • podczas uruchamiania rozwiązania za pomocą (Ctrl+F5) uruchamiany jest projekt aktywny. To samo dotyczy generowania (F6) rozwiązania. Nazwa aktywnego projektu jest pogrubiona jako [19] w rozwiązaniu.
  • w [20]: aby zmienić aktywny projekt w rozwiązaniu
  • w [21]: projekt [metier] jest teraz aktywnym projektem w rozwiązaniu

6.4.3. Warstwa [dao]

Odniesienia do projektu (patrz [1] w projekcie)

Dodajemy odniesienie [nunit.framework] niezbędne do testów [NUnit]

Entities (patrz [2] w projekcie)

Klasa [TrancheImpot] pochodzi z poprzednich wersji. Klasa [FileImpotException] z poprzedniej wersji została przemianowana na [ImpotException], aby uczynić ją bardziej ogólną i nie wiązać jej z konkretną warstwą [dao]:


using System;

namespace Entites {
    public class ImpotException : Exception {

        // kod błędu
        public int Code { get; set; }

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

Warstwa [dao] (por. [3] w projekcie)

Interfejs [IImpotDao] pochodzi z poprzedniej wersji. To samo dotyczy klasy [HardwiredImpot]. Klasa [FileImpot] została zmodyfikowana w celu uwzględnienia zmiany wyjątku z [FileImpotException] na [ImpotException]:


...

namespace Dao {
    public class FileImpot : IImpotDao {

        // kody błędów
        [Flags]
        public enum CodeErreurs { Acces = 1, Ligne = 2, Champ1 = 4, Champ2 = 8, Champ3 = 16 };

...

        // konstruktor
        public FileImpot(string fileName) {
            // zapisywanie nazwy pliku
            FileName = fileName;
...
            // na początku brak błędu
            CodeErreurs code = 0;
            try {
                using (StreamReader input = new StreamReader(FileName)) {
                    while (!input.EndOfStream && code == 0) {
...
                        // błąd?
                        if (code != 0) {
                            // odnotowuje się błąd
                            fe = new ImpotException(String.Format("Ligne n° {0} incorrecte", numLigne)) { Code = (int)code };
                        } else {
...
                        }
                    }
                }
            } catch (Exception e) {
                // odnotowuje się błąd
                fe = new ImpotException(String.Format("Erreur lors de la lecture du fichier {0}", FileName), e) { Code = (int)CodeErreurs.Acces };
            }
            // czy należy zgłosić błąd?
...
        }
    }
}
  • wiersz 8: kody błędów, które wcześniej znajdowały się w klasie [FileImpotException], zostały przeniesione do klasy [FileImpot]. Są to bowiem kody błędów specyficzne dla tej implementacji interfejsu [IImpotDao].
  • wiersze 26 i 34: do enkapsulacji błędu wykorzystywana jest klasa [ImpotException], a nie klasa [FileImpotException].

Test [Test1] (por. [4] w projekcie)

Klasa [Test1] ogranicza się do wyświetlania przedziałów podatkowych na ekranie:


using System;
using Dao;
using Entites;

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

            // tworzymy warstwę [dao]
            IImpotDao dao = null;
            try {
                // tworzenie warstwy [dao]
                dao = new FileImpot("DataImpot.txt");
            } catch (ImpotException e) {
                // wyświetlenie błędu
                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);
                // zatrzymanie programu
                Environment.Exit(1);
            }
            // wyświetlanie przedziałów podatkowych
            TrancheImpot[] tranchesImpot = dao.TranchesImpot;
            foreach (TrancheImpot t in tranchesImpot) {
                Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
            }
        }
    }
}
  • wiersz 13: warstwa [dao] jest zaimplementowana przez klasę [FileImpot]
  • wiersz 14: obsługiwany jest wyjątek typu [ImpotException], który może wystąpić.

Plik [DataImpot.txt], niezbędny do przeprowadzenia testów, jest automatycznie kopiowany do folderu uruchomieniowego projektu (patrz [5] w projekcie). Projekt [dao] będzie zawierał kilka klas z metodą [Main]. Należy zatem wyraźnie wskazać klasę, która ma zostać uruchomiona, gdy użytkownik zażąda uruchomienia projektu za pomocą skrótu Ctrl-F5:

  • w [1]: przejść do właściwości projektu
  • w [2]: określenie, że jest to aplikacja konsolowa
  • w [3]: określenie klasy do wykonania

Wykonanie poprzedniej klasy [Test1] daje następujące wyniki:

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

Test [Test2] (por. [4] w projekcie)

Klasa [Test2] działa tak samo jak klasa [Test1], implementując warstwę [dao] za pomocą klasy [HardwiredImpot]. Wiersz 13 klasy [Test1] zastępuje się następującym:


                dao = new HardwiredImpot();

Projekt został zmodyfikowany tak, aby odtąd uruchamiał klasę [Test2]:

Wyniki wyświetlane na ekranie są takie same jak poprzednio.

Test NUnit [NUnit1] (por. [4] w projekcie)

Test jednostkowy [NUnit1] wygląda następująco:


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

namespace Tests {
    [TestFixture]
    public class NUnit1 : AssertionHelper{
        // warstwa [dao] do przetestowania
        private IImpotDao dao;

        // konstruktor
        public NUnit1() {
            // inicjalizacja warstwy [dao]
            dao = new FileImpot("DataImpot.txt");
        }

        // test
        [Test]
        public void ShowTranchesImpot(){
            // wyświetlanie przedziałów podatkowych
            TrancheImpot[] tranchesImpot = dao.TranchesImpot;
            foreach (TrancheImpot t in tranchesImpot) {
                Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
            }
            // kilka testów
            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));
        }
    }
}
  • klasa testowa wywodzi się z klasy [AssertionHelper], co pozwala na użycie metody statycznej Expect (wiersze 27–30).
  • wiersz 10: odwołanie do warstwy [dao]
  • wiersze 13–16: konstruktor instancjonuje warstwę [dao] przy użyciu klasy [FileImpot]
  • wiersze 19–20: metoda testowa
  • wiersz 22: pobieramy tablicę przedziałów podatkowych z warstwy [dao]
  • wiersze 23–25: wyświetla się je tak jak poprzednio. W rzeczywistym teście jednostkowym wyświetlanie to nie miałoby sensu. Tutaj służy ono celom dydaktycznym.
  • wiersz 27: sprawdzamy, czy rzeczywiście istnieje 7 przedziałów podatkowych
  • wiersze 28–30: sprawdzamy wartości przedziału podatkowego nr 2

Aby wykonać ten test jednostkowy, projekt musi być typu [Class Library]:

  • w [1]: zmieniono charakter projektu
  • na [2]: wygenerowany plik DLL będzie nosił nazwę [ImpotsV5-dao.dll]
  • w [3]: po wygenerowaniu (F6) projektu folder [dao/bin/Release] zawiera pliki DLL i [ImpotsV5-dao.dll]

Następnie plik DLL [ImpotsV5-dao.dll] jest ładowany do frameworka NUnit i uruchamiany:

  • w [1]: testy zakończyły się powodzeniem. Uznajemy zatem warstwę [dao] za gotową do działania. Jej DLL zawiera wszystkie klasy projektu, w tym klasy testowe. Te ostatnie są zbędne. Przebudowujemy warstwę DLL w celu wykluczenia z niej klas testowych.
  • w [2]: folder [tests] został wykluczony z projektu
  • w [3]: nowy projekt. Jest on ponownie generowany przez F6 w celu wygenerowania nowego pliku DLL.

6.4.4. Warstwa [metier]

  • w [1], projekt [metier] stał się aktywnym projektem rozwiązania
  • w [2]: numery referencyjne projektu
  • w [3]: warstwa [metier]
  • w [4]: klasy testowe
  • w [5]: plik [DataImpot.txt] z przedziałami podatkowymi skonfigurowany w [6] w celu automatycznego skopiowania do folderu wykonawczego projektu [7]

Odniesienia do projektu (patrz [2] w projekcie)

Podobnie jak w przypadku projektu [dao], dodaje się odniesienie [nunit.framework] niezbędne do testów [NUnit]. Warstwa [metier] wymaga warstwy [dao]. Potrzebuje zatem odniesienia do warstwy DLL tej warstwy. Postępujemy w następujący sposób:

  • w [1]: dodajemy nowe odniesienie do odniesień projektu [metier]
  • w [2]: wybieramy zakładkę [Browse]
  • w [3]: wybieramy folder [dao/bin/Release]
  • w [4]: należy wybrać plik DLL oraz [ImpotsV5-dao.dll] wygenerowane w projekcie [dao]
  • w [5]: nowy numer referencyjny

Warstwa [metier] (por. [3] w projekcie)

Interfejs [IImpotMetier] pochodzi z poprzedniej wersji. To samo dotyczy klasy [ImpotMetier].

Test [Test1] (por. [4] w projekcie)

Klasa [Test1] ogranicza się do wykonania kilku obliczeń dotyczących wynagrodzenia:


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

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

            // tworzymy warstwę [metier]
            IImpotMetier metier = null;
            try {
                // utworzono warstwę [metier]
                metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
            } catch (ImpotException e) {
                // wyświetlenie błędu
                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);
                // zatrzymanie programu
                Environment.Exit(1);
            }
            // obliczamy kilka podatków
            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)));
        }
    }
}
  • wiersz 14: utworzenie warstw [metier] i [dao]. Warstwa [dao] jest zaimplementowana za pomocą klasy [FileImpot]
  • wiersze 12–21: obsługa ewentualnego wyjątku typu [ImpotException]
  • wiersze 23–26: wielokrotne wywołania jedynej metody CalculerImpot z interfejsu [IImpotMetier].

Projekt [metier] jest skonfigurowany w następujący sposób:

  • [1]: projekt jest typu aplikacja konsolowa
  • [2]: klasą wykonywaną jest klasa [Test1]
  • [3]: w wyniku kompilacji projektu powstanie plik wykonywalny [ImpotsV5-metier.exe]

Uruchomienie projektu daje następujące wyniki:

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

Test [NUnit1] (por. [4] w projekcie)

Klasa testów jednostkowych [NUnit1] wykorzystuje cztery poprzednie obliczenia i weryfikuje ich wyniki:


using Dao;
using Metier;
using NUnit.Framework;

namespace Tests {
    [TestFixture]
    public class NUnit1:AssertionHelper {
        // warstwa [metier] do przetestowania
        private IImpotMetier metier;

        // producent
        public NUnit1() {
            // inicjalizacja warstwy [metier]
            metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
        }

        // test
        [Test]
        public void CalculsImpot(){
            // wyświetlanie przedziałów podatkowych
            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));
        }
    }
}
  • wiersz 14: utworzenie warstw [metier] i [dao]. Warstwa [dao] jest zaimplementowana za pomocą klasy [FileImpot]
  • wiersze 21–24: wielokrotne wywołania jedynej metody CalculerImpot z interfejsu [IImpotMetier] wraz ze sprawdzaniem wyników.

Projekt [metier] jest obecnie skonfigurowany w następujący sposób:

  • [1]: projekt jest typu „biblioteka klas”
  • [2]: wygenerowanie projektu spowoduje utworzenie pliku DLL oraz [ImpotsV5-metier.dll]

Projekt został wygenerowany (F6). Następnie wygenerowany plik DLL, [ImpotsV5-metier.dll] oraz zostaje załadowany do NUnit i przetestowany:

 

Powyższe testy zakończyły się powodzeniem. Uznajemy zatem warstwę [metier] za gotową do użytku. Jej plik DLL zawiera wszystkie klasy projektu, w tym klasy testowe. Te ostatnie są zbędne. Przebudowujemy warstwę DLL w celu wykluczenia z niej klas testowych.

  • do [1]: folder [tests] został wykluczony z projektu
  • w [2]: nowy projekt. Jest on ponownie generowany przez F6 w celu utworzenia nowego pliku DLL.

6.4.5. Warstwa [ui]

  • w [1], projekt [ui] stał się aktywnym projektem rozwiązania
  • w [2]: numery referencyjne projektu
  • w [3]: warstwa [ui]
  • w [4]: plik [DataImpot.txt] dotyczący przedziałów podatkowych, skonfigurowany w [5] tak, aby był automatycznie kopiowany do folderu wykonawczego projektu [6]

Odniesienia do projektu (patrz [2] w projekcie)

Warstwa [ui] potrzebuje warstw [metier] i [dao] do prawidłowego przeprowadzenia obliczeń podatkowych. Wymaga zatem odniesienia do warstw DLL tych dwóch warstw. Postępuje się tak samo, jak pokazano w przypadku warstwy [metier]

Główna klasa [Dialogue.cs] (por. [3] w projekcie)

Klasa [Dialogue.cs] pochodzi z poprzedniej wersji.

Testy

Projekt [ui] jest skonfigurowany w następujący sposób:

  • [1]: projekt jest typu „aplikacja konsolowa”
  • [2]: w wyniku kompilacji projektu powstanie plik wykonywalny [ImpotsV5-ui.exe]
  • [3]: klasa, która zostanie uruchomiona

Przykładowe uruchomienie (Ctrl+F5) wygląda następująco:

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

Wróćmy do kodu w pliku [Dialogue.cs], który tworzy warstwy [dao] i [metier]:


// tworzenie warstw [metier et dao]
            IImpotMetier metier = null;
            try {
        // tworzenie warstwy [metier]
                metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
            } catch (ImpotException e) {
                // wyświetlanie błędu
...
                // zatrzymanie programu
                Environment.Exit(1);
            }

Wiersz 5 tworzy warstwy [dao] i [metier], wyraźnie określając nazwy klas implementacyjnych obu warstw: FileImpot dla warstwy [dao], ImpotMetier dla warstwy [metier]. Jeśli implementacja jednej z warstw zostanie wykonana przy użyciu nowej klasy, wiersz 5 zostanie zmieniony. Na przykład:


                metier = new ImpotMetier(new HardwiredImpot());

Poza tą zmianą nic się nie zmieni w aplikacji, ponieważ każda warstwa komunikuje się z następną za pośrednictwem interfejsu. Dopóki ten ostatni się nie zmienia, komunikacja między warstwami również pozostaje niezmieniona. Framework Spring pozwala nam pójść o krok dalej w kierunku niezależności warstw, umożliwiając nam przeniesienie nazw klas implementujących poszczególne warstwy do pliku konfiguracyjnego. Zmiana implementacji warstwy sprowadza się wówczas do zmiany pliku konfiguracyjnego. Nie ma to żadnego wpływu na kod aplikacji.

W powyższym przykładzie warstwa [ui] zwróci się do Springa z prośbą outworzenie instancji warstw [dao], [1] oraz [metier] i [2] na podstawie informacji zawartych w pliku konfiguracyjnym. Warstwa [ui] zwróci się następnie do Springa [3] z prośbą o odwołanie do warstwy [metier]:


            // tworzenie warstw [metier et dao]
            IImpotMetier metier = null;
            try {
                // kontekst Spring
                IApplicationContext ctx = ContextRegistry.GetContext();
                // żądanie odniesienia do warstwy [metier]
                metier = (IImpotMetier)ctx.GetObject("metier");
            } catch (Exception e1) {
...
}
  • wiersz 5: instancjonowanie warstw [dao] i [metier] przez Spring
  • wiersz 7: pobieramy odwołanie do warstwy [metier]. Należy zauważyć, że warstwa [ui] posiadała to odwołanie bez podania nazwy klasy implementującej warstwę [metier].

Framework Spring występuje w dwóch wersjach: Java i .NET. Wersja .NET jest dostępna pod adresem URL (marzec 2008 r.) [http://www.springframework.net/]:

  • w [1]: strona [Spring.net]
  • w [2]: strona z plikami do pobrania
  • w [3]: pobierz Spring 1.1 (marzec 2008)
  • w [4]: pobierz plik .exe, a następnie zainstaluj go
  • w [5]: folder utworzony podczas instalacji
  • w [6]: folder [bin/net/2.0/release] zawiera pliki DLL biblioteki Spring przeznaczone dla projektów Visual Studio w wersji 2.0 lub nowszej. Spring to bogaty framework. Aspekt Springa, który wykorzystamy tutaj do zarządzania integracją warstw w aplikacji, nazywa się IoC: odwrócenie kontroli (Inversion of Control) lub też DI: wstrzykiwanie zależności (Dependence Injection). Spring udostępnia biblioteki do obsługi baz danych za pomocą NHibernate, generowania i wykorzystywania usług internetowych, aplikacji internetowych itp.
  • DLL niezbędne do zarządzania integracją warstw w aplikacji to DLL, [7] oraz [8].

Te trzy elementy DLL przechowujemy w folderze [lib] naszego projektu:

  • [1]: te trzy pliki DLL umieszczono w folderze [lib] za pomocą Eksploratora Windows
  • [2]: w projekcie [ui] wyświetlamy wszystkie pliki
  • [3]: folder [ui/lib] jest teraz widoczny. Dodajemy go do projektu
  • [4]: folder [ui/lib] stanowi część projektu

Utworzenie folderu [lib] nie jest wcale konieczne. Odwołania można było utworzyć bezpośrednio w trzech plikach DLL znajdujących się w folderze [bin/net/2.0/release] z pliku [Spring.net]. Utworzenie folderu [lib] pozwala jednak na rozwijanie aplikacji na komputerze, na którym nie ma pliku [Spring.net], co zmniejsza jej zależność od dostępnego środowiska programistycznego.

Do projektu [ui] dodajemy odwołania do trzech nowych plików DLL:

  • [1]: tworzymy odniesienia do trzech plików DLL znajdujących się w folderze [lib] [2]
  • [3]: trzy pliki DLL stanowią część odniesień projektu

Wróćmy do ogólnego przeglądu architektury aplikacji:

Powyżej warstwa [ui] zwróci się do Springa z prośbą outworzenie instancji warstw [dao], [1] oraz [metier] i [2] na podstawie informacji zawartych w pliku konfiguracyjnym. Warstwa [ui] zwróci się następnie do Springa [3] z prośbą o odwołanie do warstwy [metier]. W warstwie [ui] przełoży się to na następujący kod:


            // tworzy się warstwy [metier et dao]
            IImpotMetier metier = null;
            try {
                // kontekst Spring
                IApplicationContext ctx = ContextRegistry.GetContext();
                // żądamy odniesienia do warstwy [metier]
                metier = (IImpotMetier)ctx.GetObject("metier");
            } catch (Exception e1) {
...
}
  • wiersz 5: instancjonowanie warstw [dao] i [metier] przez Spring
  • wiersz 7: pobieranie odwołania do warstwy [metier].

Powyższa linia [5] wykorzystuje plik konfiguracyjny [App.config] projektu Visual Studio. W projekcie C# plik ten służy do konfiguracji aplikacji. [App.config] nie jest zatem pojęciem charakterystycznym dla Springa, lecz pojęciem z Visual Studio, z którego korzysta Spring. Spring potrafi korzystać z innych plików konfiguracyjnych niż [App.config]. Przedstawione tutaj rozwiązanie nie jest zatem jedynym dostępnym.

Utwórzmy plik [App.config] za pomocą kreatora Visual Studio:

  • w pliku [1]: dodanie nowego elementu do projektu
  • w pliku [2]: wybierz „Application Configuration File”
  • w [3]: [App.config] to domyślna nazwa tego pliku konfiguracyjnego
  • w [4]: plik [App.config] został dodany do projektu

Zawartość pliku [App.config] jest następująca:


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

[App.config] to plik XML. Konfiguracja projektu odbywa się pomiędzy tagami <configuration>. Konfiguracja wymagana dla pliku Spring jest następująca:


<?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>
  • wiersze 11–23: sekcja ograniczona tagiem <spring> nazywana jest grupą sekcji <spring>. W pliku [App.config] można utworzyć dowolną liczbę grup sekcji.
  • Grupa sekcji zawiera sekcje: tak jest w tym przypadku:
    • wiersze 12–14: sekcja <spring/context>
    • wiersze 15–22: sekcja <spring/objects>
  • wiersze 4–9: region <configSections> definiuje listę handlerów grup sekcji obecnych w pliku [App.config].
  • wiersze 5–8: definiują listę handlerów sekcji z grupy <spring> (name="spring").
  • wiersz 6: moduł obsługi sekcji <context> z grupy <spring>:
    • name: nazwa zarządzanej sekcji
    • type: nazwa klasy zarządzającej sekcją w postaci NomClasse, NomDLL.
    • sekcja <context> grupy <spring> jest zarządzana przez klasę [Spring.Context.Support.ContextHandler], którą można znaleźć w pliku DLL [Spring.Core.dll]
  • wiersz 7: menedżer sekcji <objects> grupy <spring>

Wiersze 4–9 są standardowymi elementami pliku [App.config] z biblioteką Spring. Wystarczy je skopiować z jednego projektu do drugiego.

  • wiersze 12–14: definiują sekcję <spring/context>.
  • wiersz 13: tag <resource> służy do wskazania lokalizacji pliku definiującego klasy, które Spring ma instancjonować. Mogą one znajdować się w pliku [App.config], jak w tym przypadku, ale mogą również znajdować się w innym pliku konfiguracyjnym. Lokalizacja tych klas jest określona w atrybucie uri tagu <resource>:
    • <resource uri="config://spring/objects"> wskazuje, że lista klas do instancjonowania znajduje się w pliku [App.config] (config:), w sekcji //spring/objects, c.a.d, w tagu <objects> należącym do tagu <spring>.
    • <resource uri="file://spring-config.xml"> oznaczałoby, że lista klas do instancjonowania znajduje się w pliku [spring-config.xml]. Plik ten należy umieścić w folderach wykonawczych projektu (bin/Release lub bin/Debug). Najprościej jest umieścić go, tak jak zrobiono to w przypadku pliku [DataImpot.txt], w katalogu głównym projektu wraz z właściwością [Copy to output directory=always].

Wiersze 12–14 są standardowymi elementami pliku [App.config] w środowisku Spring. Wystarczy je skopiować z jednego projektu do drugiego.

  • wiersze 15–22: definiują klasy, które mają zostać zainicjowane. W tej części odbywa się specyficzna konfiguracja aplikacji. Tag <objects> wyznacza sekcję definicji klas, które mają zostać zainicjowane.
  • wiersze 16–18: definiują klasę, która ma zostać zainicjowana dla warstwy [dao]
    • wiersz 16: każdy obiekt instancjonowany przez Spring jest objęty tagiem <object>. Tag ten posiada atrybut name, który jest nazwą instancjonowanego obiektu. To właśnie za jego pośrednictwem aplikacja zwraca się do Springa z prośbą o referencję: „daj mi referencję do obiektu o nazwie dao”. Atrybut „type” określa klasę, której instancja ma zostać utworzona, w postaci NomClasse, NomDLL. Zatem wiersz 16 definiuje obiekt o nazwie „dao”, będący instancją klasy „Dao.FileImpot”, która znajduje się w pakiecie „DLL” „ImpotsV5-dao.dll”. Należy zauważyć, że podaje się pełną nazwę klasy (wraz z przestrzenią nazw), a rozszerzenie .dll nie jest określone w nazwie klasy DLL.

W Springu klasę można instancjonować na dwa sposoby:

  1. za pomocą konkretnego konstruktora, do którego przekazywane są parametry: tak właśnie zrobiono w wierszach 16–18.
  2. poprzez konstruktor domyślny bez parametrów. Obiekt jest wówczas inicjowany za pomocą swoich właściwości publicznych: tag <object> zawiera wówczas podtagi <property> służące do inicjalizacji tych różnych właściwości. Nie mamy tutaj przykładu takiego przypadku.
  • (ciąg dalszy)
    • wiersz 16: instancjonowaną klasą jest klasa FileImpot. Posiada ona następujący konstruktor:

        public FileImpot(string fileName);

Parametry konstruktora są definiowane za pomocą tagów <constructor-arg>.

  • wiersz 17: definiuje pierwszy i jedyny parametr konstruktora. Atrybut index to numer parametru konstruktora, a atrybut value – jego wartość: <constructor-arg index="i" value="valuei"/>
  • wiersze 19–21: definiują klasę, która ma zostać zainicjowana dla warstwy [metier]: klasę [Metier.ImpotMetier], która znajduje się w klasie DLL [ImpotsV5-metier.dll].
    • wiersz 19: instancjonowaną klasą jest klasa ImpotMetier. Posiada ona następujący konstruktor:

        public ImpotMetier(IImpotDao dao);
  • (ciąg dalszy)
    • wiersz 20: definiuje pierwszy i jedyny parametr konstruktora. W powyższym przykładzie parametr konstruktora o nazwie dao jest odwołaniem do obiektu. W tym przypadku w tagu <constructor-arg> używa się atrybutu ref zamiast atrybutu value, który został użyty dla warstwy [dao]: <constructor-arg index="i" ref="refi"/>. W powyższym konstruktorze parametr dao reprezentuje instancję na warstwie [dao]. Instancja ta została zdefiniowana w wierszach 16–18 pliku konfiguracyjnego. Tak więc w wierszu 20:

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

ref="dao" oznacza obiekt Spring „dao” zdefiniowany w wierszach 16–18.

Podsumowując, plik [App.config]:

  • tworzy instancję warstwy [dao] przy użyciu klasy FileImpot, która jako parametr otrzymuje obiekt DataImpot.txt (wiersze 16–18). Powstały obiekt nosi nazwę „dao”
  • tworzy instancję warstwy [metier] za pomocą klasy ImpotMetier, która jako parametr otrzymuje poprzedni obiekt „dao” (wiersze 19–21).

Pozostaje nam tylko wykorzystać ten plik konfiguracyjny Springa w warstwie [ui]. W tym celu duplikujemy klasę [Dialogue.cs], tworząc klasę [Dialogue2.cs], a następnie ustalamy tę ostatnią jako główną klasę projektu [ui]:

  • w [1]: kopia klasy [Dialogue.cs]
  • na [2]: połączenie
  • do [3]: kopia pliku [Dialogue.cs]
  • w [4]: zmiana nazwy na [Dialogue2.cs]
  • w [6]: ustanawiamy [Dialogue2.cs] jako główną klasę projektu [ui].

Poniższy kod z pliku [Dialogue.cs]:


            // tworzy się warstwy [metier et dao]
            IImpotMetier metier = null;
            try {
        // tworzenie warstwy [metier]
                metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
            } catch (ImpotException e) {
                // wyświetlenie błędu
                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);
                // zatrzymanie programu
                Environment.Exit(1);
            }
            // pętla nieskończona
            while (true) {
...

w pliku [Dialogue2.cs] zmienia się na:


            // tworzenie warstw [metier et dao]
            IApplicationContext ctx = null;
            try {
                // kontekst Spring
                ctx = ContextRegistry.GetContext();
            } catch (Exception e1) {
                // wyświetlenie błędu
                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;
                }
                // zatrzymanie programu
                Environment.Exit(1);
            }
            // żądanie odniesienia do warstwy [metier]
            IImpotMetier metier = (IImpotMetier)ctx.GetObject("metier");
            // pętla nieskończona
            while (true) {
....................................
  • wiersz 2: IApplicationContext zapewnia dostęp do wszystkich obiektów instancjonowanych przez Spring. Obiekt ten nazywamy kontekstem Springa aplikacji lub, prościej, kontekstem aplikacji. Na razie kontekst ten nie został zainicjowany. Czyni to następująca po nim instrukcja try/catch.
  • wiersz 5: konfiguracja Springa w pliku [App.config] jest odczytywana i przetwarzana. Po tej operacji, o ile nie wystąpił żaden wyjątek, wszystkie obiekty z sekcji <objects> zostały zainicjowane:
  • obiekt Spring „dao” jest instancją w warstwie [dao]
  • obiekt Spring „metier” jest instancją na warstwie [metier]
  • wiersz 19: klasa [Dialogue2.cs] wymaga odwołania do warstwy [metier]. Odwołanie to jest pobierane z kontekstu aplikacji. Obiekt IApplicationContext zapewnia dostęp do obiektów Springa poprzez ich nazwy (atrybut „name” w tagu <object> konfiguracji Springa). Zwracane odwołanie jest odwołaniem do typu generycznego Object. Konieczne jest przekształcenie zwracanego odwołania na właściwy typ, w tym przypadku typ interfejsu warstwy [metier]: IImpotMetier.

Jeśli wszystko przebiegło pomyślnie, po wierszu 19 klasa [Dialogue2.cs] posiada odwołanie do warstwy [metier]. Kod w wierszach 21 i kolejnych należy do klasy [Dialogue.cs], którą już omówiliśmy.

  • wiersze 6–17: obsługa wyjątku, który występuje, gdy przetwarzanie pliku konfiguracyjnego Springa nie może zostać zakończone. Może to wynikać z różnych przyczyn: nieprawidłowej składni samego pliku konfiguracyjnego lub niemożności utworzenia instancji jednego z skonfigurowanych obiektów. W naszym przykładzie ten ostatni przypadek miałby miejsce, gdyby plik DataImpot.txt z wiersza 17 pliku [App.config] nie został znaleziony w folderze uruchomieniowym projektu.

Wyjątek zgłaszany w wierszu 6 stanowi łańcuch wyjątków, w którym każdy wyjątek ma dwie właściwości:

  • Komunikat: komunikat o błędzie związany z wyjątkiem
  • InnerException: poprzedni wyjątek w łańcuchu wyjątków

Pętla w wierszach 10–14 powoduje wyświetlenie wszystkich wyjątków z łańcucha w postaci: klasy wyjątku i powiązanego komunikatu.

Po uruchomieniu projektu [ui] z poprawnym plikiem konfiguracyjnym otrzymujemy typowe wyniki:

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

Po uruchomieniu projektu [ui] z nieistniejącym plikiem [DataImpotInexistant.txt],


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

otrzymuje się następujące wyniki:

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'.
  • wiersz 17: pierwotny wyjątek typu [FileNotFoundException]
  • wiersz 15: warstwa [dao] hermetyzuje ten wyjątek w typie [Entites.ImpotException]
  • wiersz 9: wyjątek wygenerowany przez Spring, ponieważ nie udało się utworzyć instancji obiektu o nazwie „dao”. W trakcie tworzenia tego obiektu wystąpiły wcześniej dwa inne wyjątki: te z wierszy 11 i 13.
  • Ponieważ nie udało się utworzyć obiektu „dao”, nie można było utworzyć kontekstu aplikacji. Takie jest znaczenie wyjątku z linii 5. Wcześniej wystąpił jeszcze jeden wyjątek, ten z linii 7.
  • Wiersz 3: wyjątek najwyższego poziomu, ostatni w łańcuchu: zgłoszono błąd konfiguracji.

Z tego wszystkiego należy zapamiętać, że to właśnie wyjątek najgłębszy, w tym przypadku ten z linii 17, jest często najbardziej znaczący. Należy jednak zauważyć, że Spring zachował komunikat o błędzie z linii 17, aby przekazać go do wyjątku najwyższego poziomu w linii 3, tak aby uzyskać pierwotną przyczynę błędu na najwyższym poziomie.

Sam Spring zasługuje na osobną książkę. Tutaj jedynie pobieżnie poruszyliśmy ten temat. Można go pogłębić, zapoznając się z dokumentem [spring-net-reference.pdf], który znajduje się w folderze instalacyjnym Springa:

 

Warto również zapoznać się z dokumentem [http://tahe.developpez.com/dotnet/springioc], samouczkiem dotyczącym Springa przedstawionym w kontekście VB.NET.