09 July 2008 3-Schichten, 3-Tier, Architektur, HowTo, Unit Tests Robert Muehsig

Eine 3-schichtige Architektur ist eigentlich ein "Klassiker" in der Softwareentwicklung. Da allerdings das Thema sehr weitläufig ist und Anfänger (und unbelehrbare Entwickler) aus Mangel an Zeit, Lust oder Erfahrung zurückschrecken gibt es genügend Beispiele wo einfach darauf verzichtet wurde.

Um den Grundgedanken zu vermitteln und um zu zeigen, dass es eigentlich sehr einfach ist, sowas am Projektanfang zu implementieren, schreibe ich diesen Artikel

Was für "Schichten" und warum 3?
Fast jede Software greift auf Daten zurück - sei es XML, ein Webservice, eine Datenbank, eine Textdatei oder ein X-beliebiges anderes System.
Diese Daten werden irgendwie verarbeitet - sei es eine mathematische Funktion, eine Validierung oder eine bestimmte Filter und Suchfunktion.
Damit das Ergebnis auch irgendwo angezeigt wird (bzw. die Eingaben entgegen genommen werden) gibt es in den meisten Fällen auch ein Frontend, sei es eine Consolen-Applikation, eine Website oder irgend etwas anderes.

Jetzt wären wir bei den 3-Schichten angekommen:
 image

"Nur 3? Ich hab mehr!"
Natürlich kann man unzählige Schichten noch dazwischen schieben. Ein Beispiel ist z.B. die Software Factory von Microsoft. Da gibt es noch etliche Mappings zwischen den Data-Access-Schichten bis hin zu den Service-Schichten. Siehe auch den Wiki-Artikel zu den Schichtenmodellen.

"Ich frage nur Daten ab - ich brauch nur 2 Schichten."
Über SQL etc. kann man natürlich auch Filtern, Sortieren etc. - da könnte man auch die "Business" Schicht in Frage stellen. Aus meiner Erfahrung sollte man das allerdings lieber nicht machen - später können noch irgendwelche Anforderung dazukommen, die nix im Data Access Layer zu suchen haben. Bis es soweit ist, könnte die Business-Schicht die Daten einfach nur "durchreichen".

Beispielapplikation:
image

Data-Access Layer: "ThreeTier.Data"
Business Layer: "ThreeTier.Service"
Presentation Layer: "ThreeTier.ConsoleApp"
+ Unit-Tests: "ThreeTier.Tests"

Hier haben wir eine recht einfache Beispielapplikation - das ganze noch mit ein paar kleinen Unit-Tests bestückt (Einführung in Unit-Tests hier).

Die Architektur sieht man z.B. auch recht gut in Rob Conerys Storefront Projekt.

Schichten im Detail: ThreeTier.Data
image 
Hier definiere ich erst mal meine Objekte, welche ich im System nutze - simple POCOs.
Zugegeben, man kann sich darüber streiten ob man sein "Model" tatsächlich mit in dem Data-Projekt haben möchte. Da allerdings alles meistens mit irgendwelchen Daten zusammenhängt, passt das schon.
Wir haben hier nur die User Klasse:

image 
Im Ordern "DataAccess" liegt unsere Schnittstellen (Einführung zu Schnittstellen) zu den Datenquellen.
In diesem Fall haben wir nur die Schnittstelle "IUserRepository" (Repository zu dt. sowas wie Lager, Speicherort etc.) - dort definieren wir, welche Operationen ich generell auf eine X-beliebige Datenquelle ich ausführen möchte:
image
Das "DemoUserRepository" ist die konkrete Implementierung dieser Schnittstelle. Da ich keine DB oder ähnliches wollte, werden hier statische Daten zurückgegeben.

Welchen Vorteil bringt mir jetzt das Interface?
Das Interface könnte man hier in Frage stellen, allerdings erlaubt es später recht einfach die Datenquelle zu wechseln - weil alles auf der Schnittstelle beruht.
Da man im Regelfall mit einer DB etc. arbeitet möchte man z.B. in Unit-Tests nicht unbedingt die Datenbank fluten, sondern kann sich hier statische Testdaten zurückgegeben lassen. Einfach durch die Schnittstelle.
So könnte man auch leichter von einem "Showcase" zu einer echten Implementierung umschwenken.

Da ich in meinem Beispiel aber nur statische Daten zurückgebe, habe ich im Unit-Tests genau diese getestet.

Schichten im Detail: ThreeTier.Service

image
Im Service haben wir nach dem gleichen Prinzip auch eine Schnittstelle für unseren "UserService" erstellt.

image

In unserem UserService gibt es einmal eine Login-Methode und eine Methode, welche (ganz im Sinne von Social Networking) die Freunde von einem User zurück gibt. Hierbei habe ich zudem auch nur statische Daten genommen. Das ganze basiert allerdings auf dem "UserRepository".

Schichten im Detail: ThreeTier.ConsoleApp
image

Mal wieder ein Konsolenprogramm - zwar ist das keine schöne Oberfläche, aber für das Beispiel soll es genügen:

static void Main(string[] args)
        {
            Console.WriteLine("Great Social Community System - Please Login...");
            Console.Write("Name: ");
            string loginname = Console.ReadLine();
            Console.Write("PW: ");
            string password = Console.ReadLine();

            IUserService srv = new DemoUserService();

            if (srv.Login(loginname, password))
            {
                Console.WriteLine("Hello: " + loginname);
                Console.WriteLine("Your demo friend collection in the system: ");
                List<User> friends = srv.GetFriendsFromUser(loginname).ToList();

                foreach (User friend in friends)
                {
                    Console.WriteLine(" + " + friend.Login + " - Id: " + friend.Id);
                }
            }
            else
            {
                Console.WriteLine("Login failed");
            }

            Console.ReadLine();
        }

Ausgabe:

image 

Extras: Unit-Tests

Um ein gutes Beispiel zu geben, habe ich sogar 6 Unit-Tests geschrieben. Das Frontend hab ich allerdings nicht getestet ;)
image

Code-Coverage: 97% (Data + Service)
image

Resultat:

Durch die 3-Schichtige Architektur ist es später leichter Möglich neue Features einzubauen und die Applikation zu Warten. Im Team macht sich das auch recht gut, da man dadurch eine bessere Teamaufteilung machen kann.

[ Download Source Code ]

Video:

</param></param></embed>

Written by Robert Muehsig

Software Developer - from Saxony, Germany - working on primedocs.io. Microsoft MVP & Web Geek.
Other Projects: KnowYourStack.com | ExpensiveMeeting | EinKofferVollerReisen.de