ASP.NET: Schichtentrennung – Basisklasse und Überlegungen

Bevor wir nun tatsächlich auf das Thema Schichtentrennung intensiver zu sprechen kommen, hier ein paar grundlegende Überlegungen zum Sinn und Zweck von Schichtentrennungen:

Viele Applikationen, leider auch sehr viele Webapplikationen, sind monolithisch aufgebaut. Das bedeutet, dass alles im Frontend stattfindet und die einzelnen Komponenten des Frontends fest miteinander verdrahtet sind. Das führt jedoch zu ein paar grundsätzlichen Problemen:

  • Einzelne Komponenten können nicht gegeneinander ausgetauscht werden
  • Komponenten müssen sehr viel voneinander wissen, um überhaupt sinnvoll interagieren zu können
  • Die Wartbarkeit fällt hinten herunter, denn der selbe Code findet sich an verschiedenen Stellen im Frontend wieder (Redundanz)
  • Änderungen werden zu Geduldsspielen, da die Änderungen an vielen Stellen vorgenommen werden müssen und es Abhängigkeiten gibt
  • Die Testbarkeit einzelner Funktionalitäten ist nicht gegeben

Es gibt potentiell dutzende weiterer Probleme.

Mit all diesen Problemen möchte die Schichtentrennung und die Kapselung von Funktionalitäten aufräumen. Hier geht es in erster Linie einmal darum, verschiedene Ebenen einer Applikation zu identifizieren:

  • Frontend (Webseite, Windows-Client, Fassade, Webdienst, …)
  • Geschäftslogik (Implementierung von Geschäftsprozessen, Verbergung von deren Details vor der Frontend-Logik)
  • Infrastruktur- / Datenlogik (Kapselung des Zugriffs auf Daten)

Dabei gibt es die Regel, dass die einzelnen Schichten stets nur über genau definierte Kommunikationspfade miteinander sprechen:

  • Das Frontend spricht mit der Geschäftslogik
  • Die Geschäftslogik spricht mit der Infrastruktur
  • Die Infrastruktur spricht mit der Geschäftslogik
  • Die Geschäftslogik spricht mit der Infrastruktur
  • Die Infrastruktur spricht nicht direkt mit dem Frontend!
  • Das Frontend spricht nicht direkt mit der Infrastruktur!

Diese genau definierten Kommunikationspfade müssen herausgearbeitet werden. Zu diesem Zweck muss man sich Gedanken über die Abbildung von Geschäftsprozessen (also großflächigeren Vorgängen, etwa einer Benutzerregistrierung samt aller Bedingungen) und Use-Cases (also kleinere, atomarere Schritte, etwa das Abfragen, ob ein Benutzer bereits im System existiert) machen. Geschäftsprozesse bestehen dabei in der Regel aus mehreren Schritten, Use Cases stellen üblicherweise kleinere Schritte dar. Beide können auf Ebene der Geschäftslogik implementiert sein, wobei die Geschäftsprozesse üblicherweise von außen ansprechbar sind und ihrerseits intern die verschiedenen kleinen Schritte (in Methodenform) umsetzen bzw. aufrufen. Als Regel sollte hier gelten: Je weniger ein Client (also eine Komponente, die eine Funktionalität nutzt) von der internen Implementierung einer Funktionalität weiß, desto besser ist es!

Aus diesem Grund wird die Geschäftslogik üblicherweise in Form einer Fassade definiert, die ihrerseits intern die verschiedenen Funktionalitäten (Use Cases) aufruft bzw. auslöst. Diese Fassade sollte aber so gestaltet sein, dass die interne Implementierung auch wieder gelöst werden kann (üblicherweise geschieht dies in Form einer Factory oder eines Providers). Das bedeutet für uns: Es muss eine Basisklasse oder ein Interface implementiert werden, das seinerseits alle notwendigen Funktionalitäten definiert und später implementiert werden kann.

Dies soll im Folgenden anhand einer kleinen Kundenverwaltung demonstriert werden, die ein paar rudimentäre Funktionalitäten (anlegen, ändern, löschen, zurückgeben) definiert. Diese Definition findet anhand einer abstrakten Basisklasse statt. Dieser Entwurfsansatz nennt sich Contract-First-Design, da zunächst der Vertrag (die Basisklasse) definiert wird und erst anschließend die Implementierung dieser Klasse stattfinden kann und muss.

Die Basisklasse sieht dabei wie folgt aus:

using System;
using System.Collections.Generic;
using System.Text;
using de.ksamaschke.Tools;

namespace BusinessLayer
{
   ///

   /// Defines methods for the handling of customers
   ///

   public abstract class CustomerManager : BaseManager
   {
      ///

      /// Returns a list of all customers
      ///

      public abstract List GetAllCustomers();

      ///

      /// Returns a specific customer
      ///

      public abstract Customer GetCustomer(Guid id);

      ///

      /// Returns a list of customers identified
      /// by their names
      ///

      public abstract List
         FindCustomersByName(string name);

      ///

      /// Returns a list of customers identified
      /// by their email-addresses
      ///

      public abstract List
         FindCustomersByEMail(string email);

      ///

      /// Updates a customer
      ///

      public abstract Customer
         UpdateCustomer(Customer customer);

      ///

      /// Deletes a customer
      ///

      public abstract bool
         DeleteCustomer(Customer customer);
   }
}

Verwendet wird hier auch eine Customer-Klasse, die die einzelnen Datensätze repräsentiert:

using System;
using System.Collections.Generic;
using System.Text;

namespace BusinessLayer
{
   public class Customer
   {

      private Guid _id = Guid.NewGuid();

      ///

      /// Id of the customer
      ///

           
      public Guid Id
      {
         get { return _id; }
         set { _id = value; }
      }

      private string _firstname;

      ///

      /// First name of the customer
      ///

           
      public string FirstName
      {
         get { return _firstname; }
         set { _firstname = value; }
      }

      private string _lastname;

      ///

      /// Last name of the customer
      ///

           
      public string LastName
      {
         get { return _lastname; }
         set { _lastname = value; }
      }

      private string _email;

      ///

      /// E-Mail-Address of the customer
      ///

           
      public string EMail
      {
         get { return _email; }
         set { _email = value; }
      }  
   }
}

Im nächsten Teil der Serie zeige ich dann, wie die Fassadenklasse aufgebaut und implementiert sein kann. Erst danach widmen wir uns einer konkreten Implementierung der obigen Basisklasse und dem Einsatz in einem Beispielprojekt.

Zum 1. Teil. Zum 3. Teil.

No Comment

Comments are closed.