1 Design
Patterns
1.1 Quellen
Gute Einführung mit Beispielen in C# hier
Viel zur Historie und Besipiele in verschiedenen Sprachen
gibt’s hier
Eine interaktive Einführung mit lustigen Beispielen
gibt’s hier
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
|
beschreiben grundlegende Entwürfe
fördern die Wiederverwendung von bewährten
Lösungen
bilden ein einheitliches Vokabular für ein
Entwicklerteam im Entwurfsprozess (unabhängig von einer
Programmiersparache)
|
kein Garant für gutes Design
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.
|
Abstract Factory
Builder
Factory Method
Prototype
Singleton
|
Adapter
Composite
Bridge
Decorator
Facade
Flyweight
Proxy
|
Chain of Responsibility
Command
Interpreter
Iterator
Mediator
Memento
Null Object
Observer
State
Strategy
Template Method
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|