Archive for the 'Architektur' Category

ASP.NET: Schichtentrennung – Implementierung

Das Schicke an der Schichtentrennung und den damit verbundenen Konzepten, ist, dass die Implementierung später gegen eine andere Implementierung ausgetauscht werden kann. Genau diesen Ansatz macht man sich zu Nutze, wenn man schnell Ergebnisse erzielen möchte, indem man zunächst eine einfache Implementierung erstellt und eben erst später die echte, große, datenbankgestützte Version bereit stellt.

Das vorausgeschickt, ist die folgende Implementierung nur als ein erster Ansatz zu sehen, den Sie durch eine Version ersetzen können, der Ihren eigenen Anforderungen besser entspricht. Die hier vorgestellte Implementierung nutzt nur den Speicher, um die angelegten Kunden vorzuhalten. Technisch macht sie nix anderes, als eine Liste von Kunden im Speicher zu halten. Einfach, aber für einen Test und einen ersten Prototypen sicherlich ausreichen.

Wichtig: Diese Komponente liegt in einem eigenen Projekt mit dem Namen MemoryCustomerManager (genau so wird der Name der Assemblierung heißen).

Der Code ist ziemlich selbsterklärend – hier werden schließlich nur Operationen auf einer Liste von Kunden vorgenommen. Damit alles später zusammen funktioniert, muss diese Implementierung somit lediglich von der Basisklasse CustomerManager erben und die benötigten Methoden implementieren:

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

namespace MemoryCustomerManager
{
   ///

   /// Implementation of the business layer
   ///

   public class MemoryCustomerManager : CustomerManager
   {

      private static List _customers =
         new List
();

      ///

      /// List of customers
      ///

           
      private static List CustomersList
      {
         get { return _customers; }
      }  

      ///

      /// Returns all customers
      ///

      public override List GetAllCustomers()
      {
         // Sort
         Sort(CustomersList);

         // Return the customers
         return CustomersList;
      }

      ///

      /// Returns a specific customer
      ///

      public override Customer GetCustomer(Guid id)
      {
         // Check all customers
         foreach (Customer cust in CustomersList)
         {
            // Compare the id
            if (cust.Id.Equals(id))
            {
               // Found it!
               return cust;
            }
         }

         // Found nothing
         return null;
      }

      ///

      /// Finds all customers by their names
      ///

      public override List
         FindCustomersByName(string name)
      {
         List
customers =
            new List
();
         string nameLower = name.ToLower();

         // Check every customer
         foreach (Customer cust in CustomersList)
         {
            if (cust.LastName.ToLower()
               .Equals(nameLower))
            {
               customers.Add(cust);
            }
         }

         // Sort the customers
         Sort(customers);

         // Done
         return customers;
      }

      ///

      /// Finds all customers by their email-addresses
      ///

      public override List
         FindCustomersByEMail(string email)
      {
         List
customers =
            new List
();
         string emailLower = email.ToLower();

         // Check every customer
         foreach (Customer cust in CustomersList)
         {
            if (cust.EMail.ToLower().Equals(emailLower))
            {
               customers.Add(cust);
            }
         }

         // Sort the list
         Sort(customers);

         // Return the customers
         return customers;
      }

      ///

      /// Updates a customer
      ///

      public override Customer
         UpdateCustomer(Customer customer)
      {
         // Delete an existing customer
         DeleteCustomer(customer);

         // Add the customer
         CustomersList.Add(customer);

         return customer;
      }

      ///

      /// Deletes a customer
      ///

      public override bool
         DeleteCustomer(Customer customer)
      {
         // Check, whether the customer
         // exists in the list
         Customer existing = null;
         foreach (Customer cust in CustomersList)
         {
            if (cust.Id.Equals(customer.Id))
            {
               existing = cust;
               break;
            }
         }

         // Replace the old customer
         if (null != existing)
         {
            CustomersList.Remove(existing);
            return true;
         }

         return false;
      }

      ///

      /// Sort the customers
      ///

      private void Sort(List customers)
      {
         customers.Sort(new CustomerSorter());
      }
   }
}

Innerhalb der Klasse wird Bezug auf eine Klasse CustomerSorter genommen. Diese hat die Aufgabe, die Liste der Kunden stets alphabetisch sortiert zu halten. Wir implementieren dies mit Hilfe des Interfaces IComparer. Dabei ist lediglich die Methode Compare() zu überschreiben – und diese Überschreibung ist ziemlich simpel, denn tatsächlich geschieht nix anderes, als die Nachnamen der Kunden über deren Funktionalitäten miteinander zu vergleichen und das numerische Ergebnis dieses Vergleiches zurück zu geben. Dabei kann es zu folgenden Rückgaben kommen:

  • Zahl größer als 0: Der Wert der Instanz, auf der verglichen worden ist, ist größer.
  • 0: Beide Werte sind gleich.
  • Zahl kleiner als 0: Der Wert der Instanz, mit der verglichen worden ist, ist größer.

Dementsprechend kann das natürlich auch umgedreht werden, wenn man am Ende des Tages eine absteigende Sortierung wünscht. Wenn die Nachnamen gleich sind, werden die Vornamen miteinander verglichen. Sind auch die gleich, kommen die E-Mail-Adressen an die Reihe.

Lange Rede, kurzer Sinn: Dies ist der Code der CustomerSorter-Klasse:

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

namespace MemoryCustomerManager
{
   public class CustomerSorter : IComparer
   {
      ///

      /// Compares two customers
      ///

      public int Compare(Customer x, Customer y)
      {
         int result = x.LastName.CompareTo(y.LastName);
        
         // Compare the first names when neccessary
         if (result == 0)
         {
            result = x.FirstName.CompareTo(y.FirstName);
         }

         // Compare the emails when neccessary
         if (result == 0)
         {
            result = x.EMail.CompareTo(y.EMail);
         }

         return result;
      }
   }
}

Damit ist die Implementierung komplett. Im nächsten Teil unserer Serie widmen wir uns dann der Nutzung dieser ganzen Komponenten im Web-Umfeld.

Teil 1. Teil 2. Teil 3.

ASP.NET: Schichtentrennung – Fassade

Schichtentrennung lebt vom Verbergen der Implementierung und davon, möglichst wenig an Informationen über das Erzeugen von Instanzen oder Abhängigkeiten nach draußen gelangen zu lassen. Das Grundprinzip sollte stets sein, so wenig wie möglich fest miteinander zu koppeln.

Aus diesem Grund bedienen wir uns einer Fassadenklasse, die das Instanzieren und Benutzen der Implementierung unserer Basisklasse verbirgt. Somit haben wir eine dedizierte Abgrenzung zur Frontendschicht geschaffen, was es uns im weiteren Verlauf des Lebenszyklus eines Projektes erleichtern würde, auch weitreichendere Änderungen zu implementieren (und sei es, die komplette Basisklasse gegen irgend eine andere Implementierung auszutauschen). Also, Fassade davor, und schon kann man auch mal was ändern, ohne das es weh tun muss.

So sieht die Fassadenklasse aus:

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

using de.ksamaschke.Tools;


namespace BusinessLayer
{
   ///

   /// API for handling customers
   ///

   public class Customers
   {

      private static CustomerManager _manager;

      ///

      /// Reference to the
      /// CustomerManager-implementation
      ///

           
      private static CustomerManager Manager
      {
         get
         {
            // Get the manager
            if (null == _manager)
            {
               try
               {
                  _manager = ManagerLoader
                     .Load();
               }
               catch
               {
                  throw;
               }
            }

            return _manager;
         }
      }

      ///

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

      public static List GetAllCustomers()
      {
         return Manager.GetAllCustomers();
      }

      ///

      /// Returns a specific customer
      ///

      public static Customer GetCustomer(Guid id)
      {
         return Manager.GetCustomer(id);
      }

      ///

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

      public static List
         FindCustomersByName(string name)
      {
         return Manager.FindCustomersByName(name);
      }

      ///

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

      public static List
         FindCustomersByEMail(string email)
      {
         return Manager.FindCustomersByEMail(email);
      }

      ///

      /// Updates a customer
      ///

      public static Customer
         UpdateCustomer(Customer customer)
      {
         return Manager.UpdateCustomer(customer);
      }

      ///

      /// Deletes a customer
      ///

      public static bool
         DeleteCustomer(Customer customer)
      {
         return Manager.DeleteCustomer(customer);
      }
   }
}

Diese Fassade ist absichtlich sehr übersichtlich gehalten. Im Wesentlichen stellt sie nur die gleichen Funktionalitäten wie die intern verwendete CustomerManager-Klasse dar. Die konkret verwendete Instanz wird über die Eigenschaft Manager abgerufen, wo sie in Form eines Singletons gehalten wird – somit gibt es nur eine Instanz und die Instanz muss nicht bei jedem Zugriff neu erzeugt werden.

Im nächsten Teil widmen wir uns der konkreten Implementierung einer CustomerManager-Ableitung.

Hier geht es zu Teil 1 und Teil 2.

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.

ASP.NET: Schichtentrennung – Hilfskomponente

So, auf vielfachen Wunsch: Eine kleine Serie zum Thema Schichtentrennung. Bevor wir aber anfangen können, mit Schichten zu arbeiten, benötigen wir einen Mechanismus, der es uns erlaubt, Komponenten anhand von Konfigurationseinstellungen zu laden. Das nutzen wir später, um diverse Entwurfsmuster brauchbar implementieren zu können.

Damit Komponenten geladen werden können, definieren wir zunächst eine Basisklasse BaseManager. Diese Basisklasse verfügt über eine überschreibbare Methode Init(), die von ableitenden Klassen genutzt werden kann, um sich zu initialisieren. Wir lagern also diese spezielle und individuelle Logik auf die einzelnen nachzuladenden Komponenten aus. Die Nutzung dieser Basisklasse ist jedoch optional – Komponenten sollten sich stets auch laden lassen, ohne das diese Basisklasse erweitert wird:

using System;

namespace de.ksamaschke.tools
{
   ///

   /// Base manager implementation
   ///

   public abstract class BaseManager
   {
      ///

      /// Initializes the component
      ///

      public virtual void Init()
      {
         // Intentionally left blank
      }
   }
}

Im nächsten Schritt wird der Lademechanismus definiert. Dieser ist wiederverwendbar gestaltet – er sollte also in jedem Kontext funktionieren können. Somit gibt es hier keine festen Verdrahtungen mit irgendwelchen Klassen, sondern die Instanzen werden per Reflection erzeugt.

Wie aber kommen wir an die benötigten Informationen (Klassenname, Assembly-Name)?

Diese liegen in der Konfigurationsdatei der Applikation (je nach Einsatzzweck entweder die web.config oder die app.config für Client-Applikationen). Der Einfachheit halber tragen wir die Typinformationen im appSettings-Bereich ein (in meinen Projekten gibt es hier üblicherweise einen eigenen Konfigurationsbereich je Komponente, aber das würde hier den Rahmen sprengen). Gefunden werden diese Typinformationen über den Typnamen der beim Aufruf der Methode Load() angegebenen Basiskomponente. Damit schlagen wir gleich zwei Fliegen mit einer Klappe: Das Ding wird wiederverwendbar, da ich keine feste Verdrahtung mit irgendwelchen Klassen habe und wir zwingen uns selbst zum so genannten Contract First-Design, bei dem wir zunächst eine Basisklasse und erst später konkrete Implementierungen bereit stellen.

Der Lademechanismus befindet sich in der Klasse ManagerLoader. Hier werden zunächst die Informationen aus der Konfigurationsdatei ausgelesen und anschließend wird versucht, die Komponente zu instanzieren. War dies erfolgreich, wird geprüft, ob die Komponente von der Basisklasse BaseManager erbt und wenn dem so ist, wird die Init()-Methode eingebunden:

using System;
using System.Configuration;
using System.Runtime.Remoting;

namespace de.ksamaschke.tools
{
   ///

   /// Loads a manager
   ///

   public class ManagerLoader
   {
      ///

      /// Loads a specific manager
      ///

      public static T Load()
      {
         // Get the name of the settings-area
         string settingsName = typeof(T).Name;
        
         // Get the config-value
         string nameAndAssembly =
            ConfigurationManager.AppSettings[
            settingsName];

         // Check the config-value
         if (!String.IsNullOrEmpty(nameAndAssembly))
         {
            // Get the type-name
            string typeName =
               nameAndAssembly.Substring(0,
                  nameAndAssembly.IndexOf(“,”));

            // Get the assembly-name
            string assemblyName =
               nameAndAssembly.Substring(
               nameAndAssembly.IndexOf(“,”) + 1);

            // Try to create an instance
            try
            {
               // Get the instance
               ObjectHandle handle =
                  Activator.CreateInstance(
                     assemblyName, typeName);

               if (null != handle)
               {
                  object result = handle.Unwrap();

                  // Try to initialize
                  if (null != result &&
                      result is BaseManager)
                  {
                     ((BaseManager)result).Init();
                  }

                  if (result is T)
                  {
                     return  (T) result;
                  }
               }
            }
            catch
            {
               // Intentionally left blank
            }
         }
        
         throw new Exception(
            string.Format(
               “Unable to load manager for type {0}”,
               settingsName));
      }
   }
}

Um später mit dieser Klasse Komponenten laden zu können, müssen Sie in der Konfigurationsdatei einen Eintrag anlegen, der dieses Format hat:


  
              value=”Managers.MemoryCustomerManager,
                  CustomerManagerImplementation” />

(Ohne den Zeilenumbruch innerhalb des -Elements)

Damit haben wir die Grundvoraussetzungen geschaffen, um später mit Schichtentrennungen arbeiten zu können. Sie sollten diese Klassen in einem eigenen C#-Projekt ablegen, so dass Sie sie später immer wieder verwenden können.

Zum 2. Teil. Zum 3. Teil.