Inhaltsverzeichnis         

1 Design Patterns

1.1 Quellen

  1. Gute Einführung mit Beispielen in C# hier

  2. Viel zur Historie und Besipiele in verschiedenen Sprachen gibt’s hier

  3. Eine interaktive Einführung mit lustigen Beispielen gibt’s hier

  4. Helmut Balzert: Lehrbuch der Software- Technik, Bd. I, 2. Auflage; Spektrum Akademischer Verlag Heildelberg – Berlin 2000

1.2 Einführung

Design Patterns oder dt. Entwurfsmuster beschreiben eine Methodik aus dem Softwareentwurf.

Phase

1

2

3

4

5

6

Beschreibung

Planung

Definition

Entwurf

Implementierung

Abnahme

Wartung



Ein Entwurfsmuster ist ein generische Lösung für ein allgemeines Entwurfsproblem, das sich praktisch bewährt hat. Der Einsatz von Entwurfsmustern hat folgende Vor- und Nachteile:

Vorteile

Nachteile

  1. beschreiben grundlegende Entwürfe

  2. fördern die Wiederverwendung von bewährten Lösungen

  3. bilden ein einheitliches Vokabular für ein Entwicklerteam im Entwurfsprozess (unabhängig von einer Programmiersparache)

  1. kein Garant für gutes Design

  2. können optimale Lösungen für spezielle Probleme verdrängen

1.2.1 Struktur einer Musterdefinition

Die Definition von Entwurfsmustern haben folgendes Schema:


Beispiel

Typ

Erzeugungsmuster

Name

Fabrikmethode

Problembeschreibung

Objekte sind zu instanziieren, wobei zum Entwurfszeitpunkt nur die Basisklasse der Objekte bekannt ist. Gewünscht ist ein bezüglich des Typs der Objekte offener Entwurf. Einzige Einschränkung ist, das die Typen der Objekte von der Basisklasse abgeleitet sein müssen. new Basisklasse schränkt die Instaziierung auf Objekte vom Typ Basisklasse ein, und scheidet als Lösung aus.

ges: T o = _App.create() mit T erbt von B

Lösungsbeschreibung

App ist eine Basisklasse, welche die Anwendung darstellt. Sie besitzt eine virtuelle Instanz- methode create, die ein Objekt vom Typ der Basisklasse T zurückgibt.

Ableitungen von App überschreiben die Methode create. In create werden Objekte instanziiert und zurückgegeben, deren Typ von der Basisklasse abgeleitet ist.

class Basis {…}

class App {
 public virtual Basis create();
}

class My : Basis {…}

class MyApp : App {
  public overrides Basis create {
     return new My();
  }
}

Konsequenzen

  • Grundlegender Aufbau einer Anwendung, die Ergebnisse, Meldungen etc. generiert, kann durch eine Basisklasse App beschrieben werden, ohne die Vielfalt der Strukturen von Ergebnissen, Meldungen etc. einzuschränken.

1.3 Historie



1964

Christopher Alexander: Sammlung von Muster/Vorlagen für Architekturentwürfe (Bauwesen)

1987

Kent Beck und Ward Cunnigham übertragen die Idee der Entwurfsmuster aus der Architektur in die Softwareentwicklung graphischer Benutzeroberflächen mit Smalltalk.

1988

Erich Gamma verallgemeinert den Ansatz von Beck und Cunnigham in einer Promotion an der Uni Zürich auf den gesamten Softwareentwurf

1994

Erich Gamma, Richard Helm, Ralph E. Johnson und John Vlissides veröffentlichen einen allgemeinen Musterkatalog in dem Buch Design Patterns: Elements of Reusable Object-Oriented Software. Das Werk verhalf dem Einsatz von Entwurfsmustern zum Durchbruch. In der Folge erhielten die vier Autoren den Spitznamen Gang of Four (GoF). Es wird auch von GoF- Entwürfen geprochen.





1.4 Klassifizierung

Erzeugungsmuster

Strukturmuster

Verhaltensmuster

Beschreiben Techniken und Organisationsformen der Instanziierung, durch welche Programme von Konfigurationsdetails unabhängig werden und Rechenzeit sparen.

Beschreiben Strukturen aus Klassenbibliotheken, die Wiederverwendbarkeit erhöhen, Komplexität mindern und die Implementierung der Geschäftslogik vereinfachen

Beschreiben Schnittstellen und Klassen, mit denen eine übersichtliche und erweiterbare Implementierung eines gewünschten Laufzeitverhaltens möglich wird.

  1. Abstract Factory

  2. Builder

  3. Factory Method

  4. Prototype

  5. Singleton

  1. Adapter

  2. Composite

  3. Bridge

  4. Decorator

  5. Facade

  6. Flyweight

  7. Proxy

  1. Chain of Responsibility

  2. Command

  3. Interpreter

  4. Iterator

  5. Mediator

  6. Memento

  7. Null Object

  8. Observer

  9. State

  10. Strategy

  11. Template Method

  12. Visitor





1.5 Erzeugungsmuster

1.5.1 Fabrikmethode

Problem

Objekte sind zu instanziieren, wobei zum Entwurfszeitpunkt nur die Basisklasse der Objekte bekannt ist. Gewünscht ist ein bezüglich des Typs der Objekte offener Entwurf. Einzige Einschränkung ist, das die Typen der Objekte von der Basisklasse abgeleitet sein müssen. new Basisklasse schränkt die Instaziierung auf Objekte vom Typ Basisklasse ein, und scheidet als Lösung aus.

ges: T o = _App.create() mit T erbt von B

Lösungs

App ist eine Basisklasse, welche die Anwendung darstellt. Sie besitzt eine virtuelle Instanz- methode create, die ein Objekt vom Typ der Basisklasse T zurückgibt.

Ableitungen von App überschreiben die Methode create. In create werden Objekte instanziiert und zurückgegeben, deren Typ von der Basisklasse abgeleitet ist.

class Basis {…}

class App {
 public virtual Basis create();
}

class My : Basis {…}

class MyApp : App {
  public overrides Basis create {
     return new My();
  }
}

Konsequenzen

  • Grundlegender Aufbau einer Anwendung, die Ergebnisse, Meldungen etc. generiert, kann durch eine Basisklasse App beschrieben werden, ohne die Vielfalt der Strukturen von Ergebnissen, Meldungen etc. einzuschränken.

1.5.1.1 Beispiel: DirTree.MakeProgressInfo

Beim Verzeichnis- scann wird regelmäßig ein Event gefeuert, das über den Prozessfortschritt informiert. Der Prozessfortschritt wird durch die Fabrikmethode MakeProgressInfo (=create) in der abstrakten Basisklasse DirTree (=App) bereitgestellt.

In den abgeleiteten Klassen kann die Prozessfortschrittsinformation ausgestaltet werden, indem von der Basisklasse DMS.ProgressInfo abgeleitet wird.

Ableitungen des Verzeichnisscanners DirTree können nun die Fabrikmethode MakeProgressInfo überladen. Sie erzeugen Objekt, deren Typ von DMS.ProgressInfo abgeleitet ist.

Ein Klient kann Prozessfortschrittsinformationen verarbeiten, ohne sich zwangsläufig von einer konkreten Implementierung für DirTree abhängig zu machen.

1.5.2 Abstrakte Fabrik

Problem

In der Implementierung eines Programmes werden werden Methoden von Instanzen aufgerufen. Um das Programm an verschiedene Systemkonfigurationen anzupassen, sollen die Methodenimplementierungen/Instanzen austauschbar sein.

// Konfig 1
void Programm() {

   // Konfigurationsspezifisch
   var A = new TA_1();
   var B = new TB_1();

   // Invariant
   A.method();
   B.method();
}

// Konfig 2
void Programm() {

   // Konfigurationsspezifisch
   var A = new TA_2();
   var B = new TB_2();

   // Invariant
   A.method();
   B.method();
}

Lösung

Die im Programm aufgerufenen Instanz- Methoden werden als Member abstrakter Basisklassen definiert. Die Methodenaufrufe erfolgen im Programm über Referenzen vom Typ der abstrakten Basisklassen. Die Instanziierung wird in den Methoden von sog. Fabrikinstanzen gekapselt. Die Typen der Fabrikinstanzen sind von einer abstrakten Basisklasse, der sog. Abstrakten Fabrik abgeleitet. Dem Programm wird beim Start die eine Referenz vom Typ Abstrakte Fabrik übergeben. Über diese werden die Fabrikmethoden aufgerufen.

abstract class TA {…}
abstract class TB {…}

class AbstrakteFabrik {
   public abstract TA createTA();
   public abstract TB createTB();   
}

class Konfig1Fabrik : AbstrakteFabrik {
    
   public class TA_1 : TA {…}
    
   public override TA createTA() {
     return new TA_1();
   }
   …
}

class Konfig2Fabrik : AbstrakteFabrik {…}

// Bezüglich der Konfigurationen invariante Programmstruktur
void Programm(AbstrakteFabrik fab) {

   var A = fab.createTA();
   var B = fab.createTB();

   A.method();
   B.method();
}

Konsequenzen

  • Ermöglicht die Isolation von Konfigurationsdetails von der Programmlogik



1.5.2.1 Beispiel: System.Data.Common.DbProviderFactory

Datenbankzugriffslogik kann unabhängig von einem speziellen Datenbankserversystem geschrieben werden.

1.5.3 Builder

Problem

Das Datenformat, in dem komplex strukturierte Ergebnisse eines Programmes ausgegeben werden sollen, steht zur Entwurfszeit nicht fest. Mehrere Ausgabeformate sind möglich. Die Menge der Ausgabeformate soll erweiterbar sein.

Lösung

Es wird eine Schnittstelle, genannt Builder definiert, die alle Methoden definiert, mit denen einzelne Teile der komplexen Ergebnisstruktur generiert werden. Z.B.

interface IBuilder {
  void open();
  void createHead(…);
  void createBody(...);
  void createFooter(...);
  void close();
}



Dem Programm wird eine Referenz auf einen IBuilder übergeben. Das Programm erzeugt mit den über die Schnittstellenreferenz aufrufbaren Methoden das Ergebnis.

void Programm(IBuilder bld) {
  // Berechne Ergebnis// Ausgabe
  bld.open();bld.createHeader(...);bld.close();
  …
}

Für jedes gewünschte Datenformat wird die Schnittstelle IBuilder durch eine Klasse implementiert.

class XmlBuilder : IBuilder {…}
class NetworkStreamBuilder : IBuilder {…}

Konsequenz

  • Bezüglich der Ausgabe sehr flexibele Entwürfe

1.5.3.1 Beispiel: FileClassificatior.IContentVectorWriter

Der FileClassifikatorServer scannt ein Dateiverzeichnis. Die Dateien im Verzeichnis werden dabei klassifiziert, und es wird bezüglich der Klassen eine Statistik erzeugt (wie viel Dateien der Klasse A sind im Unterverzeichnis X enthalten). Die Statistik kann in verschiedenen Formaten ausgegeben werden. Dies wird mittels des Builder- Patterns durch die Schnittstelle IContentVectorWriter implementiert.

1.5.4 Prototyp

Problem

Die Instanziierung von Objekten ist rechenintensiv (z.B. Inhalte müssen aus normalisierter Datenbank geladen werden). Dabei unterscheiden sich die Objekte untereinander nur marginal.

Wenn ein Anwender zu Laufzeit interaktiv eine Vorlage für eine Familie von Dokumenten (z.B. Geschäftsbrief), dann liegt aus der Entwurfsperspektive die gleiche Problematik vor.

Lösung

Es wird eine Schnittstelle definiert, die eine Kopierfunktion enthält, welche eine vollständige Kopie eines Objektes zurückgibt. Die Klasse des Prototypen- Objektes muss diese Schnittstelle implementieren.

interface IClonable {
   object clone();
}

class Prototype : IClonable {…}

Zum Programmstart wird das Prototypen Objekt instanziiert. Alle weiteren Objekte entstehen durch kopieren des Prototypenobjekts und anschließendem überschreiben der Eigenschaften, die das neuerstellte Objet vom Prototypen unterscheiden.

Programm {

   var _prototype = new Prototype();

   var obj1 = _prototype.clone();
   obj1.PropX = …;

   var obj2 = _prototype.clone();
   obj2.PropX = …;

}


Konsequenz

  • Beschleunigte Instanziierung komplexer Objekte

  • Einfache Implementierung von Dokumentvorlagen



1.5.5 Singleton

Problem

Ein komplexes Objekt darf in einem Programm nur einmal instanziiert werden. Oft muss diese Objekt zudem noch global verfügbar sein.

Lösung

In der Klassendeklaration wird ein öffentlicher statischer Member vom Typ selbst angelegt. Der statische Konstruktor instanziiert das globale Objekt und speichert die Referenz im statischen Member der Klasse. Im Programm kann nun über den statischen Klassenmember auf die Instanz zugegriffen werden.

class Singleton {

  public static Singleton Instance = new Singleton();

  private Singleton() {…}
}

Konsequenz

  • Zugriffskontrolle auf die Member des Singletons (nur public Member sind sichtbar)

  • Singleton- Klasse kann durch Ableitungen spezialisiert werden.

  • Werden in der Weiterentwicklung des Entwurfs wieder mehrere Instanzen benötigt, dann kann dies leicht realisiert werden



1.5.5.1 Beispiel: Mko.DBEnum

In Datenbanken können Wertebereiche von Tabellenspalten durch Domäne- Tabellen und Beziehungen ausgedrückt werden. Mittels des DBEnum können diese Wertebereiche zu Beginn aus der DB geladen, und kohärent im Programm zur Auswertung der Datensätze genutzt werden.

1.6 Strukturmuster

1.6.1 Adapter (Wrapper- Klasse)

Problem

In einem Programm soll die Klassenbibliothek eines Lieferanten eingesetzt werden. Die Bibliothek soll/muss jedoch in Zukunft durch die eines anderen Lieferanten austauschbar sein.

Lösung

Die vom Programm benötigten Methoden aus der Klassenbibliothek werden in Schnittstellen erfasst. Im Programm werden die Methoden über Referenzen vom Typ der Schnittstellen aufgerufen. Die Schnittstellen werden durch Adapterklassen mittels der Methode einer privaten Instanz der jeweiligen Lieferantenklasse.

interface IAdapterX {
  T methodeA(...);
}

class AdapterX : IAdapterX{
 
  LieferantX x = new LieferantX();

  T methodA(...) {
    return x.methodA(...);
  }
}

Konsequenz

  • Der Zugriff auf das Produkt eines Lieferanten ist in der Adapterbibliothek gekapselt



1.6.2 Brücke (Bridge)

Problem

Eine Klasse soll ein Verfahren/System allgemein implementieren. In Ableitungen soll die allgemeine Implementierung verfeinert werden, wobei die Unabhängigkeit von irgendwelchen konkreten Umgebungen erhalten bleiben soll.

Um diese Klassenbibliothek in einem Programm in einer realen Umgebung zu nutzen, müssen die in der allgemeinen Implementierung verwendeten Operationen durch Operationen aus der realen Welt ausgetauscht werden. Ein überschreiben der Operationen in abgeleiteten Klassen ist jedoch nicht möglich, da sonst die gewünschte Abstraktion in der Klassenhierarchie verloren geht.

Lösung

Die Operationen, auf denen die allgemeingültigen Verfahren in der Klassenhierarchie basieren, werden von einer Instanz bereitgestellt. Diese implementiert eine Schnittstelle

// Operationen, auf denen Allgemeine Implementierung basiert
interface IImplementor {
   T opeation1(...);
   T operation11(...);
   T operation2(...);
   …
}

// Allgemeingültige Klassenhierarchie
class BasisAbstraction {
   IImplementor imp;
   public BaseAbstraction(IImplementor imp) {
     this.imp = imp;
   }
   …
   public virtual T method1(...) {
      …
      imp.operationA(...);
      …
   }
   …
}

class Verfeinerung : BaseAbstraction {
   public Verfeinerung(IImplementor imp): base(imp) {}
   …
   public override T method1(...) {
     …
     // anstatt operation1 wird jetzt operation11 verwendet
     imp.operation11(...);
     …
   }
}

class RealisierungA : IImplementor {…}

class RealisierungB : IImplementor {…}


ProgrammA {
 
    // Arbeiten in einer Umgebung A
    var basis = new BasisAbstraction(new RealisierungA());
    basis.method1(...);

    var verfahren = new Verfeinerung(new RealisierungA());
    verfahren.method1(...);
}

ProgrammB {

    // Arbeiten in einer Umgebung B
    var basis = new BasisAbstraction(new RealisierungB());
    basis.method1(...);

    var verfahren = new Verfeinerung(new RealisierungB());
    verfahren.method1(...);
}

Konsequenz

  • Das Anwenden von Operationen kann unabhängig von der Implementierung der Operationen verfeinert werden

1.6.2.1 Beispiel: BatchProcessing und IWorker

Die Klasse BatchProcessing ist eine allgemeine Implementierung für einen Stapelverarbeitungsprozessor. Dabei werden Aufträge in eine Warteschlange eingestellt und sequentiell verarbeitet. In einer von BatchProcessing abgeleiteten Klasse könnte die Stapelverarbeitung durch eine Priorisierung der Aufträge verfeinert werden.

Die Adaption eines BatchProcessing- Objektes in eine Programmumgebung (siehe FileClassificatorClient) erfolgt durch Objekte, die die Schnittstelle IWorker implementieren.

1.6.3 Composite

Problem

Objekte bilden eine Hierarchie. In dieser sind einige der Objekte Knoten, die Kindknoten haben. Andere wiederum sind Blattknoten. Die Objekte sind dabei von unterschiedlichem Typ.

Lösung

Die Typen aller Objekte erben von einer gemeinsamen Basisklasse. Diese definiert eine Schnittstelle, mit der ein traversieren einfach möglich ist:

class Base {
   public virtual int ChildCount {get;}
   public virtual Base getChild(int i);
}

class Blatt : Base {

   public override int ChildCount {
     get { return 1;}
   }

   public override Base getChild(int i){
      return null;
   }
  …
}

class Composit : Base {
   
   List<Base> _childs = new List<Base>();
    
   public override int ChildCount {
     get { return _childs.Count;}
   }

   public override Base getChild(int i){
      return _childs[i];
   }
  …
}

Konsequenz

  • heterogene Objekthierarchie, die einfach traversierbar ist.

1.6.3.1 Beispiel: Hierarchie der Controls in einer Webform

1.6.4 Decorator

Problem

Die Objekte einer Menge haben im wesentlichen einen gemeinsamen Aufbau. Jedoch gibt es eine Vielzahl an Varianten, die durch eine Kombination zusätzlicher Eigenschaften entstehen. Zum Entwurfszeitpunkt sind entweder nicht alle sinnvollen Kombinationen isolierbar, oder die Anzahl der Kombinationen ist sehr groß. Die Implementierung durch Ableiten von einer Basisklasse führt zu großen und unübersichtlichen Klassenhierarchien.

Lösung

Die Grundstruktur der Objekte wird durch eine Schnittstelle definiert. Die Schnittstelle wird zum einen durch Klassen implementiert, deren Instanzen die Objekte ohne Kombination von Zusatzeigenschaften darstellen.

interface ICore {
  T coremethod1(...);
  …
}

class A : ICore {…}
class B : ICore {…}

Die kombinierbaren Zusatzeigenschaften werden durch Ableitungen von der Klasse IDeco implementiert, welche ebenfalls die ICore Schnittstelle implementiert:

class IDeco : ICore {
   // Verweis auf das Objekt, welches durch eine Kombination von 
   // Zusatzeigenschaften erweitert wird
   protected ICore coreInstance;

   T extraMethod1(...);
   ... 

}
class Extra1 : IDeco {
   public Extra1(ICore baseObject) {
      coreInstance = baseObject;
   }
   ...
}

class Extra2 : IDeco {
   public Extra1(ICore baseObject) {
      coreInstance = baseObject;
   }
   ...
}

Im Programm kann eine Instanz einer Grundstruktur mittels Dekoratoren erweitert werden wie folgt:

Programm {
   var a = new A();
   a.coremethod1(...);

   var aExtra1 = new Extra1(a);
   aExtra1.coremethod1(...);
   aExtra1.extraMethod1(...);
}

Konsequenz

  • Mit einer überschaubaren Zahl von Klassen kann eine beliebige Vielzahl an Objekttypen zur Laufzeit generiert werden



1.7 Verhaltensmuster

1.7.1 Template Method

Problem

Ein Algorithmus wird implementiert durch eine Folge von Teilschritten. Zwecks Anpassung an verschiedene Aufgabenstellungen soll die Implementierung aller Teilschritte austauschbar sein.

Lösung

In einer abstrakten Klasse wird der Algorithmus in einer Methode, genannt Template Method, definiert. Dabei wird jeder Teilschritt durch den Aufruf einer abstrakten Methode in der Template Method implementiert.

abstract class Base {

  T TemplateMethod(...) {
    step1(...);
    step2(...);
    …
    stepN(...);
  }

  public abstract void step1(...);
  public abstract void step2(...);
  …
  public abstract void stepN(...);
}

In den Ableitungen der abstrakten Klasse werden die abstrakten Methoden für die Teilschritte überschrieben. Verschiedene Ableitungen kapseln so verschiedene Anwendungen des Algorithmus:

class AppAlgo1 : Base {
  public override void step1(...) {
    // Implementierung von Teilschritt 1
    …
  }
  …
}

Programm {
 
   // Algorithmus durchführen
   var App = new AppAlgo1();
   App.TemplateMethod(...);
  
}

Konsequenz

  • Ein Allgemeines Verfahren kann für verschiedene Anwendungen modular angepasst werden

1.7.1.1 Beispiel: DMS.DirTree- Allgemeine Implementierung eines Verzeichnisscanners

1.7.2 Strategie

Problem

In einem komplexen Programm gibt es isolierbare Berechnungen. Abhängig von externen Anforderungen (Konfiguration, Versuchsbedingungen, Kulturkontext) müssen verschiedene Varianten der Berechnung durchgeführt werden. Die Berechnungen sollten austauschbar sein.

Lösung

Das Programm, in dem die austauschbaren Berechnungen enthalten sind, wird Kontext genannt. Die Berechnungen selbst werden in Klassen ausgelagert. Jede Berechnungsvariante wird in einer eignen Klasse gekapselt. Diese Klassen implementieren eine Schnittstelle, die IStrategie genannt wird. Die Schnittstelle definiert den Einsprungpunkt für die Berechnungen.

interface IStrategie {
   T callAlgomrithmus(...);
}

class Algo1 : IStrategie {…}
class Algo2 : IStragegie {…}

Aus dem Kontext werden die Berechnungen über Referenz vom Typ IStrategie aufgerufen. Indem diese Referenz gegen die von einer anderen Berechnungsinstanz ausgetauscht wird, erfolgt die Anpassung an eine neue Strategie:

class Kontext {

   public  IStratiegie startegie;

   void exe(...) {
     strategie.callAlgorithmus(...);
   }
}

Programm {
   var ctx = new Kontext();
   ctx.strategie = new Algo1();
   ctx.exe(...);
   
   ctx.strategie = new Algo2();
   ctx.exe(...);

}

Konsequenz

  • Familien von Algorithmen können modular bereitgestellt werden (Jeder Algorithmus eine Klasse)

  • Anpassbarkeit zur Laufzeit

1.7.2.1 Beispiel: Klassifizierung von Dateien in DMS.FC.FileClassificator

Die Klassifikation von Dateien beim Verzeichnisdurchlauf kann nach verschiedenen Regeln erfolgen. Die Kapselung dieser in verschiedene Klassen erfolgt nach dem Strategie- Muster. Die Strategie wird durch die Schnittstelle IFileClassificator bereitgestellt:

public interface IFileClassificator
{
  bool classify(string Filename, out ContentVector vec);        
}

Der Kontext ist der Verzeichnisscanner selbst:

public class FileClassificatorServer: DMS.DirTree
{

  // Objekt, über das die Ausgabe der Ergebnisse erfolgt
  IContenVectorWriter _writer;

  // Referenz auf das aktuell verwendete Strategie- Objekt
  IFileClassificator _classificator;

  …
}

1.7.3 Iterator

Problem

In einer Klassenbibliothek soll ein allgemeines Verfahren zum Iterieren über Objektmengen angeboten werden.

Lösung

Die grundlegenden Methoden zur Iteration werden in der Schnittstelle Iterator definiert:

interface Iterator {
  // false, wenn am Ende, true sonst
  bool MoveNext();

  // false, wenn am Anfang, true sonst
  MovePred();

  // Positionierung auf das erste Element
  Reset();  

  // das aktuelle Objekt zurückgeben
  object Get();  
}

Die Schnittstelle wird von den Containerklassen der Anwendung implementiert

class List<T> : Iterator {...}

Konsequenz

  • Ist auf alle Objektmengen anwendbar

  • Nachteil sind unter Umständen erhöhte Laufzeit und Speicherkosten



1.7.3.1 Beispiel: IEnumerator aus System.Collections und C#

1.7.4 Visitor

Problem

Gegeben sind Menge von Objekte mit fixer Struktur.

M ::= {obji : obji vom Typ i (i= 1..n)}

Auf dieser Menge sind Operatoren zu implementieren. Die Menge der Operatoren soll erweiterbar sein, ohne dabei die Struktur der Objekte anpassen zu müssen.

Lösung

Es wird eine Schnittstelle IVisitor definiert, welche für jeden Objekttyp aus der Objektmenge einen Einsprungpunkt beschreibt, über den die Implementierungen der Operation erreicht wird.

interface IVisitor {
  opAuf(Typ1 obj);
  opAuf(Typ2 obj); 
}

Die Typen der Objekte implementieren die Schnittstelle Element, welche eine Methode definiert, die die Anwendung einer Operation auf ein Objekt zulässt:

interface IElement {
  Accept(IVisitor visitor);
}

Jetzt werden die Typen der Objekte auf der einen, und unabhängig davon die Operationen auf der anderen Seite implementiert:

// Typen der Objekte aus der Menge
class Typ1 : IElement {
   Accept(IVisitor visitor) {
     visitor.opAuf(this);
   }
}// Typen der Operationen
class OperationA : IVisitor {
   opAuf(Typ1 obj) {…}
   opAuf(Typ2 obj) {…}

}

Konsequenz

  • Menge der Operationen auf einer Menge von Objekten kann modular erweitert werden.



1.7.5 Interpreter

Problem

Die Eingaben für ein Programm sind in einer formalen Sprache verfasst und damit äußerst vielfältig. Durch Interpretation muss das Programm die Aufgabenstellung verstehen und als Antwort eine Lösung berechnen.

A+(B*C) → [Programm=Interpreter] → Ergebnis

Der Interpreter sollte leicht an grammatikalische Änderungen in der formalen Sprache anpassbar sein.

Lösung

Die Formale Sprache beschreibt Ausdrücke aus Operationen auf Variablen.

Lösung = A + (B*C)

Die Werte der Variablen werden durch den Kontext bereitgestellt, realisiert z.B. durch ein Dictionary

var Kontext = new Dictionary<string, TValue>();

Die Ausdrücke können auf Baumstrukturen abgebildet werden (Syntaxbaum). Die Blätter in einem solchen Baum sind die terminalen und die restlichen Knoten werden als Nichtterminale Knoten bezeichnet.

Lösung                    // nicht terminal
 |
 +- Addition              // nicht terminal
      |
      +- Variable A       // terminal
      |
      +- Multiplikation   // nicht terminal
           |
           +- Variable B  // terminal
           |
           +- Variable C  // nicht terminal

Für die Terminalen, als auch nicht- terminalen Symbole werden Klassen gebildet, die eine gemeinsame Schnittstelle implementieren:

interface IExpr {
  TValue Interpretiere(Kontext ctx);
}

// Beispiel für Klasse zu terminalen Symbolen
class Variable : IExpr {
   string _name;
   public Variable(string name) { _name = name;}
   TValue Interpretiere(Kontext ctx) {
      return ctx[_name];
   }
}

// Beispiel: Klasse für nicht- terminales Symbol
class Addiditon : IExpr {
   IExpr _summandA;
   IExpr _summandB;
   publuic Addition(IExpr A, IExpr B) {
      _summandA = A;
      _summandB = B;
   }

   TValue Interpretiere(Kontext ctx) {
      return _summandA.Interpretiere(ctx) + _summandB.Interpretiere(ctx);
   } 
}

Jetzt können die Eingaben durch Instanziieren und zuweisen von Objekten formuliert werden:

var A = new Variable("A");
var B = new Variable("B");
var C = new Variable("C");

var Lösung = new Addition(A, Multiplikation(B, C)).Interpretiere(Kontext);

Konsequenz

  • Da jedes Symbol der Sprache durch eine eigene Klasse implementiert wird → hohe Modularität

  • Nachteil: Durch Vielzahl an Klassen unübersichtlich und Ressourcenintensiv zur Laufzeit

1.7.5.1 Beispiel: Operationen mit ContentVectoren in DMS.FC.FileClassificator

1.7.6 Observer

Problem

In einem Objekt A werden Eigenschaften geändert. Der Zustand anderer Objekte im Programm leitet sich von den Eigenschaftswerten aus A ab. Es soll ein konsistenter Zustand zu jedem Zeitpunkt garantiert werden.

Lösung

Jedes abhängige Objekt, genannt Abonnent, muss nach der Änderung des Zustands von A über einen einheitlichen Methodenaufruf informiert werden. Dazu implementiert es die Schnittstelle IAbo:

interface IAbo {
  void update(...);
}

class Abonnent1 : IAbo {…}

Objekt A, genannt Verleger, implementiert die Verlegerschnittstelle:

interface IVerleger {
   void addAbo(IAbo abo);
   void removeAbo(IAbo abo);
}

class A : IVerleger {
   List<IAbo> abonennten = new List<IAbo>();

   public void addAbo(IAbo abo) {
     abonnenten.add(abo);
   }

   public void removeAbo(IAbo abo) {
     abonnenten.remove(abo);
   }

   public void update(...) {
      …
      foreach(IAbo abo in abonnenten) {
        abo.update(...);
      }
   }
}

Konsequenz

  • Die Struktur der Abonnenten kann unabhängig Entwickelt werden von der Struktur der Verleger

  • Die Menge der Abonnenten an einem Verleger ist im Allgemeinen heterogen



1.7.6.1 Beispiel: mko.Log.LogServer- Handler

1.7.7 State

Objektorientierte, erweiterbare Implementierung von Zustandsautomaten.

Problem

Mit jedem Wechsel des inneren Zustandes eines Objekts A muss die Implementierung der öffentlichen Methoden ausgetauscht werden.

Lösung

Jeder Zustand und die Zugehörige Implementierung der Methoden werden in einer speziellen Klasse gekapselt. Die Klasse implementiert die Schnittstelle IZustand, welche alle zustandsabhängigen Methoden auflistet.

interface IZustand {
  T abhängigeOp1(Kontext ctx, ...)
  …
}

Das Objekt mit dem zustandsabhängigen Verhalten besitzt eine Referenz auf das aktuelle Zustandsobjekt. Mittels diesem implementiert es seine zustandsabhängigen Methoden.

class Kontext {

   public IZustand z;

   public T abhängigeOp1(...) {
      return z.abhängigeOp1(this, ...);
   }
}

Programm {
   var obj = new Kontext();

   // In abhängigkeit vom Inneren Zustand wird bei jedem Aufruf 
   // ein mit dem Zustand verbundenes Ergebnis generiert
   obj.abhängigeOp1(...);
   obj.abhängigeOp1(...);
   obj.abhängigeOp1(...);
}

Dank des übergebenen Kontext- Parameters können die Zustandsobjekte den Folgezustand im Objekt einstellen:

class Zustand1 : IZustand {
   public T abhängigeOp1(Kontext ctx) {

      if(...)
        ctx.z = new Zustand2();
   }
}

Konsequenz

  • Zuständsklassen kapseln das Zustandsabhängige verhalten

  • Erweiterbarkeit vereinfacht



1.7.7.1 Beispiel: Dokument- Workflow

im Entwurf-> Freigegeben-> Archiviert

1.7.8 Nullobjekt

Problem


Lösung


Konsequenz




1.7.8.1 Beispiel: DMS.Batchprocessing.DeliverFinishedJob für den Fall, der Job wurde noch nicht beendet.

1.7.9 Chain of Responsibility

Problem


Lösung


Konsequenz