Muster/ Druckversion
Vorwort
Muster (engl. pattern), oft auch Entwurfsmuster (engl. design patterns) genannt, sind im Bereich der objektorientierten Softwareentwicklung inzwischen zum notwendigen Handwerkszeug des Entwicklers geworden. Das Buch von Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Design Patterns - Elements of Reusable Object-Oriented Software zu kennen gehört hierbei zum guten Ton. Auf dieses Werk beziehen sich verschiedene Autoren dieses Wikibooks, wenn sie die Abkürzung GoF (Gang of Four, Spitzname der vier Autoren) benutzen.
Ziel dieses Buches
[Bearbeiten]Dieses Buch möchte euch Muster mit dem Zweck nahe bringen, diese "Algorithmen der Objektorientierung" verständlich, sachlich und in konkreten Zusammenhang mit alltäglichen Aufgaben zu erklären.
Umfang des Buches
[Bearbeiten]Eine der größten Herausforderungen ist es, die verschiedenen Muster zu kennen, zu identifizieren und anwenden zu können und schließlich zu implementieren. Alle diese Punkte sollen dabei Berücksichtigung finden.
Ich hoffe, dass auch Du hier die notwendigen Informationen findest -- oder falls nicht, zur Vervollständigung beiträgst.
--Bastie 18:33, 15. Mär 2005 (UTC)
Vorlage
Du sollst dich in diesem, deinem Buch zurechtfinden. Daher wird jedes Muster einem speziellen Aufbau folgen.
Der erste Abschnitt ist dem Musternamen (deutsch) sowie geläufigen Übersetzungen gewidmet. Zudem wird dir kurz der Zweck des Musters näher gebracht.
Zweck und Verwendung
[Bearbeiten]Im zweiten Abschnitt wirst du den Zweck intensiver kennenlernen. Hierzu sollen auch spezielle Beispiele dienen.
UML
[Bearbeiten]Dieser Abschnitt wird dir die Notation in der UML zeigen. Dies wird hilfreich sein, wenn du mit modernen Entwicklungswerkzeugen (vorhandene) Projekte "begutachtest".
Anwendungsfälle
[Bearbeiten]Typische Anwendungsfälle des Musters
Vorteile
[Bearbeiten]Vorteile / Stärken des Musters (Der Autor würde ja den Begriff Stärken bevorzugen. Aber der Begriff Vorteile wird in den meisten gängigen Lehrbüchern und Skripten verwendet. Um keine Verwirrung zu stiften halten wir uns im Buch daher an den Begriff Vorteile)
Nachteile
[Bearbeiten]Nachteile / Schwächen des Musters
Entscheidungshilfen
[Bearbeiten]Hier wird auf das Pro und Kontra des Musters eingegangen. Das heißt wann hat es Sinn ein Muster einzusetzen bzw. wann solltest du es nochmals überdenken.
Implementation
[Bearbeiten]In diesem Abschnitt wird die Implementation beschrieben. Die konkrete Implementierung wird hier nicht zu finden sein (kein Quellcode). Dieser kann und soll per Link jedoch gern aufgenommen werden, der auch in der Subüberschrift Beispiele angeführt werden.
Beispiele
[Bearbeiten]Verwandte Muster
[Bearbeiten]Hier werden verwandte Muster mit einer kurzen Begründung zu der Verwandtschaft aufgenommen
Einführung
Entwurfsmuster stammen ursprünglich aus der Architektur - und ich finde da lassen sich schöne Analogien finden; z.B. diese: Das Problem besteht darin, dass Personen vom Erdgeschoss ins weit entfernte Dachgeschoss kommen müssen (ich schätze dieses Problem tritt in der Architektur recht häufig auf) - die Schablone zur Lösung: ein vertikaler Leutetransporter! (manche Zeitgenossen nennen sowas auch "Aufzug"). Nun gibt es aber durchaus verschiedene Arten von Aufzügen - das allgemeine Konzept eines vertikalen Leutetransporters muss eben für die verschiedenen konkreten Probleme angepasst werden: Expressaufzug, wenn's schnell gehen soll; ein Lastenaufzug, wenn's schwer wird; ein gläserner Aufzug, wenn's ums Repräsentieren geht;
Was sind Muster
[Bearbeiten]Ganz unabhängig für welchen Einsatzzweck eine Software geschrieben wird, treten immer wieder ähnliche (Entwurfs/Architektur-)Probleme auf: z.B. kann sich bei der Simulation einer Schaltung der Wert an einem Port ändern und mit diesem Port verbundene Bausteine müssen darüber informiert werden; oder bei einer Krankenhausverwaltungssoftware wird ein neuer Patient aufgenommen und dieser sollte doch dann bitte sofort in verschiedenen anderen Modulen auftauchen. Was haben diese beiden Probleme nun gemeinsam? In beiden Fällen soll eine Änderung an einem Objekt (Binärport bzw. Patientenverwaltung) einer Reihe von anderen Objekten (an den Port angeschlossenen Bausteinen bzw. Labormodul, Stationsmodul, Küchenmodul,...) bekanntgemacht werden. Ein Muster ist nun eine Art Lösungsschablone für bestimmte Problemklassen (schaue dir für die Lösung der Beispielprobleme mal den Beobachter an). Und natürlich ist diese Schablone für ein konkretes Problem anzupassen!
Musterkategorien
[Bearbeiten]In der Informatik werden Muster auf unterschiedlichen Abstraktionsebenen verwendet.
Muster der objektorientierten Analyse (OOA):
- Archetypmuster: Domänenspezifische Analysemuster. zB Analysemuster für die Medizintechnik, Automobilindustrie, etc.
- Analysemuster: Muster zur Analyse von Geschäftsprozessen (Business Processes) und Geschäftsfällen (Business Cases) - also immer wiederkehrende Tätigkeiten in der OOA
Muster im objektorientierten Design (OOD):
- Architekturmuster: Dies sind Muster der höchsten Ebene. Sie thematisieren die grundlegende Organisation und Interaktion zwischen den Komponenten einer Anwendung.
- Entwurfsmuster: Diese Muster spezialisieren sich auf Themen des Entwurfes. Sie sind das Hauptthema dieses Buches.
- Idiome: Sind programmiersprachenspezifische Muster und auf niedrigster Abstraktionsebene. Sie zeigen auf, wie Aspekte von Komponenten oder Beziehungen untereinander mit den programmiersprachspezifischen Eigenschaften dargestellt oder implementiert werden können.
Weitere Muster: Es gibt noch eine Vielzahl weiterer in der SW-Entwicklung relevante Muster. Hier zu erwähnen sind:
- Antimuster: "worst practice" - In der Realität (häufig) auftretende Muster, welche oberflächlich vielleicht brauchbar erscheinen, jedoch in der Anwendungspraxis scheitern.
- Datenbank-Entwurfsmuster
Unterschied zwischen Entwurfsmuster und Klassen
[Bearbeiten]Ein Muster beschreibt die allgemeine Lösung einer abstrakten Problemstellung. Beispiel: den vertikalen Transport von Menschen/Gütern. Die Klasse hingegen ist die Umsetzung einer konkreten Problemstellung. Beispiel: Ein gläserner Aufzug für maximal sechs Personen.
Wozu werden Muster verwendet?
[Bearbeiten]Ein Muster ist ein allgemeiner Lösungsansatz für eine bestimmte Sorte von Problemstellungen. Durch Verwendung des richtigen Musters muss man dieselbe oder ähnliche Aufgaben nicht mehrfach lösen (das Rad nicht zweimal erfinden). Dadurch kann nicht nur Entwicklungszeit eingespart, sondern auch die Kommunikation unter den Entwicklern vereinfacht werden. Der konsequente Einsatz von Mustern hilft den Code besser zu verstehen und erleichtert somit die Weiterentwicklung und Wartung bestehender Software.
Architektur- und Entwurfsmuster sind:
- "best practice", haben sich also in der Praxis bewährt
- lösen wiederkehrende Entwurfsprobleme
- können helfen, Designfehler zu verhindern
- sind plattform- und programmiersprachenunabhängig (Aber nicht unbedingt paradigmenunabhängig. Die in diesem Buch behandelten Muster sind großteils für objektorientierte Programmierung ausgelegt.)
Wie werden Muster angewandt
[Bearbeiten]Patternsprache
[Bearbeiten]Stichwort UML
Siehe auch
[Bearbeiten]- Muster - Entwurfsmuster-Auswahl
Weblinks
[Bearbeiten]Wikipedia: Entwurfsmuster (Definition)
LayerArchitecture
Schichtenarchitektur
[Bearbeiten]Der Schichtenarchitektur (engl. Layer architecture oder auch n-Thier-Architecture genannt) gliedert die Software in einzelne Schichten auf. Dieses Muster wird seit langer Zeit in der Softwareentwicklung angewendet und ist oft auch Grundlage für das Verständnis anderer Architekturen. Schichtenarchitekturen können nicht nur im Rahmen objektorientierter Entwicklung sondern auch zB bei der prozeduralen Programmierung eingesetzt werden (zB indem definiert wird, welche Prozeduren zu welcher Schicht gehören und man sich an die unten beschriebenen Regeln hält).
Zweck und Funktionsweise
[Bearbeiten]Das Softwaresystem soll in horizontale Schichten aufgeteilt werden. Einzelnen Softwarekomponenten ist es je nach Art der Architektur gestattet, mit folgenden anderen Komponenten zu kommunizieren:
- Komponenten, die sich in derselben Schicht befinden (horizontale Kommunikation)
- Komponenten, die sich in einer direkt übergeordneten oder direkt untergeordneten Schicht befinden
- Jede (außer die höchste) Schicht bietet Dienste für die übergeordnete Schicht an
Wieviele Schichten ein System hat und was die Aufgaben der Schichten sind, hängt von der zugrundeliegenden Problemstellung ab. Grundsätzlich spricht man bei n Schichten von einer n-Thier-Architektur.
Einführungsbeispiel
[Bearbeiten]Eine typische Datenverarbeitungssoftware kann zum Beispiel aus folgenden 3 Schichten bestehen (3-Thier-Architektur):
- Benutzeroberflächen-Schicht: Hier werden die Benutzersteuerelemente gerendert und die Oberfläche bereitgestellt
- Geschäftslogik-Schicht: Hier werden Berechnungen durchgeführt und die Kommunikation zur Datenzugriffsschicht ermöglicht
- Datenzugriffs-Schicht: Diese Schicht sorgt dafür, dass Daten aus der zugrundeliegenden Datenquelle (zB eine Datenbank) gelesen oder geschrieben werden kann.
In diesem Beispiel werden zB beim Laden eines Datensatzes durch den/die BenutzerIn folgende Schritte durchgeführt:
- Die Benutzeroberflächen-Schicht gibt ein Signal, dass ein Datensatz angefordert wird an die Geschäftslogik-Schicht weiter
- Die Geschäftslogikschicht leitet die Anfrage an die Datenzugriffsschicht weiter (und überprüft ggf. zB die Zugriffsrechte des Benutzers / der Benutzerin auf die angeforderten Daten, überprüft den Logikstatus, etc.)
- Die Datenzugriffsschicht liefert die Daten aus der Datenbank und gibt sie an die Geschäftslogikschicht zurück
- Die Geschäftslogikschicht leitet die Daten an die Benutzeroberflächenschicht weiter (und führt ggf. diverse Berechnungen aus - zB Summenbildungen, Aggregationen, Datenverknüpfungen, etc.)
- Die Benutzeroberflächen-Schicht zeigt die erhaltenen Daten an
Dies mag im ersten Moment etwas umständlich wirken, aber stellen wir uns folgende Szenarien vor, um die Vorteile von Schichtenarchitekturen zu sehen:
- Wenn das Datenbanksystem gewechselt wird, so muss nur die Datenzugriffsschicht angepasst/ausgetauscht werden. Die anderen Schichten arbeiten transparent weiter.
- Es soll für eine bestehende Webanwendung eine alternative Benutzeroberfläche erstellt werden (zB eine Mobile-Variante). Dann ist die Erstellung einer weiteren Benutzeroberflächenschicht nötig. Geschäftslogik und Datenzugriffsschicht können aber weiterhin verwendet werden.
Varianten
[Bearbeiten]Schichtenarchitekturen gibt es in allen Größen. Teilweise können sich bei großer Software hunderte Klassen in einer einzigen Schicht befinden (wobei man hierbei weitere Strukturierungslösungen innerhalb der Schichten designen sollte).
Eine Schicht kann aber auch nur aus einer einzigen Klasse bestehen. Zum Beispiel können die Datei Laden und Speichern - Operationen eines simplen Texteditors als eigene Klasse (Schicht) abgebildet werden, um für zukünftige Änderungen (zB verschiedene Datenpersistierungsarten) gewappnet zu sein.
Vorteile
[Bearbeiten]Man verwendet Schichtenarchitekturen um:
- Komplexe Systeme in einfachere Bausteine zu zerlegen ("divide and conquer")
- Modulare Systeme zu designen (so sollen einzelne Schichten austauschbar sein)
- Abstrakte Systeme zu designen (Änderungen in einer Schicht sind für andere Schichten transparent, sofern die Schnittstellen nicht verändert werden)
Nachteile
[Bearbeiten]Folgende Nachteile können mit einer Schichtenarchitektur verbunden sein:
- Höherer Implementierungsaufwand
- Implementieren zu vieler Schichten für das aktuelle Problem (Antimuster: Lasagnencode)
- Bei vielen Schichten müssen Daten durch alle Schichten durchgereicht werden. Dies führt teilweise zu zu komplexen und aufwändigen Vorgängen für triviale Operationen
Praxisbeispiele
[Bearbeiten]Schichtenarchitekturen werden in den unterschiedlichsten Granularitäten verwendet. Sie können innerhalb einer einzigen Software, oder verteilt über mehrere Systeme angewendet werden. Traditionelle Datenverarbeitungssysteme sind oftmals in Schichten aufgebaut (zB ERP & CRM - Software). Ein weiteres Beispiel ist das TCP/IP - Modell (und dessen Implementierung) sowie das OSI Schichtenmodell zur Netzwerkkommunikation.
Abgrenzungen
[Bearbeiten]n-Thier vs. MVC
[Bearbeiten]Generell kann man sagen, dass die Schichtenarchitektur wesentlich grobgranularer entwickelt werden kann, als MVC. Die MVC-Architektur (oder das Pattern) kann bei großer Software als Implementierung für eine Benutzeroberflächenschicht einer Mehrschichtenarchitektur verwendet werden.
Beispiel: Viele Webapplikationen werden heute mit intelligenten Clients (=Verwendung von Javascript-Frameworks zB AngularJS) entwickelt. Dadurch kann sich zum Beispiel folgendes Szenario ergeben:
- Benutzeroberflächen-Schicht: Die Website mit AngularJS umgesetzt als MVC-Pattern.
- Client-Kommunikations-Schicht: zB clientseitige Angular-Services, die von den Controllern verwendet werden, um mit dem Webserver zu kommunizieren
- Server-Kommunikationsschicht: MVC basierte Serveranwendung, die eine REST-API zur Verfügung stellt
- Datenzugriffsschicht: Eigene Schicht zum DB-Zugriff auf dem Server
(Dieser Absatz muss noch weiter ausgeführt werden! Es sind zahlreiche Diskussionen im Internet darüber zu finden, zB http://stackoverflow.com/questions/4577587/mvc-vs-3-tier-architecture )
PipesAndFiltersArchitecture
Pipes und Filter ist ein Muster zum schrittweisen Verarbeiten (z. B. Berechnen, Transformieren, ...) von Daten.
Zweck und Funktionsweise
[Bearbeiten]In der Informatik gibt es verschiedene Szenarien, bei denen Daten verarbeitet werden müssen – zum Beispiel um Daten aus verschiedenen Quellen in das Format des zu entwickelnden Systems zu transformieren:
- Verarbeiten von Usereingaben
- Verarbeiten von Eingaben durch maschinelle Schnittstellen
- Datenimport von externen Systemen
Aber auch „ausgehende“ Daten können eine Berechnung oder Transformation benötigen:
- Datenexport in eine Datei
- Anbieten einer API zur Verfügungstellung von Daten
Das Pipes-And-Filtern Pattern hat sich als nützliches Strukturmuster zum Aufbau solcher Transformationslogiken erwiesen.
Aufbau
[Bearbeiten]Das Muster besteht aus zwei zentralen Komponenten:
- Filter sind jene Elemente, die die eigentliche Datentransformation vornehmen. Das kann z. B. eine einfache Berechnung, eine Formatänderung (zB eines Datum-Feldes) oder eine komplette Umstrukturierung eines Datensatzes sein. Müssen die Daten mehrfach transformiert werden, so können mehrere einfache Filter hintereinandergeschalten sein.
- Pipes sind die Verbindungselemente, die 2 Filter miteinander verknüpfen.
Varianten und Einsatzgebiete
[Bearbeiten]- Einfache Pipes-And-Filters: Ein Datensatz im Eingangsformat wird in die Eingangspipe des ersten Filters gestellt. Der Filter bearbeitet den Datensatz und stellt den transformierten Datensatz in seine Ausgangspipe. Diese kann wieder als Eingang für den nächsten Filter verwendet werden usw., bis der letzte Filter durchlaufen ist und sich der Datensatz im gewünschten Endformat befindet.
- Mehrfache Pipes-And-Filters: z. B. kann aus verschiedenen Datenquellen gelesen werden und die Daten durch entsprechende Filter auf das interne Format des Zielsystems gebracht werden. Diese verschiedenen Filterkaskaden können parallel ausgeführt werden. Die Implementierung einzelner Filter (zB in Form von Klassen) kann in mehreren Filterkaskadenverwendet werden (Stichwort Wiederverwendbarkeit)
- Parallelisierung von Pipes-And-Filters: Dieses Architekturmuster bietet sich zur Parallelisierung an, da die einzelnen Filterschritte voneinander unabhängig sind. Oft passiert es, dass in einer Kaskade von mehreren Filtern einer besonders langsam ist (weil er kompliziertere Berechnungen durchführt als die anderen). Dieser Filter kann parallel ausgeführt werden, wobei sich jede Filterinstanz einen Datensatz aus der Eingangspipe holt und den transformierten Datensatz in die Ausgabepipe stellt. Zu beachten ist hierbei, dass sich die Reihenfolge der Datensätze somit ändern kann, weil die Ausführungszeit einer Filterinstanz für die Berechnung eines Datensatzes nicht deterministisch ist.
Vorteile
[Bearbeiten]Es ergeben sich einige Vorteile im Gegensatz zu einer monolithischen Lösung:
- Einzelne Filter sind in verschiedenen Kaskaden wiederverwendbar
- Einzelne Filter sind leicht austauschbar oder adaptierbar
- Pipes-And-Filter-Architekturen sind gut parallelisierbar
Nachteile
[Bearbeiten]- Evtl. mehr Implementierungsaufwand durch granulare Filter
- Evtl. Performanceverlust durch zu granulare Filter
- Aufwändigeres Sicherstellen von transaktionalem Verhalten (z. B. was passiert, wenn die Bearbeitung eines Datensatzes fehlschlägt? ...)
Praxisbeispiele
[Bearbeiten]Pipes und Filter kommen überall da zum Einsatz, wo Daten schrittweise manipuliert werden müssen.
- Übersetzer (Compiler) und Interpreter arbeiten mit Pipes und Filter um den Quellcode in ausführbare Maschinensprache zu übersetzen. Die einzelnen Filter können (in dieser Reiheinfolge) etwa so aussehen:
- Lexikalische Analyse
- Syntaxanalyse
- Semantische Analyse
- Optimierung
- Codegenerierung
- Datenkompression
- Encoding von Video- oder Audiodateien (MPEG, MP3, ...)
Abgrenzungen
[Bearbeiten]ServiceOrientedArchitecture
Zweck und Funktionsweise
[Bearbeiten]In der Serviceorientierten Architektur (Service-Orented Architecture, abgekürzt mit SOA) werden einzelne IT-Dienste als Services gekapselt. Als IT-Dienste gelten hierbei zB Datenbanken, Applikationsteile oder ganze Applikationen. Die Granularität ist je nach Anwendungsfall unterschiedlich fein oder grob.
Komponenten und Begriffe
[Bearbeiten]- Services: Stellen IT-Dienste zur Verfügung. Je nach Anwendungsfall wissen die Konsumenten der Services wenig bis nichts von der Architektur der darunterliegenden Systeme. Die Services können daher einen hohen Abstraktionsgrad aufweisen.
- Basis-Services: Wenn die Services sehr abstrakt sind, können Basis-Services als Schicht zwischen den Services für die Konsumenten und den darunterliegenden Systemen eingezogen werden. Diese Basis-Services sind meist granularer und kleiner und greifen auf die eigentlichen Applikationen zu.
- Servicegeber (Service provider): Dies ist ein System, das Services bereitstellt. Dies können einzelne Geschäftsapplikationen, aber auch eine Middleware-Software sein.
- Servicenehmer / Konsument (Service consumer): Dies ist ein System, das die Services des Servicegebers konsumiert
- Serviceverzeichnis (Service repository): Listet alle verfügbaren Services auf
Anforderung an ein Service
[Bearbeiten]In verschiedenen Quellen sind eine Vielzahl unterschiedlicher Anforderungen für Services definiert. Die häufig vorkommenden sind:
- Lose Kopplung (loose coupling): Ein Service muss über eine standardisierte Schnittstelle verfügen. Solange diese Schnittstelle eingehalten wird, ist die zugrundeliegende Implementierung jederzeit austauschbar. Ein Service stellt somit eine in sich geschlossene Funktion, ohne Abhängigkeiten zu anderen Services dar.
- Zustandslos (stateless): Beim Aufruf des Services befindet sich das Service in einem wohldefinierten Zustand, der bei jedem Start gleich ist. Das Service speichert keine Informationen zu früheren Aufrufen. Sollen solche Informationen persistiert werden, so hat sich der Konsument darum zu kümmern.
- Ortstransparenz (location transparency): Für den Konsumenten spielt es keine Rolle, auf welchem Server und wie das Service platziert (gehostet) ist.
- Plattformunabhängigkeit (platform independence): Servertechnologien, Betriebssystem und Programmiersprache von Services (auch von einzelnen Services in einer SOA untereinander) und Konsumenten können heterogen sein. Es wird über standardisierte Schnittstellen und über ein standardisiertes Protokoll kommuniziert.
- Wiederverwendbarkeit (Service reusability): Services sollen so allgemein wie sinnvoll möglich gehalten werden, um die Wiederverwendbarkeit eines Services zu unterstützen.
Einführungsbeispiel
[Bearbeiten]Implementierung
[Bearbeiten]Typischerweise, aber nicht notwendigerweise, werden SOA-Services mit Webservices als einheitliche Schnittstelle implementiert. Als Technologien kommen REST und SOAP in Frage, wobei SOAP wegen seiner Mächtigkeit derzeit die üblichere Variante darstellt. Prinzipiell kann man eine SOA aber mit jeder geeigneten plattformübergreifenden Schnittstellentechnologie aufbauen.
Vorteile
[Bearbeiten]- Einheitliche Kommunikation zwischen einzelnen Systemen einer SOA wird ermöglicht.
- Dies führt zur Reduktion der Komplexität in verteilten Systemen
- Über mehrere Applikationen verteilte Geschäftsprozesse können leicht abgebildet werden und sind wartbarer als bei monolithischen Lösungen
Nachteile
[Bearbeiten]- Es ist abzuwägen, wie granular die Services sein sollen. Zu feine Granularität bedeutet einen hohen Aufwand, zu hohe Granularität bedeutet womöglich schlechte Wartbarkeit und schlechte Wiederverwendbarkeit
- Eventuell entsteht Mehraufwand durch Kommunikation
Praxisbeispiele
[Bearbeiten]ServiceOrientedArchitecture
Zweck und Funktionsweise
[Bearbeiten]In der Serviceorientierten Architektur (Service-Orented Architecture, abgekürzt mit SOA) werden einzelne IT-Dienste als Services gekapselt. Als IT-Dienste gelten hierbei zB Datenbanken, Applikationsteile oder ganze Applikationen. Die Granularität ist je nach Anwendungsfall unterschiedlich fein oder grob.
Komponenten und Begriffe
[Bearbeiten]- Services: Stellen IT-Dienste zur Verfügung. Je nach Anwendungsfall wissen die Konsumenten der Services wenig bis nichts von der Architektur der darunterliegenden Systeme. Die Services können daher einen hohen Abstraktionsgrad aufweisen.
- Basis-Services: Wenn die Services sehr abstrakt sind, können Basis-Services als Schicht zwischen den Services für die Konsumenten und den darunterliegenden Systemen eingezogen werden. Diese Basis-Services sind meist granularer und kleiner und greifen auf die eigentlichen Applikationen zu.
- Servicegeber (Service provider): Dies ist ein System, das Services bereitstellt. Dies können einzelne Geschäftsapplikationen, aber auch eine Middleware-Software sein.
- Servicenehmer / Konsument (Service consumer): Dies ist ein System, das die Services des Servicegebers konsumiert
- Serviceverzeichnis (Service repository): Listet alle verfügbaren Services auf
Anforderung an ein Service
[Bearbeiten]In verschiedenen Quellen sind eine Vielzahl unterschiedlicher Anforderungen für Services definiert. Die häufig vorkommenden sind:
- Lose Kopplung (loose coupling): Ein Service muss über eine standardisierte Schnittstelle verfügen. Solange diese Schnittstelle eingehalten wird, ist die zugrundeliegende Implementierung jederzeit austauschbar. Ein Service stellt somit eine in sich geschlossene Funktion, ohne Abhängigkeiten zu anderen Services dar.
- Zustandslos (stateless): Beim Aufruf des Services befindet sich das Service in einem wohldefinierten Zustand, der bei jedem Start gleich ist. Das Service speichert keine Informationen zu früheren Aufrufen. Sollen solche Informationen persistiert werden, so hat sich der Konsument darum zu kümmern.
- Ortstransparenz (location transparency): Für den Konsumenten spielt es keine Rolle, auf welchem Server und wie das Service platziert (gehostet) ist.
- Plattformunabhängigkeit (platform independence): Servertechnologien, Betriebssystem und Programmiersprache von Services (auch von einzelnen Services in einer SOA untereinander) und Konsumenten können heterogen sein. Es wird über standardisierte Schnittstellen und über ein standardisiertes Protokoll kommuniziert.
- Wiederverwendbarkeit (Service reusability): Services sollen so allgemein wie sinnvoll möglich gehalten werden, um die Wiederverwendbarkeit eines Services zu unterstützen.
Einführungsbeispiel
[Bearbeiten]Implementierung
[Bearbeiten]Typischerweise, aber nicht notwendigerweise, werden SOA-Services mit Webservices als einheitliche Schnittstelle implementiert. Als Technologien kommen REST und SOAP in Frage, wobei SOAP wegen seiner Mächtigkeit derzeit die üblichere Variante darstellt. Prinzipiell kann man eine SOA aber mit jeder geeigneten plattformübergreifenden Schnittstellentechnologie aufbauen.
Vorteile
[Bearbeiten]- Einheitliche Kommunikation zwischen einzelnen Systemen einer SOA wird ermöglicht.
- Dies führt zur Reduktion der Komplexität in verteilten Systemen
- Über mehrere Applikationen verteilte Geschäftsprozesse können leicht abgebildet werden und sind wartbarer als bei monolithischen Lösungen
Nachteile
[Bearbeiten]- Es ist abzuwägen, wie granular die Services sein sollen. Zu feine Granularität bedeutet einen hohen Aufwand, zu hohe Granularität bedeutet womöglich schlechte Wartbarkeit und schlechte Wiederverwendbarkeit
- Eventuell entsteht Mehraufwand durch Kommunikation
Praxisbeispiele
[Bearbeiten]AbstractClassFactory
Die Abstrakte Fabrik (engl. Abstract Factory, Kit) ist ein Erzeugungsmuster (Creational Patterns). Es definiert eine Schnittstelle zur Erzeugung einer Familie von Objekten, wobei die konkreten Klassen der zu instanzierenden Objekte dabei nicht näher festgelegt werden.
Verwendung
[Bearbeiten]Die Abstrakte Fabrik findet Anwendung, wenn
- ein System unabhängig von der Art der Erzeugung seiner Produkte arbeiten soll,
- ein System mit einer oder mehreren Produktfamilien konfiguriert werden soll,
- eine Gruppe von Produkten erzeugt und gemeinsam genutzt werden soll oder
- wenn in einer Klassenbibliothek die Schnittstellen von Produkten ohne deren Implementierung bereitgestellt werden sollen.
Eine typische Anwendung ist die Erstellung einer grafischen Benutzeroberfläche mit unterschiedlichen Themes.
Eine Abstrakte Fabrik vereinigt die Verantwortungen "Zusammenfassung der Objektgenerierung an einer Stelle" und "Möglichkeit zu abstrakten Konstruktoren" (siehe auch unten unter "Verwandte Entwurfsmuster").
Allgemein kann man also sagen:
Das Muster Abstrakte Fabrik findet immer da Anwendung, wo zur Laufzeit entschieden werden soll welche Produktklasse verwendet wird. Denn der Empfänger kann, in solchen Fällen, nicht selbständig die Entscheidung treffen welche Produktklasse er benötigt. Somit liegt die Verantwortung und Entscheidung, welche konkrete Produktlasse benötigt wird, im Bereich der Fabrikklasse.
UML-Diagramm
[Bearbeiten]Akteure
[Bearbeiten]- AbstrakteFabrik
- definiert eine Schnittstelle zur Erzeugung abstrakter Produkte einer Produktfamilie
- KonkreteFabrik
- erzeugt konkrete Produkte einer Produktfamilie durch Implementierung der Schnittstelle
- ConcreteFactory1 erzeugt die Produkte ConcreteProductA1 und ConcreteProductB1
- ConcreteFactory2 die Produkte ConcreteProductA2 und ConcreteProductB2.
- AbstraktesProdukt
- definiert eine Schnittstelle für eine Produktart
- Beachte, dass die abstrakten Klassen AbstractProduct1 und AbstractProduct2 in keiner Beziehung zueinander stehen, d.h. sie haben keine gemeinsame Superklasse.
- KonkretesProdukt
- definiert ein konkretes Produkt einer Produktart durch Implementierung der Schnittstelle
- wird durch die korrespondierende konkrete Fabrik erzeugt
- Klient
- verwendet die Schnittstellen der abstrakten Fabrik und der abstrakten Produkte
Vorteile
[Bearbeiten]Konkrete Klassen werden isoliert
[Bearbeiten]Der Client Code wird unabhängig von den konkreten Produkte-Klassen. Er deklariert nur den Interfacenamen oder die abstrakte Produkte-Klasse, jedoch keine konkreten Produkte-Klassen.
Einfaches Auswechseln von Klassen
[Bearbeiten]Der Klassenname der konkreten Produkte erscheint genau einmal im Code, nämlich in der ConcreteFactory. Dadurch wird der Austausch durch eine andere Klasse (beispielsweise eine andere Version des Produkts) massiv vereinfacht. Zudem können alle Produkte gemeinsam ersetzt werden, indem eine andere ConcreteFactory Instanz benutzt wird.
Nachteile
[Bearbeiten]Unterstützung mit einer neuen Produktefamilie ist aufwändig
[Bearbeiten]Um einen neuen Satz von Produkten zu unterstützen ist eine neue ConcreteFactory zu erstellen, und es muss unter sämtlichen AbstractProduct-Klassen eine entsprechende ConcreteProduct-Klasse für die neue Produktefamilie erstellt werden. Bei einer großen Anzahl an Produktearten entsteht dadurch ein erheblicher Aufwand.
Erweiterung durch eine neue Produktart ist schwierig
[Bearbeiten]Soll eine neue Produktart hinzugefügt werden (z.B. neben der CPU, MMU noch ein Mainboard), so muss jede Factory mit der entsprechenden Factory Method createMainboard() erweitert werden. Dann muss für das neue Product Mainboard eine abstrakte Klasse AbstractMainboard und für jede Implementation eine konkrete Klasse (gem. Bsp. EmberMainboard, EnginolaMainboard etc.) erstellt werden. Dazu ist das Wissen der Eigenschaften aus allen Produktefamilien notwendig.
Verwendung in der Analyse
[Bearbeiten]Wegen der gemeinsamen Komplexität der beiden wesentlichen Verantwortungen ("Zusammenfassung der Objektgenerierung an einer Stelle" und "Möglichkeit zu abstrakten Konstruktoren") ist dieses Pattern für die Analyse praktisch irrelevant.
Klassenhierarchie der Produkte vorbestimmt. Die ConcreteProduct-Klassen sind alle unter ihrer entsprechenden AbstractProduct-Klasse subklassiert. Dadurch können die Produkte keiner weiteren Superklasse angehören, welche aus Client-Sicht vorteilhafter wäre. Durch die Verwendung von Java-Interfaces für die AbstractProducts wird keine Superklasse „verschwendet“. Eine Alternative ist das Design mit dem Bridge Design Pattern
Beispiel
[Bearbeiten]Es soll eine Spielesammlung per Software entwickelt werden. Die verwendeten Klassen sind dabei
- Spielbrett (erstes abstraktes Produkt), auf das Spielfiguren platziert werden können und das beispielsweise eine Methode besitzt, um sich auf dem Bildschirm anzuzeigen. Konkrete, davon abgeleitete Produkte sind Schachbrett, Mühlebrett, Halmabrett etc.
- Spielfigur (zweites abstraktes Produkt), die auf ein Spielbrett gesetzt werden kann. Konkrete, davon abgeleitete Produkte sind Hütchen, Schachfigur (der Einfachheit halber soll es hier nur einen Typ an Schachfiguren geben), Holzsteinchen etc.
- Spielfabrik (abstrakte Fabrik), die Komponenten (Spielbrett, Spielfiguren) eines Gesellschaftsspiels erstellt. Konkrete, davon abgeleitete Fabriken sind beispielsweise Mühlefabrik, Damefabrik, Schachfabrik etc.
Ein Klient (z.B. eine Instanz einer Spieler- oder Spielleiter-Klasse) kann sich von der abstrakten Fabrik Spielfiguren bzw. ein Spielbrett erstellen lassen. Je nachdem, welches konkrete Spiel gespielt wird, liefert beispielsweise die...
- Schachfabrik ein Schachbrett und Schachfiguren
- Damefabrik ebenfalls ein Schachbrett, aber Holzsteinchen
- Mühlefabrik ein Mühlebrett, aber ebenfalls Holzsteinchen
Ein Beispiel in Java folgt.
/*
* GUIFactory example
*/
abstract class Button {
public abstract void paint();
}
abstract class GUIFactory {
public static GUIFactory getFactory() {
int sys = readFromConfigFile("OS_TYPE");
if (sys == 0) return new WinFactory();
else return new OSXFactory();
}
public abstract Button createButton();
}
class OSXFactory extends GUIFactory {
public Button createButton() {
return new OSXButton();
}
}
class WinFactory extends GUIFactory {
public Button createButton() {
return new WinButton();
}
}
class OSXButton extends Button {
public void paint() {
System.out.println("I'm an OSXButton: ");
}
}
class WinButton extends Button {
public void paint() {
System.out.println("I'm a WinButton: ");
}
}
public class Application {
public static void main(String[] args) {
GUIFactory factory = GUIFactory.getFactory();
Button button = factory.createButton();
button.paint();
}
// Output is either:
// "I'm a WinButton:"
// or:
// "I'm an OSXButton:"
}
Ein Beispiel in C# ist hier zu finden: http://en.csharp-online.net/Abstract_Factory_design_pattern:_Example
Verwandte Entwurfsmuster
[Bearbeiten]Das Muster der Fabrikmethode ist dem der Abstrakten Fabrik strukturell ähnlich, aber sowohl in seiner Motivation als auch in der Verwendung verschieden. Im Gegensatz zur Abstrakten Fabrik zielt dieses Muster nicht auf austauschbare Produktfamilien, sondern auf ein einzelnes Produkt. Dabei wird die Bestimmung der Klasse eines Produktes in die Unterklassen der Erzeugerhierarchie verschoben. Die Erzeugerhierarchie muss dadurch nicht zwangsläufig parallel zur Produkthierarchie sein, wie dies beim Muster der Abstrakten Fabrik der Fall ist.
Möchte man generell eine zusätzliche Hierarchie von Fabriken zu einer Hierarchie von Produkten vermeiden, kann das Muster des Prototyps verwendet werden. Bei diesem Muster werden zur Erzeugung neuer Objekte prototypische Instanzen kopiert.
Singleton
Zweck und Verwendung
[Bearbeiten]Das Einzelstück wird benutzt, um genau eine Instanz einer Klasse und einen zentralen Zugriffspunkt auf diese Instanz bereitzustellen. Dies ist sinnvoll, wenn nur ein Objekt gebraucht wird, um Aktionen im System auszuführen. Bisweilen muss verhindert werden, dass von einer Klasse mehr als eine Instanz erzeugt werden kann - genau dies leistet das Singleton-Muster.
Zum Beispiel kann eine zentrale Konfigurationsklasse als Einzelstück implementiert werden. Dies ist eine aus objektorientierter Sicht sauberere Umsetzung als beispielsweise eine Umsetzung mittels globalen Variablen. (siehe Kapitel Vorteile)
UML
[Bearbeiten]Anwendungsfälle
[Bearbeiten]Ein Beispiel für solch einen Fall ist eine Klasse, die die Verbindung zu einer Datenbank verwaltet und für den Rest des Programms Funktionen zur Verfügung stellt, um auf die Datenbank zuzugreifen. Angenommen die Datenbank bietet selbst keine Mechanismen, um beispielsweise atomare Operationen zu gewährleisten; dies bleibt dann der Datenbankklasse vorbehalten. Gäbe es nun mehrere Instanzen dieser Datenbank-Klasse, könnten wiederum verschiedene Teile des Programms gleichzeitig Änderungen an der Datenbank vornehmen, indem sie sich unterschiedlicher Instanzen bedienen. Wenn es allerdings nur genau ein Exemplar der Datenbank-Klasse gibt, tritt dieses Problem nicht auf.
Die GoF-Muster Abstract-Factory, Builder und Prototype können Singleton in ihrer Implementierung verwenden.
Vorteile
[Bearbeiten]- Die Einzelinstanz muss nur erzeugt werden, wenn sie benötigt wird.
- Die Einzelinstanz wird erst zu dem Zeitpunkt erzeugt, zu dem sie benötigt wird (Lazy loading)
- Der Zugriff auf das Einzelstück ist leichter handhabbar, wenn diese als globale Klassenvariable in jeder Klasse (inklusive den Zuweisungen des Objektes an diese Variablen) implementiert werden muss.
- Zugriffskontrolle kann realisiert werden.
- Das Singleton kann durch Unterklassenbildung spezialisiert werden.
- Welche Unterklasse verwendet werden soll, kann zur Laufzeit entschieden werden.
- Sollten später mehrere Objekte benötigt werden, ist eine Änderung leichter möglich als bei globalen Variablen.
Nachteile
[Bearbeiten]Diese Inhalte stammen aus der deutschen Wikipedia. Ein Abgleich mit gängiger Literatur folgt.
- Es besteht die große Gefahr, durch exzessive Verwendung von Singletons quasi ein Äquivalent zu globalen Variablen zu implementieren und damit dann prozedural anstatt objektorientiert zu programmieren.
- Abhängigkeiten zur Singleton-Klasse werden verschleiert, d. h. ob eine Singleton-Klasse verwendet wird, erschließt sich nicht aus dem Interface einer Klasse, sondern nur anhand der Implementierung. Zudem wird die Kopplung erhöht, was Wiederverwendbarkeit und Übersichtlichkeit einschränkt.
- Fehler erzeugt werden sollen – fast unmöglich. Mit der Java Reflection API ist es jedoch möglich, die Kapselung der Singleton zu verletzen und die Instanziierung zu kontrollieren.
- Die Konfiguration des Singletons ist – zumindest bei Lazy-Initialization (s. u.) – nur über andere Singletons möglich, zum Beispiel Environment-Variablen, aus einem Registry, aus „well-known“ Files o. Ä.
Entscheidungshilfen
[Bearbeiten]Ein Singleton sollte dann eingesetzt werden, wenn sichergestellt sein muss, dass nicht mehr als ein Objekt einer Klasse erzeugt werden kann. Dabei ist jedoch genau zu überlegen, worauf sich "ein Objekt" bezieht: Pro Programm? Bei ge-cluster-ten Anwendungen: pro Cluster? Bei Java-Programmen: pro ClassLoader?
Singletons sind das semantische Äquivalent einer globalen Variablen, mit all der damit verbundenen Problematik; es ist genau zu überlegen, ob das jeweilige Ziel nicht anders besser erreicht werden kann.
Implementation
[Bearbeiten]Anstatt selbst eine neue Instanz durch Aufruf des Konstruktors zu erzeugen, müssen sich Benutzer der Singleton-Klasse eine Referenz auf eine Instanz über die statische getInstance()
-Methode besorgen - die Singleton-Klasse ist also selbst für die Verwaltung ihrer einzigen Instanz zuständig. Die getInstance()
-Methode kann nun sicherstellen, dass bei jedem Aufruf eine Referenz auf dieselbe und einzige Instanz - gehalten in einer (versteckten) Klassenvariable - zurückgegeben wird. Üblicherweise wird diese eine Instanz von getInstance()
beim ersten Aufruf erzeugt.
In nebenläufigen Programmen muss der Programmierer beim Schreiben der getInstance()
-Methode besondere Vorsicht walten lassen (siehe Code-Beispiele).
Bei der Implementierung zu beachten:
- In Multithreaded-Umgebungen können Fehler passieren, wenn konkurrierende Threads gleichzeitig Singletons erzeugen. Die Erzeugungsmethode darf also nur von einem Thread gleichzeitig (also exklusiv) ausgeführt werden können.
- Eine Ressourcen-Deallokation von Ressourcen, die das Singleton verwendet, ist schwierig. So ist zum Beispiel bei einem Singleton für ein Logging-System oft unklar, wann die Logdatei geschlossen werden soll. Dies ist beim Design der Anwendung zu bedenken.
Beispiele
[Bearbeiten]Konkrete Implementationen für die Programmiersprachen
- C++,
- C#,
- Java,
- Java mit Multithreading und
- PHP
Verwandte Muster
[Bearbeiten]- Fabrikmuster, da das Singleton üblicherweise eine Klassenfabrik für genau eine Instanz der eigenen Klasse darstellt
Builder
Erbauer
[Bearbeiten]Der Erbauer (engl. Builder) ist ein Erzeugungsmuster (Creational Patterns). Es trennt die Konstruktion komplexer Objekte von deren Repräsentationen, wodurch die selben Konstruktionsprozesse wiederverwendet werden können.
Verwendung
[Bearbeiten]Entwickler verwenden den Erbauer, wenn
- zu einem komplexen Objekt unterschiedliche Repräsentationen existieren sollen.
- die Konstruktion eines komplexen Objekts unabhängig von der Erzeugung der Bestandteile sein soll.
- der Konstruktionsablauf einen internen Zustand erfordert, den man vor einem Klienten verbergen möchte.
Typische Anwendungen sind z.B. Applikationen zur Konvertierung.
UML-Diagramm
[Bearbeiten]Akteure
[Bearbeiten]- Erbauer
- spezifiziert eine abstrakte Schnittstelle zur Erzeugung der Teile eines komplexen Objektes
- KonkreterErbauer
- erzeugt die Teile des komplexen Objekts durch Implementation der Schnittstelle
- definiert und verwaltet die von ihm erzeugte Repräsentation des Produkts (hat also internen Zustand)
- bietet Schnittstelle zum Auslesen des Produkts
- Direktor
- konstruiert ein komplexes Objekt unter Verwendung der Erbauer-Schnittstelle. Der Direktor arbeitet eng mit dem Erbauer zusammen: Er weiß, welche Baureihenfolge der Erbauer verträgt (z.B. müssen in einem Baum zuerst Blätter, dann innere Knoten erzeugt werden; oder umgekehrt). Daher ist eine weitere Verantwortung des Direktors:
- Entkopplung des Konstruktionsablaufs vom Klient.
- Produkt
- repräsentiert das zu konstruierende komplexe Objekt
- beinhaltet Klassen zur Definition der einzelnen Teile
Vorteile
[Bearbeiten]- Die Implementationen der Konstruktion und der Repräsentationen werden isoliert.
- Erbauer verstecken ihre interne Repräsentation vor dem Direktor.
- Neue Repräsentationen lassen sich leicht durch neue konkrete Erbauerklassen einfügen.
- Der Konstruktionsprozess wird an einer dedizierten Stelle (im Direktor) gesteuert; spätere Änderungen - etwa ein Mehrphasen-Konstruktionsprozess statt einer Einphasen-Konstruktion - lassen sich ohne Änderung der Klienten realisieren.
Variante
[Bearbeiten]Man kann auch das Produkt selber die Erbauer-Schnittstelle implementieren lassen.
- Vorteil: Dadurch erspart man sich u.U. einige Klassen.
- Konsequenz: Das erzeugte Produkt "schleppt" die Erbauer-Schnittstelle sein ganzes Leben mit sich herum, sodass auch später von außen Produktteile angebaut werden können.
Verwendung in der Analyse
[Bearbeiten]Dieses Muster wird in der Software-Analyse wegen der schwierigen Metapher selten verwendet.
Die Variante allerdings, wo ein Objekt selbst Verfahren zur Verfügung stellt, um weitere Teile "anzubauen", bewährt sich in pipeline-artigen Business-Prozessen. Der Business-Prozess als Direktor weist das Dokument als Erbauer an, neue Teile zu erzeugen und in sich einzuhängen. Z.B. kann ein Aktenverwaltung in einzelnen Schritten Vermerke an einen "Aktenlauf" anhängen.
Beispiele
[Bearbeiten]- Ein Automobil besteht aus Rädern, Motor und Karosserie. Die Bestandteile liegen als verschiedene Klassen vor (z.B. schwacher und starker Motor). Der Direktor konstruiert ein Fahrzeug, indem er eine der konkreten Erbauerklassen Geländewagen oder Limousine erhält und dort die Methoden zum Bauen der Teile in der (für den Erbauer) richtigen Reihenfolge aufruft. Beide Klassen erben von der Erbauerklasse Fahrzeug. Der Direktor stellt ein Produkt her, indem er die Erbauermethoden
baueRäder()
,baueKarosserie()
undbaueMotor()
aufruft. Limousine erzeugt ein Teile für ein Auto, das aus normalen Rädern, Limousinenkarosserie und schwachem Motor besteht. Der Geländewagen setzt sich aus off-road Rädern, höhergelegter Karosserie und starkem Motor zusammen. Braucht der Entwickler ein Cabriolet, schreibt er einen konkreten Erbauer Cabriolet für Cabrioletteile.
- Java:
java.text.StringBuffer, java.io.Writer
- .NET:
System.Text.StringBuilder, System.IO.StreamWriter
In den Java- und .NET-Beispielen kann der Klient der Direktor sein. Eine Entkopplung, z.B. über eine Klassenfabrik, ist aber zu empfehlen.
Verwandte Entwurfsmuster
[Bearbeiten]- Die Abstrakte Klassenfabrik ähnelt dem Erbauer, weil sie ebenfalls komplexe Objekte erzeugen kann. Dabei steht aber nicht die Struktur im Vordergrund, sondern die Abstraktion vom konkreten Typ der erzeugten Objekte.
- Der Erbauer erzeugt oft ein Kompositum.
- Bei Applikationen zur Konvertierung ist der Direktor - oder sogar der Erbauer - oft ein Besucher oder eventuell ein Interpreter der Struktur, die konvertiert werden soll.
ClassFactory
KlassenFabrik
[Bearbeiten]Fabrik (englisch Factory) es wird zumeist zwischen Fabrikmethode und Abstrakten Fabrik unterschieden.
Zweck
[Bearbeiten]Das Factory-Pattern stellt ein Interface zur Erstellung eines Objektes. Dabei wird es den Unterklassen der Factory-Klasse überlassen, welche konkreten Klassen instanziiert werden. Die Factory-Klasse biete dabei eine abstrakte Methode, die von den Unterklassen überschrieben werden.
UML
[Bearbeiten]Abstrakte Fabrik
[Bearbeiten]Fabrikmethode
[Bearbeiten]Anwendungsfälle
[Bearbeiten]Das "Fabrik-Muster" ist besonders hilfreich, um zur Laufzeit oder in verschiedenen Ausführungskontexten unterschiedliche Objekte zu erzeugen indem man die konkrete Fabrikklasse austauscht.
Das Factory-Pattern kann eingesetzt werden, wenn die aufrufende Klasse nicht weiß, welche konkreten Klassen und wie konkrete Klassen zu instanziieren sind.
Auch wenn eine Verzögerung bei der Instantiierung notwendig ist, ist das Factory-Pattern sinnvoll einzusetzen.
Beispiel: Es kann eine konkrete Fabrik geben, die zu Testzwecken Produkte erzeugt, mit deren Hilfe sich das Verhalten der Objekte, die die Produkte verwenden gezielt prüfen lässt. Eine Instanz außerhalb des Klienten übergibt diesem je nach Kontext (Test oder Anwendung) die passende konkrete Fabrik als Besucher.
Vorteile
[Bearbeiten]- abstrakter, wiederverwendbarer Code
- Entkopplung von konkreten Implementierungen
- Modularisierung: Austausch von konkreter Objekterzeugung und Erweiterung mit neuen Objekten
Implementation
[Bearbeiten]Für die Umsetzung einer Fabrikmethode kann wie folgt implementiert werden:
- Für jede unterschiedliche Erzeugungsmöglichkeit eine eigene Methode.
- Eine
create()
-Methode welche mit einem Parameter für die gewünschte Erzeugung.
Für die Umsetzung der Abstrakten Fabrik ist folgende Implementation notwendig:
- Ein Interface für die Fabrik, diese sollte eine create Methode für jedes Produkt enthalten.
- Eine konkrete Implementierung für jede Familie.
- Ein Interface für jedes Produkt.
- Eine Implementierung für jedes Produkt zu jeder Familie.
Verwandte Muster
[Bearbeiten]- Eine konkrete Fabrik kann mittels des Prototypmusters implementiert werden.
- Die abstrakte Fabrik bietet Fabrikmethoden (
erzeugeA()
, ...) an, die in den konkreten Fabrik-Unterklassen implementiert/überschrieben werden. - Für jede Produktfamilie wird genau ein Exemplar einer konkreten Fabrik benötigt. Die konkrete Fabrik könnte somit als Singleton implementiert werden.
Multiton
Multiton
[Bearbeiten]Das Multiton ist sozusagen eine Erweiterung des Einzelstücks (auch Singletons), indem es nicht ein einziges Objekt, sondern eine Reihe von Objekten erzeugt, auf die mit einem eindeutigen Schlüssel zugegriffen werden kann.
Zweck
[Bearbeiten]Mit diesem Erzeugungsmuster können eine Anzahl von zusammenhängende Objekte so erzeugt werden, dass für jedes dieser Objekte wie beim Einzelstück sichergestellt ist, dass höchstens eins existiert.
UML
[Bearbeiten]Anwendungsfälle
[Bearbeiten]Das Multiton ist eine flexiblere Form eines Registrys - einer simplen Datensammlung von Objekten. Manche Entwurfsbücher sehen das Multiton als eine Erweiterung des Singleton und erwähnen es nicht als eigenständiges Muster.
Vorteile
[Bearbeiten]- Sicherheit, dass für die eindeutig erreichbare Objekte nur eine Instanz vorhanden ist.
- Speicher wird dadurch eingespart
- Es wird immer auf ein und dieselbe Instanz zugegriffen (keine Daten-Duplikation)
Nachteile
[Bearbeiten]So wie es beim Einzelstück auch der Fall ist, wird mit einem Multiton
- der Modul- oder Unit-Test erschwert, weil es einen globalen Zustand in die Anwendung einfügt
- Parallelisierungen erschwert
- Bei Sprachen mit Freispeichersammlung kann es durch Multiton zu Speicherlecks kommen.
Beispiele
[Bearbeiten]Ein Beispiel aus der Wikipedia folgt:
C#
[Bearbeiten]public enum EFamily
{
ANNA,
SIMON,
SUSANNA,
UWE,
}
public class Family
{
private static Dictionary<EFamily, Family> instances = new Dictionary<EFamily, Family>();
private String name;
public String Name {
get
{
return name;
}
private set
{
name = value;
}
}
private Family() { }
public Family(string name)
{
Name = name;
}
public static Family getInstance(EFamily key)
{
Family instance = null;
try
{
instance = instances[key];
}
catch (Exception)
{
instance = new Family(GetName(key));
instances.Add(key, instance);
}
return instance;
}
public static String GetName(EFamily key)
{
String name = key.ToString().ToLower();
name = name[0].ToString().ToUpper() + name.Substring(1);
return name;
}
public void WhoAmI()
{
Console.WriteLine("I am " + Name + '.');
}
}
class AllDesignPatterns
{
static void Main(string[] arguments)
{
Family father = Family.getInstance(EFamily.UWE);
father.WhoAmI();
Family mother = Family.getInstance(EFamily.ANNA);
mother.WhoAmI();
IsSame(father, mother);
Family daughter = Family.getInstance(EFamily.SUSANNA);
daughter.WhoAmI();
IsSame(daughter, mother);
Family son = Family.getInstance(EFamily.SIMON);
son.WhoAmI();
IsSame(father, son);
Family member = Family.getInstance(EFamily.SUSANNA);
member.WhoAmI();
IsSame(daughter, member);
}
private static void IsSame(Family first, Family second)
{
if (first != null && second != null)
{
Console.Write(first.Name + " is");
Console.Write(!first.Equals(second) ? " not" : "");
Console.WriteLine(" the same person as " + second.Name + '.');
}
}
}
returns the following:
Susanna is the same person as Susanna. I am Uwe. I am Anna. Uwe is not the same person as Anna. I am Susanna. Susanna is not the same person as Anna. I am Simon. Uwe is not the same person as Simon. I am Susanna. Susanna is the same person as Susanna.
Java
[Bearbeiten] public class FooMultiton {
private static final Map<Object, FooMultiton> instances = new HashMap<Object, FooMultiton>();
private FooMultiton() /* also acceptable: protected, {default} */ {
/* no explicit implementation */
}
public static FooMultiton getInstance(Object key) {
synchronized (instances) {
// Our "per key" singleton
FooMultiton instance = instances.get(key);
if (instance == null) {
// Lazily create instance
instance = new FooMultiton();
// Add it to map
instances.put(key, instance);
}
return instance;
}
}
// other fields and methods ...
}
Implementation
[Bearbeiten]Für die Umsetzung des Multitons wird meist eine Hashtabelle o.Ä. verwendet. Dadurch ist es möglich, mit einem Schlüssel das jeweilige Objekt zu erreichen. Gibt es das Objekt noch nicht, wird null
zurückgeliefert. Beim Schreiben wird das bestehende Objekt überschrieben oder neu angelegt - je nachdem, ob es schon existierte oder nicht.
Verwandte Muster
[Bearbeiten]Das Einzelstück (oder Singleton) ist die einfachste Form ohne Schlüssel, wo nur genau ein Objekt verwaltet wird.
Prototype
Prototyp
[Bearbeiten]Es wird ein Prototyp benutzt um mittels Klonen neue Objekte mit den selben Eigenschaften zu erzeugen.
Zweck
[Bearbeiten]Wenn Objekte mit unterschiedlichen, aber vorher definierten Eigenschaften erzeugt werden sollen, dann kann dies mittels eines Prototyps erfolgen.
UML
[Bearbeiten]Dieser Abschnitt wird dir die Notation in der UML zeigen. Dies wird hilfreich sein, wenn du mit modernen Entwicklungswerkzeugen (vorhandene) Projekte "begutachtest".
Entscheidungshilfen
[Bearbeiten]- Durch Abänderung des Prototypen werden automatisch alle daraus erzeugten Objekte geändert.
- Es ist jedoch nicht immer einfach die notwendigen Voraussetzungen zum Klonen zu schaffen.
Implementation
[Bearbeiten]Für die Umsetzung musst du folgendes implementieren:
- Ein Klone-Methode, welche eine vollständige Kopie [engl. deep clone] des Objekts zurückliefert.
- Instanzen der Objekte, welche später als Prototyp gelten.
- Create-Methoden oder ähnliches, welche bei der Anforderung eines Objekts den jeweilen Prototyp kopieren.
Verwandte Muster
[Bearbeiten]- Abstrakte-Fabrik-Muster: Eine abstrakte Fabrik kann Prototypen speichern, die bei Aufruf geklont und zurückgegeben werden. Das Prototypmuster kann anstelle einer der Produkthierarchie ähnlichen konkrete-Fabrik-Hierarchie benutzt werden.
- Kompositionsmuster
- Dekorierermuster
Weblinks
[Bearbeiten]Wikipedia: Prototyp (Entwurfsmuster)
Adapter
Adapter
[Bearbeiten]Ein Adapter passt die Schnittstelle einer (externen) Funktionalität an.
Zweck
[Bearbeiten]Es wird ermöglicht eine Anwendung gegen ein Interface zu schreiben, welches auf eine externe Funktionalität (z.B. Bibliothek eines Fremdherstellers) verweist. Dabei wird eine Unterklasse dieses Interfaces verwendet, um die externe Klasse anzusteuern. Notwendig ist dies beispielsweise dann, wenn auf eine Bibliothek, deren Parametertypen nicht mit dem restlichen Programm vereinbar sind, zugegriffen werden soll. In einem solchen Fall kann ein Adapter, der eine Konvertierung vornimmt und den Anruf im Anschluss weiterleitet, Abhilfe schaffen.
UML
[Bearbeiten]Entscheidungshilfen
[Bearbeiten]Wenn die Funktionalität extern bereits zur Verfügung steht, jedoch nicht zu der Funktionsweise bzw. zum internen Aufbau (z.B. Namensgebung, Parametertypen) passt, dann kann ein Adapter eingesetzt werden. Allerdings sollte im Einzelfall geprüft werden, ob stattdessen nicht ein Refactoring der bereits bestehenden Funktionalität dem Einsatz eines Adapters vorzuziehen wäre - was natürlich nur dann möglich ist, wenn der Quellcode vorliegt.
Implementation
[Bearbeiten]- Ein Interface, welches die Funktionalität des Adapters definiert.
- Eine konkrete Implementierung des Adapters, welche sich der externen Funktionalität bedient, um die angebotene Schnittstelle zu realisieren.
Neben der Architektur, wie sie in obigem UML-Diagram skizziert ist, ist es auch möglich, dass der Adapter zusätzlich von der externen Klasse erbt.
Beispiele sind verfügbar in den Sprachen
Verwandte Muster
[Bearbeiten]Eine Brücke (engl. bridge) kann verwendet werden, um flexibel zwischen verschiedenen Adaptern zu wechseln. Der Stellvertreter reicht Aufrufe ebenfalls weiter - allerdings mit dem Unterschied, dass dieser und die Klasse, die die eigentliche Funktionalität bereitstellt, dieselbe Schnittstelle aufweisen; der Mechanismus ist also dem des Adapters ähnlich, der Zweck jedoch ein anderer.
Weblinks
[Bearbeiten]Wikipedia: Adapter (Entwurfsmuster)
Bridge
Brücke
[Bearbeiten]Eine Brücke (eng. bridge) gehört zur Kategorie der Strukturmuster (Structural Patterns). Das Muster dient zur Trennung der Implementierung von ihrer Abstraktion, der Schnittstelle, wodurch beide unabhängig voneinander verändert werden können.
Verwendung
[Bearbeiten]Problem
[Bearbeiten]Normalerweise wird eine Implementierung durch Vererbung ihrer Abstraktion realisiert. Dies kann jedoch dazu führen, dass die Vererbungshierarchie sowohl konkrete als auch abstrakte Klassen ungeordnet beinhaltet. Die Hierarchie wird dadurch unübersichtlich und schwer wartbar.
Lösung
[Bearbeiten]Werden die Abstraktionen und die Implementierungen in zwei getrennten Hierarchien verwaltet, so gewinnen sie Übersichtlichkeit. Außerdem wird die Unabhängigkeit der Schnittstelle der Anwendung zu ihrer Implementierung gesteigert.
Allgemeine Verwendung
[Bearbeiten]Eine Brücke findet Anwendung, wenn
- Abstraktion und Implementierung unabhängig voneinander erweiterbar sein sollen,
- eine dauerhafte Verbindung zwischen Abstraktion und Implementierung verhindert werden soll,
- Änderungen der Implementierung ohne Auswirkungen auf die Schnittstelle des Klienten sein sollen,
- die Implementierung vor dem Klienten verborgen bleiben soll oder
- die Implementierung von verschiedenen Klassen gleichzeitig genutzt werden soll.
UML-Diagramm
[Bearbeiten]Akteure
[Bearbeiten]- Abstraktion (im Beispiel: List)
- definiert die Schnittstelle der Abstraktion
- hält eine Referenz zu einem Implementierer
- SpezAbstraktion (im Beispiel: SortedList)
- erweitert die Schnittstelle
- Implementierer (im Beispiel: ListImpl)
- definiert die Schnittstelle der Implementierung
- kann sich von Schnittstelle der Abstraktion erheblich unterscheiden
- KonkrImplementierer (im Beispiel: ArrayList)
- enthält eine konkrete Implementierung durch Implementierung der Schnittstelle
Vorteile
[Bearbeiten]- Abstraktion und Implementierung werden entkoppelt.
- Die Implementierung ist während der Laufzeit dynamisch änderbar.
- Die Erweiterbarkeit von Abstraktion und Implementierung ist verbessert.
- Durch Angabe eines Parameters bei der Erzeugung einer Abstraktion kann die Implementierung gewählt werden.
- Für den Klienten wird die Implementierung vollständig versteckt.
- Vermeidung einer starken Vergrößerung der Anzahl der Klassen.
Nachteile
[Bearbeiten]- Keine Implementierung von Funktionalität in abstrakten Klassen erlaubt.
Beispiel
[Bearbeiten]...
Verwandte Entwurfsmuster
[Bearbeiten]Zum Erzeugen der Brücke kann eine Abstrakte Fabrik verwendet werden.
Ein Adapter ist der Brücke ähnlich. Jedoch dient der Adapter einer nachträglichen Anpassung einer Klasse an eine Anwendung, während die Brücke eine gezielte Designentscheidung ist.
PAC
Darstellung-Abstraktion-Steuerung
[Bearbeiten]Darstellung-Abstraktion-Steuerung (engl. Presentation-Abstraction-Control, PAC) bezeichnet ein Architekturmuster zur Strukturierung von interaktiven Softwaresystemen. Es gehört somit zu einer dem Entwurfsmuster übergeordneten Kategorie.
Siehe auch
[Bearbeiten]
Decorator
Dekorierer
[Bearbeiten]Der Dekorierer (engl. decorator) wird dazu verwendet um zur Laufzeit Funktionalität zu einem Objekt hinzuzufügen.
Zweck
[Bearbeiten]Wenn man ein Objekt zur Laufzeit dynamisch um eine bestimmte Funktionalität erweitern will, d.h. man will, dass die Funktion, welche bei einem Methodenaufruf ausgeführt wird, um weitere Funktionalität ergänzt wird. Dies ist im GUI Bereich häufig der Fall. Wenn zum Beispiel die Funktion zum Zeichnen eines Zeichens um das Zeichnen eines Rahmens ergänzt werden soll, dann kann ein Dekorator verwendet werden.
UML
[Bearbeiten]Dieser Abschnitt wird dir die Notation in der UML zeigen. Dies wird hilfreich sein, wenn du mit modernen Entwicklungswerkzeugen (vorhandene) Projekte "begutachtest".
Entscheidungshilfen
[Bearbeiten]PRO:
- Die Aspektkonstruktion mit Hilfe des Dekorierermusters vereinfacht die Subsystem-Bildung. Werkzeuge und die Aspektdekorierer können in ein Subsystem, während die Materialeine in ein anderes untergebracht werden.
KONTRA:
Implementation
[Bearbeiten]Um den Dekorator zu verwenden braucht man:
- Ein Interface von welchem sowohl die Substance als auch der Dekorator ableiten.
- Einen Decorator welcher die Referenz zu exakt einem dieser Interfaces hält.
- Implementierungen der Interfacemethoden im Decorator, welche die Funktionalität halten und anschließend an die Substance verweisen.
- Eine Substance Klasse welche die basis Funktionalität beinhaltet.
Verwandte Muster
[Bearbeiten]Der Decorator ist eine Aggregation, die nur eine Referenz hält.
Weblinks
[Bearbeiten]Wikipedia: Dekorierer
Facade
Fassade
[Bearbeiten]Die Fassade (engl. facade oder façade) ist eine Schnittstelle für ein Paket.
Zweck
[Bearbeiten]Um den Aufbau innerhalb eines Pakets möglichst flexibel gestalten zu können, ist es meist vorteilhaft, wenn auf die internen Klassen nicht direkt zugegriffen werden kann. Stattdessen setzt man eine Fassade ein, welche alle externen Zugriffe auf das Paket an die entsprechenden Klassen weiterleitet; sie stellt also eine (stabile) Schnittstelle für das gesamte Paket dar. So kann die Implementierung einzelner Funktionen oder gar die gesamte interne Architektur geändert werden, ohne dass die externen Zugriffe ebenfalls entsprechend umgeschrieben werden müssten.
UML
[Bearbeiten]Entscheidungshilfen
[Bearbeiten]- Überschreitet die Architektur einer Bibliothek eine gewisse Komplexität oder ist mit häufigen Änderungen der internen Struktur zu rechnen, kann eine Fassade eingebaut werden, die eine stabile, einfache Schnittstelle zum Zugriff auf die Funktionen dieser Bibliothek bereitstellt.
- Wenn auf einzelne Instanzen "hinter" der Fasade zugegriffen werden muss, kann das leider nicht über die Fasade implementiert werden, bzw. es widerspricht ihrem eigentlichen Sinn.
Implementation
[Bearbeiten]Um eine Fassade zu verwenden, gilt es:
- Eine Fassadenklasse für das Paket bereitzustellen, welche Methoden für externe Zugriffe anbietet und Anforderungen an interne Klassen delegiert.
- Im Fassadenkonstruktor die gesamte Klassenstruktur des Paketes zu erzeugen.
- Interne Klassen gegen den Zugriff von außen zu schützen (z.B. durch Setzten entsprechender Modifier, wie
package
oderprotected
).
Verwandte Muster
[Bearbeiten]Eine Fassade wird meist als Singleton implementiert.
Weblinks
[Bearbeiten]Wikipedia: Fassade (Entwurfsmuster)
Flyweight
Fliegengewicht
[Bearbeiten]Das Fliegengewicht (engl. flyweight) dient dazu eine große Anzahl an Objekten zu verwalten, ohne eine Instanz von jedem Einzelnen zu erstellen.
Zweck
[Bearbeiten]Um von einer Klasse keine Vielzahl von Objekten mit ähnlichen Zuständen erzeugen zu müssen, werden repräsentative Instanzen in einem Pool gehalten, der bei Bedarf passende Referenzen liefert.
UML
[Bearbeiten]Dieser Abschnitt wird dir die Notation in der UML zeigen. Dies wird hilfreich sein, wenn du mit modernen Entwicklungswerkzeugen (vorhandene) Projekte „begutachtest“.
Entscheidungshilfen
[Bearbeiten]Wenn von einer Klasse zahlreiche Instanzen erzeugt werden sollen, die zumeist den gleichen oder nur wenige verschiedene Zustände annehmen, dann ist das ein Indiz dafür, dass diese als Fliegengewichte realisiert werden können.
Implementation
[Bearbeiten]Zur Implementierung werden benötigt:
- Eine Fabrikklasse welche den Pool verwaltet, auf Anforderung Objektreferenzen zurück gibt und ggf. Objekte neu erstellt und im Pool ablegt.
- Ein Interface der Fliegengewichte.
- Eine oder mehrere Implementierungen der Fliegengewichte.
Verwandte Muster
[Bearbeiten]Fliegengewichte werden gerne innerhalb eines Proxys verwendet, da so z. B. die Anzahl der Referenzen auf diese gezählt werden können.
Weblinks
[Bearbeiten]Wikipedia: Fliegengewicht (Entwurfsmuster)
Composition
Kompositum
[Bearbeiten]Das Kompositum (engl. composition) wird dazu verwendet einen Baum an Objekten darzustellen.
Zweck
[Bearbeiten]Wenn Objekte als Baum dargestellt werden sollen, so kann dieses Pattern verwendet werden. Dabei gibt es Objekttypen welche den Baum weiter verschachteln und solche die ein Blatt darstellen.
UML
[Bearbeiten]Entscheidungshilfen
[Bearbeiten]Hier wird auf das Pro und Kontra des Musters eingegangen. Das heißt wann macht es Sinn ein Muster einzusetzen bzw. wann solltest du es nochmals überdenken.
- Ein Blatt und ein Ast sehen durch ihr gemeinsames Interface immer gleich aus. Eine Unterscheidung muss explizit abgefragt werden. Für einen Algorithmus bedeutet das, er kann einfach durch einen Baum traversieren, solange bis keine weiteren Unterelemente mehr da sind. Die traversierung ist aber bedingt duch die Traversierungsmöglichkeit, heißt im Klartext, dass auch ein Blatt traversiert werden kann, aber keine Unterelemente bietet, es hat somit die gleiche Schnittstelle wie ein Ast. Sollen die Schnittstellen unterschieden werden, so muss explizit der Typ erfragt werden, wofür man wiederum keine gemeinsame Basisklasse benötigt.
Implementation
[Bearbeiten]Zur Implementierung dieses Pattern gehören:
- Eine Schnittstelle, welche von allen Klassen des Baumes implementiert wird.
- Mindestens eine Nicht-Blatt-Klasse, welche eine oder mehrere Referenzen zu der Schnittstelle hält.
- Eine oder mehrere Blatt-Klassen, welche keine weiteren Referenzen halten.
Verwandte Muster
[Bearbeiten]Der Dekorator stellt ein Kompositum dar welche jeweils nur eine Referenz hält. Auch kann der Dekorator zur Erweiterung eines Blattes verwendet werden.
Weblinks
[Bearbeiten]Wikipedia: Kompositum (Entwurfsmuster)
ModelViewController
Vorwort
[Bearbeiten]Dieses Entwurfsmuster realisiert die Trennung zwischen Daten (Model), Darstellung (View) und Programmlogik (Funktionalität, business logic) (Controller). Es ist umstritten, ob es ein Entwurfsmuster oder ein Muster der höheren Kategorie, sprich ein Architekturmuster ist.
Voraussetzungen
[Bearbeiten]- Verständnis der Grundprinzipien objektorientierten Designs und objektorientierter Konzepte, insbesondere Kapselung.
- Verständnis des Beobachter-Musters.
Beispiel für einen Anwendungsfall
[Bearbeiten]Wir betrachten die Implementierung einer Anwendung, mit der das Betrachten von Aktienkursen (das heißt, deren Zahlenform) möglich ist. Die Kursdaten stammen von einem Server im Netz, mit dem das Programm kommuniziert.
Pflichtenheft
[Bearbeiten]Folgendes soll die Anwendung tun:
- Den Kurs der ausgewählten Aktie holen und anzeigen; ändert sich die Auswahl, so soll der Kurs der neuen Aktie geladen werden.
- Immer wieder nach einem gewissen Intervall (z.B. 30 Sekunden) soll der Kurs aktualisiert, also der aktuelle vom Server geholt werden.
Design
[Bearbeiten]Es ist durchaus möglich, die Anwendung in einem Programmstück zu schreiben; nur ist es designtechnisch nicht so schön, insbesondere nicht so flexibel. Des Weiteren ließe sich das Programm eigentlich gut aufteilen:
- Daten
- Der aktuelle Kurs der Aktie, z.B. eine Zahl.
- Präsentation
- Die Präsentation des Kurses, ein Element der Benutzeroberfläche.
- Das Auswahlfeld zur Auswahl der zu betrachtenden Aktie, ein Element der Benutzeroberfläche.
- Programmlogik
- Der Programmteil, der den Kurs holt, z.B. als Programmbibliothek.
- Der Programmteil, der in bestimmten Intervallen den Kurs aktualisiert, als eigener Thread.
Dieses Design trennt das Programm in einzelne, einfacher zu überschauende Teile. Das Design verwendet dabei das Model-View-Controller-Muster.
Prinzipien
[Bearbeiten]Model-View-Controller trennt eine Anwendung in diese drei Teile auf:
- Model
- Daten. Die Daten an sich, mit begrenzten Informationen bezüglich ihrer Darstellung.
- View
- Präsentation der Daten. Abhängig vom Model und von den Controllern (sofern vorhanden).
- Controller
- Management der Daten. Abhängig vom Model, jedoch nicht von den Views.
Meist wird für das Zusammenspiel von Model und Views das Beobachter-Muster verwendet, mit den Views als Beobachter des Models.
Daher ergeben sich typische Abläufe in Programmen mit MVC:
- Ein View kann den Controller veranlassen, Dinge zu tun.
- Der Controller kann das Model aktualisieren.
- Das aktualisierte Model kann seine Beobachter, also die Views, dazu veranlassen, sich zu aktualisieren.
Das Beispiel wurde bereits nach dem MVC-Schema aufgeteilt. In diesem Fall wird dadurch folgendes passieren können:
- Das Aktienauswahlfeld (Teil des View) gibt bei einer Auswahländerung dem Controller den Auftrag, den Kurs der neuen Aktie zu laden.
- Der Thread, der die automatische Aktualisierung verwaltet, gibt in regelmäßigen Abständen dem Controller den Auftrag, den Kurs der gerade gewählten Aktie zu laden.
- Der Controller kommuniziert mit den Server und schreibt letztendlich den neuen Kurs in das Objekt mit der Variable mit dem aktuellen Kurs (Model).
- Immer wenn sich das Model ändert, benachrichtigt es alle seine Beobachter (in diesem Fall die Präsentation des Kurses, Teil des View), so dass sich diese dann aktualisieren, also den neuen Wert anzeigen.
Vorteile
[Bearbeiten]- Ein Model kann ohne weiteres mehrere Views unterstützen, sofern sie das Beobachter-Muster verwenden.
- Ein Model kann ohne weiteres mehrere Controller unterstützen. Das wird insbesondere mit mehreren Views dahingehend verwendet, dass ein Model eine Zahl von View-Controller-Paaren besitzen kann. (Insbesondere kann auch Wikibooks, in Verbindung mit Webbrowsern, als eine Anwendung dieser Art gesehen werden - auch wenn die Komponenten hier über mehrere Rechner verteilt sind.)
- Die Komponenten lassen sich beliebig verändern - sofern ihre Schnittstellen gleich bleiben, ist keine Anpassung der anderen Komponenten notwendig.
Nachteile
[Bearbeiten]- Für kleinere Anwendungen ergibt sicht oftmals nicht unerheblicher Overhead
- Eine klare, eindeutige Trennung zwischen den Schichten ist oft nicht möglich.
Anwendungen
[Bearbeiten]Das Entwurfsmuster wird generell häufig eingesetzt, vorallen aufgrund seiner Flexibilität. Einzelne Komponenten können leicht verändert oder sogar ausgetauscht werden, ohne dass die anderen Komponenten davon betroffen sind, solange das Interface identisch bleibt. Dies erlaubt zum Beispiel den Wechsel zwischen verschiedenen Datenbanksystemen durch Manipulation des Models, oder die Möglichkeit Ausgaben für verschiedene Medien zu generieren durch Austausch der View-Schicht.
Weblinks
[Bearbeiten]Wikipedia: Model View Controller
NullPattern
Nullobjekt
[Bearbeiten]Das Nullobjekt (engl. Null Object pattern) erstellt einen Algorithmus in Form einer leeren Implementation.
Zweck
[Bearbeiten]Ziel dieses Entwurfsmusters ist es, eine leere Implementation bereitzustellen, womit Abfragen für den Fall, dass das Objekt null
ist, entfallen.
UML
[Bearbeiten]Entscheidungshilfen
[Bearbeiten]Dieses Strukturmuster wird vor allem in Zusammenhang mit dem Muster Schablonenmethode verwendet, um zum einem unnötige if
-Abfragen zu vermeiden und zum anderen den dadurch resultierenden Performance-Overhead zu negieren.
Implementation
[Bearbeiten]Für die Umsetzung muss folgendes implementiert werden.
- Eine Klasse mit mind. einer Methode.
Konkrete Hinweise zur Implementation finden sich zu den Programmiersprachen:
Verwandte Muster
[Bearbeiten]
Proxy
Stellvertreter
[Bearbeiten]Der Delegierer oder Stellvertreter (engl. proxy) kontrolliert den Zugriff auf ein Objekt durch ein vorgelagertes Stellvertreterobjekt.
Zweck
[Bearbeiten]Die Idee des Stellvertreter-Musters besteht darin, dem tatsächlichen Objekt einen Stellvertreter vorzulagern, der Methodenaufrufe entgegennimmt und diese - je nach Verwendungszweck ergänzt durch entsprechende eigene Aktionen - dann an das eigentliche Objekt weiterreicht. Dieser Vorgang vollzieht sich für den Benutzer transparent, d.h. er bleibt von ihm unbemerkt. Erreichen lässt sich dies, indem das echte Objekt und sein Stellvertreter die gleiche Schnittstelle offerieren.
Hier einige typische Situationen, in denen sich dieses Muster als nützlich erweisen kann:
- Caching: Angenommen die eigentliche Operation ist sehr teuer; ein Stellvertreter könnte in einem Cache Berechnungsergebnisse gespeichert halten und nur bei Bedarf (d.h. wenn sich das passende Ergebnis noch nicht im Cache befindet) die Berechnung durch das tatsächliche Objekt veranlassen.
- Synchronisation: Der Stellvertreter synchronisiert die Zugriffe auf das tatsächliche Objekt. Hilfreich, wenn selbiges nicht für Multithread-Umgebungen ausgelegt ist.
- Protokollierung: Zugriffe werden protokolliert (z.B. während eines Testlaufs)
- Remote-Proxy: Das tatsächliche Objekt befindet sich in einem anderen Adressraum; der Stellvertreter gibt den Aufruf über das Netzwerk weiter.
- Zugriffsschutz: Besonders nützlich, wenn unterschiedliche Benutzer verschiedene Zugriffsrechte auf das zu schützende Objekt haben sollen.
- Virtueller Stellvertreter: Erzeugt teure Objekte erst auf Verlangen (on demand, verzögertes Laden)
Stellvertreter können bei Bedarf auch kaskadiert eingesetzt werden.
UML
[Bearbeiten]Entscheidungshilfen
[Bearbeiten]Die obige Auflistung möglicher Szenarien ließe sich fast beliebig erweitern; generell kann ein Stellvertreter nützlich sein, wenn es für den Zugriff auf das tatsächliche Objekt spezielle Anforderungen gibt, wobei diese Funktionalität aber nicht von diesem selbst bereitgestellt werden soll.
Implementation
[Bearbeiten]Um einen Stellvertreter zu implementieren braucht man:
- Eine dem Stellvertreter und dem tatsächlichen Objekt gemeinsame Schnittstelle
- Eine Implementation des tatsächlichen Objektes und
- Ein Stellvertreterobjekt, welches Methodenaufrufe an das tatsächliche Objekt weiterleitet. Diese Weiterleitung kann durch entsprechende Funktionalität (siehe Auflistung weiter oben) ergänzt werden.
Beispiele gibt es für die folgenden Sprachen:
Verwandte Muster
[Bearbeiten]Der Dekorator kann als ein Spezialfalls eines Stellvertreters betrachtet werden. Außerdem zu nennen wäre der Adapter: Auch dieser leitet Aufrufe an ein "echtes Objekt" weiter - allerdings geschieht dies nicht transparent, da der Adapter die Schnittstelle des tatsächlichen Objektes nicht übernimmt, sondern seine eigene zur Verfügung stellt.
Observer
Der Beobachter (engl. Observer) dient der losen Kopplung zweier Objekte.
Zweck
[Bearbeiten]Ziel ist es, das beobachtende Objekt über Änderungen im beobachteten Objekt (auch Subjekt genannt) zu informieren. Durch die lose Kopplung ist es möglich, verschiedenste Objekte miteinander zu verbinden. Der Ereignismechanismus (Event) in grafischen Oberflächen basiert auf diesem Prinzip - meist in der Erweiterung des Model-View-Controller-Musters.
UML
[Bearbeiten]Klassendiagramm
[Bearbeiten]Sequenzdiagramm
[Bearbeiten]Entscheidungshilfen
[Bearbeiten]- Wenn Instanzen verschiedenster Klassen über Änderungen informiert werden sollen, ist der Beobachter die richtige Wahl.
- Von einem Einsatz sollte abgesehen werden, wenn lediglich zwei Instanzen miteinander kommunizieren müssen, da der Overhead für die Implementierung des Beobachter-Musters zu groß wäre.
Implementierung
[Bearbeiten]An der Umsetzung sind mehrere Klassen beteiligt: Beobachter-Interface und seine Erben sowie der Beobachtete (Subjekt). Das Subjekt muss Methoden bereitstellen, über die neue Beobachter registriert oder bereits registrierte Beobachter wieder entfernt werden können. Hinzu kommt eine Methode, die dazu dient, die registrierten Beobachter über Änderungen zu informieren (im UML-Diagram ist diese Methode mit benachrichtigen()
bezeichnet). Die Beobachter ihrerseits müssen (mindestens) eine Methode zur Verfügung stellen, über die sie bei Änderungen informiert werden können (im UML-Diagram mit aktualisiere()
bezeichnet).
Tritt nun eine Zustandsänderung im beobachteten Objekt auf, muss dessen benachrichtigen()
-Methode die aktualisiere()
-Methoden aller registrierten Beobachter aufrufen. Diese werden üblicherweise den neuen Zustand des Beobachteten erfragen (subjekt.gibZustand()
in obigem UML-Diagram) und entsprechend reagieren, z.B. durch Neuzeichnen einer Oberflächenkomponente in der Model-View-Controller-Architektur.
Neben der vorgestellten Variante gibt es noch eine weitere, die beispielsweise im GUI-Teil der Java-Bibliothek verwendet wird: Bei dieser Variante verfügt die aktualisiere()
-Methode des Beobachters noch über einen zusätzlichen Parameter, ein Event-Objekt, das Informationen über den neuen Zustand und üblicherweise auch eine Referenz auf den Urheber der Änderung, also das Subjekt, das den Aufruf der aktualisiere()
-Methode veranlasst hat, enthält (letzteres ist wichtig, da ein Beobachter bei dieser Variante bei mehreren beobachteten Objekten registriert werden kann). Anstatt sich den neuen Zustand dann beim Subjekt selbst zu besorgen, kann diese Information aus dem Event-Objekt ausgelesen werden.
Konkrete Implementationen finden sich zu den Programmiersprachen:
Konsequenzen
[Bearbeiten]- Abstrakte Kopplung von Subject und Beobachter: z.B. können sie in verschiedenen Schichten der Architektur sein. Wäre dies nicht der Fall dann würde es Kommunikation über Schichten hinweg geben, was das Schichtenprinzip verletzen würde.
- Broadcast-Kommunikation wird möglich: Die Benachrichtigung muss den Empfänger nicht kennen.
- Unerwartete Updates: Der Beobachter weiß nicht, wie viele weitere Beobachter es gibt, und kann daher auch nicht einschätzen, wie aufwändig eine Statusänderung ist.
Verwandte Muster
[Bearbeiten]- Das Model-View-Controller-Muster basiert auf dem Prinzip des Beobachters.
Weblinks
[Bearbeiten]- Wikipedia: Beobachter (Entwurfsmuster)
- Weitere detaillierte Erklärung des Observer-Patterns (Beobachter-Muster)
Visitor
Besucher
[Bearbeiten]Der Besucher gehört zu der Kategorie der Verhaltensmuster (Behavioural Patterns). Es dient zum Kapseln von Operationen, die auf Elementen einer Objektstruktur ausgeführt werden. Neue Operationen können dadurch ohne Veränderung der betroffenen Elementklassen definiert werden.
Verwendung
[Bearbeiten]Die Integration verschiedener nicht miteinander verwandter Operationen in die Klassen einer Objektstruktur gestaltet sich oft schwierig. Bei der Erweiterung um neue Operationen müssen alle Klassen erweitert werden. Das Besuchermuster lagert die Operationen in externe Besucherklassen. Dazu müssen die zu besuchenden Klassen jedoch eine Schnittstelle zum Empfang eines Besuchers definieren.
Generell empfiehlt sich die Verwendung von Besuchern, wenn
- viele unterschiedliche, nicht verwandte Operationen auf einer Objektstruktur realisiert werden sollen,
- sich die Klassen der Objektstruktur nicht verändern,
- häufig neue Operationen auf der Objektstruktur integriert werden müssen oder
- ein Algorithmus über die Klassen einer Objektstrukur verteilt arbeitet, aber zentral verwaltet werden soll.
UML-Diagramm
[Bearbeiten]Akteure
[Bearbeiten]- Besucher
- definiert für jede Klasse konkreter Besucher eine Besuchsfunktion
- KonkreterBesucher
- implementiert Besuchsfunktionen
- jede Besuchsfunktion ist ein Fragment des Algorithmus, welcher auf der gesamten Objektstruktur angewendet wird
- lokaler Zustand dient als Kontext für den Algorithmus
- Element
- definiert eine Schnittstelle zum Empfang eines Besuchers
- KonkretesElement
- implementiert den Empfang eines Besuchers
- ObjektStruktur
- Kollektion oder zusammengesetzte Objektstruktur.
Pro
[Bearbeiten]- Neue Operationen lassen sich leicht durch die Definition neuer Besucher hinzufügen.
- Verwandte Operationen werden im Besucher zentral verwaltet und von besucherfremden Operationen getrennt.
- Besucher können über mehreren Klassenhierarchien arbeiten.
Kontra
[Bearbeiten]- Die gute Erweiterungsmöglichkeit der Klassen von Besuchern muss mit einer schlechten Erweiterbarkeit der Klassen der konkreten Elemente erkauft werden. Müssen neue konkrete Elemente hinzugefügt werden, so führt dies dazu, dass enorm viele Besucher-NimmEntgegegen-Methoden implementiert werden müssen.
- Neue Klassen konkreter Elemente erfordern Erweiterungen in allen Besuchern.
Beispiele
[Bearbeiten]Virtuelles Reisebüro
[Bearbeiten]Ein Reiseveranstalter bietet seinen Kunden verschiedene Busreisen, Ferienhäuser und Mietwagen an. Jedem Objekt sind eine Beschreibung und eine Preiskategorie für Sommer und Winter zugewiesen. Die Preise der Kategorien sind in einem Preismodul gespeichert. Bei Ferienhäusern sind darüber hinaus Bilder, bei Mietwagen technische Daten abgelegt. Sowohl die Klassen für Busreisen, Ferienhäuser und Mietwagen, als auch das Preismodul bieten eine Schnittstelle zum Empfang eines Besuchers. Das Preismodul ist außerhalb der Klassenhierarchie von Busreisen, Ferienhäusern und Mietwagen.
Ein Kunde kann sich nun eine Reise zusammenstellen. Fragt er dann nach dem Gesamtpreis, so besucht ein Besucher zunächst die interessierenden Objekte, fragt die jeweilige Kategorie ab. Für jede Kategorie verwaltet er einen lokalen Zähler. Zuletzt besucht er das Preismodul und berechnet auf Grund der dort abgelegten Preise und seiner lokal gesammelten Informationen den Gesamtpreis.
Entscheidet sich der Kunde, die Reise zu buchen, kann ein anderer Besucher eine Reisebestätigung erstellen. Dazu besucht er wieder die den Kunden interessierenden Objekte und das Preismodul. Sein lokaler Zustand besteht aus einem Dokument, das er gemäß den Informationen der Objekte gestaltet. Bei allen Objekten listet er zunächst die Beschreibung und die Preiskategorie auf, bei Mietwagen zusätzlich die technischen Daten. Beim Besuch des Preismoduls ergänzt er dann die einzelnen Beschreibungen um die konkreten Preise.
Beide Besucher übergreifen Klassenhierarchien, da sie sowohl auf der Klassenhierarchie der buchbaren Reiseelemente als auch auf dem Preismodul arbeiten.
Besucher in Compiler (Übersetzern)
[Bearbeiten]Im Compilerbau liegt nach der syntaktischen Analyse meist ein abstrakter Syntaxbaum vor. Ein solcher Baum lässt sich durch Klassen für die verschiedenen Elemente und Verwendung von Aggregationen gut als Objektstruktur beschreiben. Auf dieser Objektstruktur kann man nun einen allgemeinen Besucher definieren, der den Baum traversiert. Dazu werden bei der Implementierung der Besuchsfunktion für eine Elementklasse des Baums die aggregierten Elemente nacheinander besucht. Von diesem allgemeinen Besucher lassen sich nun verschiedene Besucher ableiten, die unterschiedliche Operationen auf dem abstrakten Syntaxbaum implementieren.
In einem Besucher lässt sich die semantische Analyse realisieren. Dazu besucht dieser die Elemente des Baums und erweitert die Symboltabelle um Informationen zu Typen von Variablen und Routinen oder überprüft Ausdrücke unter Einbeziehung der Symboltabelle, ob sie wohlgetypt sind. Je nach den Eigenschaften der Quellsprache muss die Sammlung von Informationen und die Typprüfung auch auf zwei Besucher verteilt werden.
Ein weiterer Besucher kann dann die Synthese des Zielcodes realisieren. Auch dieser besucht dazu die einzelnen Elemente und sammelt die Zielcodefragmente in seinem lokalen Zustand. Abhängig von der Klasse des besuchten Elements kann er dann bereits gesammelte Fragmente zu größeren kombinieren.
Weitere Besucher können Debuginformationen sammeln oder Codeoptimierungen auf Quellcodebasis durchführen. Alle Besucher können dabei auf die Besuchsfunktionen des allgemeinen Besuchers zurückgreifen, wenn ein Element ohne weitere Operationen nur traversiert werden soll. Auch der Zielcode kann zunächst wiederum in einer Baumstruktur erzeugt werden, um dann verschiedene Optimierungen in unterschiedlichen Besuchern zu realisieren.
Interpreter
Interpreter
[Bearbeiten]Der Interpreter gehört zu der Kategorie der Verhaltensmuster (behavioural patterns).
Das Interpretermuster definiert eine Repräsentation für die Grammatik einer Sprache und die Möglichkeit Sätze dieser Sprache zu interpretieren.
Verwendung
[Bearbeiten]Wenn ähnliche Probleme oft genug gelöst werden müssen, ist es häufig sinnvoll das Problem mit einer einfachen Sprache zu beschreiben. Beispiele für ein solches Problem sind:
- das Auswerten von regulären Ausdrücken und
- die Berechnung von logischen oder mathematischen Formeln
UML-Diagramme
[Bearbeiten]Klassendiagramm
[Bearbeiten]Bestandteile
[Bearbeiten]Die SymbolSchnittstelle definiert die Funktion interpretiere, die von allen abgeleiteten Klassen implementiert wird und das entsprechende Symbol auswertet.
Ein TerminalSymbol steht für einen festen Wert innerhalb des Satzes, z. B. einen Zahlenwert innerhalb einer mathematischen Formel.
Ein NichtTerminalSymbol repräsentiert eine Verknüpfung zweier Symbole, z. B. Addition oder Multiplikation. Sowohl Terminalsymbol als auch Nichtterminalsymbole können miteinander verknüpft werden.
Den Satz, der interpretiert werden soll, baut der Klient in Form eines Syntaxbaumes aus den Nichtterminal- und Terminalsymbolen zusammen.
Im Kontext werden die konkreten Werte der Terminalsymbole gekapselt, mit denen der Satz interpretiert werden soll.
Vorteile
[Bearbeiten]- Die Grammatik kann leicht geändert oder erweitert werden.
- Der selbe Satz oder Ausdruck kann durch Ändern des Kontextes immer wieder auf neue Art und Weise interpretiert werden.
Nachteile
[Bearbeiten]Für komplexe Grammatiken und sehr große Sätze ist das Interpretermuster ungeeignet, da
- die Klassenhierachie zu groß wird und
- die Effizienz bei großen Syntaxbäumen leidet.
Sollen komplexe Grammatiken verarbeitet werden, eignen sich Parsergeneratoren besser. Große Syntaxbäume werden üblicherweise in andere Strukturen konvertiert und z. B. mit Hilfe von Zustandsautomaten bearbeitet.
Verwandte Entwurfsmuster
[Bearbeiten]Der Syntaxbaum wird durch ein Kompositum beschrieben.
Ein Besucher kann das Verhalten aller Nichttermalsymbole in sich kapseln, um die Anzahl der Klassen zu verringern und/oder das Verhalten dieser austauschbar zu gestalten.
Mit Hilfe des Fliegengewicht können Terminalsymbole gemeinsam genutzt werden.
Ein Iterator kann verwendet werden, um den Syntaxbaum zu traversieren.
Iterator
Iterator
[Bearbeiten]Greife auf alle Elemente einer Sammlung zu.
Zweck
[Bearbeiten]Häufig besteht die Notwendigkeit auf alle Elemente einer Sammlung (engl. Collection) (z.B. eines Arrays oder einer Liste) zuzugreifen - beispielsweise um diese an eine Funktion zu übergeben. Der Iterator ermöglicht es, dies sequentiell (d.h. in einer bestimmten Reihenfolge) zu tun, ohne dabei wissen zu müssen, wie die Sammlung aufgebaut ist.
Beispielsweise könnten natürliche Zahlen (oder andere Objekte, auf denen eine Ordnung definiert ist) in einer Liste oder einem Binärbaum gespeichert abgelegt sein. Wollen wir die bisher abgelegten Zahlen z.B. mit einer besonderen Formatierung ausgeben, stehen wir vor dem Problem, dass sich die Algorithmen zum Traversieren (d.h. Durchlaufen) eines Baumes und einer Liste stark unterscheiden - wir müssten in unserem Programm also eine Abfrage einbauen, die - je nachdem ob wir es mit einem Baum oder einer Liste zu tun haben - zu unterschiedlichen Unterprogrammen springt. Der Einsatz eines Iterators macht diese umständliche Vorgehensweise überflüssig: Über die eigentliche Sammlung wird einfach ein passender Iterator "gestülpt" und wir können nacheinander auf die einzelnen Zahlen zugreifen, indem wir uns der (einfachen und immer gleichen) Schnittstelle des Iterators bedienen (polymorphe Traversierung).
Weiterhin erlaubt das Iterator-Muster die mehrfache und gleichzeitige Traversierung derselben Sammlung: Dazu reicht es aus, mehrere Iterator-Objekte zu erzeugen!
UML
[Bearbeiten]Entscheidungshilfen
[Bearbeiten]Wenn auf alle Elemente verschiedenartiger Sammlungen zugegriffen werden soll, kann man das Iterator-Muster verwenden. Meist verursacht es jedoch Probleme, wenn die Sammlung während der Traversierung geändert werden soll (insbesondere falls diese Änderungen die Anzahl der Elemente in der Sammlung beeinflussen). Diesbezüglich existieren jedoch verschiedenen Lösungsansätze. Die 2 trivialsten Möglichkeiten sind die folgenden. Die einfachste Variante ist vor dem Durchlauf eine Kopie der Daten anzulegen und auf diesen die Traversierung durchzuführen("Snapshot"-Variante). Eine dynamische Lösung der Problematik würde bei Veränderungen dem Iterator diejenigen Aktionen mitteilen, um dann, falls es notwendig ist, darauf zu reagieren. Problematisch ist der Zugriff gegebenenfalls auch in Multithread-Umgebungen.
Implementation
[Bearbeiten]Kernstück der Implementierung ist eine einheitliche Schnittstelle, die alle für die Traversierung benötigten Funktionen bereitstellt:
start()
springt zum ersten Element der Sammlung. Üblicherweise ist der Aufruf dieser Funktion beim ersten Durchlauf nicht erforderlich; soll aber in der Mitte abgebrochen und wieder von vorne begonnen werden, leistet sie das Gewünschte.gibElement()
greift auf das aktuelle Element zu,weiter()
springt zum nächsten Element,gibtWeiteres()
prüft, ob das letzte Element erreicht ist oder noch weitere vorhanden sind.
Diese Schnittstelle muss nun für jede Sammlung, für die ein Iterator verfügbar sein soll, getrennt implementiert werden.
Dazu wird meistens intern ein Zeiger auf das aktuelle Element, so wie es in der zugrundeliegenden Sammlung enthalten ist, gespeichert; zusätzlich bedarf es eines Algorithmus, der für ein Element der Sammlung dessen Nachfolger bestimmt. Dieser Algorithmus hängt natürlich in besonderer Weise von der Art der Sammlung ab, für die der Iterator implementiert werden soll: Bei einer einfach verketteten Liste wird häufig geradewegs der Verkettung gefolgt, bei einem Binärbaum hat man bereits die Wahl zwischen einer Pre-, In- oder Post-Order-Traversierung und bei Hashtabellen ist von Fall zu Fall zu entscheiden.
Weitergehende Iteratoren ermöglichen auch die Vor- und Rückwärtsnavigation durch die Sammlung, oder beeinflussen die Reihenfolge nach bestimmten Sortierkriterien.
Man kann des Weiteren bei der Implementierung verschiedene Varianten eines Iterators unterscheiden. Bezüglich der Kontrolle der Iteration sind 2 Varianten denkbar:
- Externer Iterator( Die Traversierung ist im Aggregat enthalten)
- Interner Interator( Die Traversierung wird im Iterator selbst durchgeführt)
Beispielimplementationen sind verfügbar in
Verwandte Muster
[Bearbeiten]- Der Iterator wird oft verwendet, um innerhalb des Besucher-Musters alle Objekte zu besuchen.
- Kompositum :Innerhalb rekursiver Datenstrukturen werden Iteratoren häufig benutzt
- Fabrik Methode: polymorphe Iteratoren basieren oft auf der Fabrik Methoden Muster um die jeweilige Unterklasse des zu benutzenden Iterators zu instanzieren
- Memento : Mementos können benutzt werden um einen Status einer Iteration zu speichern
Weblinks
[Bearbeiten]Wikipedia: Iterator (Entwurfsmuster)
Command
Das Kommando oder der Befehl (engl. Command) ist ein Verhaltensmuster (Behavioral Patterns). Es dient zum Kapseln von Anfragen als Kommando-Objekte, um damit Empfänger zu parametrisieren. Anfragen können dabei in Warteschlangen gestellt, aufgezeichnet und später ggf. auch wieder rückgängig gemacht werden.
Auch bekannt als: Aktion, Transaktion (Action, Transaction)
Zweck und Verwendung
[Bearbeiten]Das Kommando-Muster kapselt einen (parametrierbaren) Befehl in ein Objekt um den Aufruf eines Befehls von dessen Ausführung zu entkoppeln. Genauer definiert sich das Muster wie folgt:
- Objekte sollen mit einer auszuführenden Aktion, dem Befehl, parametrisiert werden. Das ist die objektoriertierte Entsprechung zu Rückruffunktionen (eng. callback function). (zB eine Schaltfläche in einer GUI soll mit einer Aktion verknüpft werden)
- Befehle, also auszuführende Aktionen, werden in einzelne Objekte gekapselt. Dies folgt dem objektorientierten Prinzip der Kapselung des Veränderbaren.
- Das Erstellen des Befehls und das tatsächliche Ausführen finden zu verschiedenen Zeiten oder in einem anderen Kontext (Thread, Prozess, Rechner) statt.
UML-Diagramm
[Bearbeiten]Akteure
[Bearbeiten]- Befehl (Command)
- Basisklasse aller Befehle
- definiert die Schnittstelle zum Ausführen des Befehls
- Konkreter Befehl (Concrete Command)
- speichert den zum Ausführen nötigen Zustand, darunter typischerweise auch einen Verweis auf den Empfänger
- implementiert die Befehlsschnittstelle
- Klient (Client)
- erzeugt einen konkreten Befehl und versieht ihn mit einem Verweis auf den Empfänger und allen anderen nötigen Informationen
- gibt dem Aufrufer eine Referenz auf den konkreten Befehl
- Aufrufer(Invoker)
- besitzt einen oder mehrere Verweise auf Befehle
- fordert diese Befehle bei Bedarf auf, ihre Aktion auszuführen
- Empfänger (Receiver)
- Der konkrete Befehl ruft Methoden des Empfängerobjektes auf, um seine Aktion auszuführen.
- An den Empfänger werden keine besonderen Anforderungen gestellt. Er muss nichts über die anderen Akteure wissen. Somit kann jede Klasse als Empfänger dienen.
Anwendungsfälle
[Bearbeiten]Da Befehle in Objekten gekapselt sind, können sie gespeichert, herumgereicht, gruppiert oder modifiziert werden. Dies erlaubt eine Reihe von interessanten Anwendungsmöglichkeiten:
- Implementation einer Warteschlange, bei der die Befehle nacheinander abgearbeitet werden. Dabei sind die Befehle lose an die ausführende Einheit gekoppelt, so dass ein Austauschen, Ändern etc. zur Laufzeit möglich ist.
- Implementation von Logging von Befehlen und einer Wiederherstellung nach einem Systemabsturz.
- Gruppierung von mehreren Befehlsobjekten in einem Makrobefehl, um verschiedene Aktivitäten gebündelt auszuführen.
- Implementation eines Rückgängig-Mechanismus (Undo). Bei jeder Ausführung werden die zur Umkehrung nötigen Daten im Befehls-Objekt gespeichert und das Objekt selber auf einem Stapel gesichert. Um das Gegenteil Wiederherstellen (Redo) zu implementieren, genügt ein zweiter Stapel für die rückgängig gemachten Befehle.
- Implementierung einer zeitliche Entkopplung zwischen Aufruf und Abarbeitung (asynchrone Abarbeitung).
- Parallelisierung zum Beispiel unter Zuhilfenahme eines Master/Worker-Musters: Die einzelnen Arbeitspakete, die von einem Threadpool parallel abgearbeitet werden, können als Kommandos implementiert werden.
Ideen hinter erweiterten Anwendungen
[Bearbeiten]Der Makro-Befehl:
- Ein Makro-Befehl gruppiert eine Menge von Aktionen und führt sie gemeinsam aus. Dabei sind die einzelnen Aktionen in den Befehlsobjekten gekapselt. Idee ist nun, in einem Makro-Befehlsobjekt mehrere Befehlsobjekte zu speichern und diesen in einer vorgegebenen Reihenfolge zu befehlen, die Aktionen auf dem ihnen zugeordnetem Empfängerobjekt auszuführen.
- Der Makro-Befehl implementiert ebenfalls die Schnittstelle "Abstrakter Befehl". Ergänzt wird der Makro-Befehl um eine Liste oder ein Array zur Speicherung einer Reihe von Befehlsobjekten. Bei der Initialisierung (oder später dynamisch zur Laufzeit) wird dem Makro-Befehl ein Array von Befehlsobjekten übergeben.
- In der Methode ausführen(), welche einen Befehl zu Abarbeitung der Aktionen bewegt, iteriert der Makro-Befehl durch das Befehlsarray und ruft für jedes der Befehlsobjekte die Methode ausführen() auf.
- Eine Rückgängig-Funktionalität kann auch hier einfach gewährleistet werden, indem der Makro-Befehl rückwärts durch das Befehlsarray iteriert und auf jedem gespeichertem Befehlsobjekt die rückgängig()-Methode aufruft, die den Zustand des Systems vor der letzten Ausführung der ausführen()-Methode wiederherstellt.
Rückgängig-Funktion für Benutzeroberflächen:
- Konkrete Befehle realisieren dann Aktionen wie Datei öffnen, Rückgängig oder Schreibmarke nach rechts
- Klienten sind die Applikation oder Dialoge.
- Aufrufer sind Schaltflächen, Menüpunkte oder Hotkeys.
- Empfänger sind die Applikation (Datei öffnen) oder das Dokument (Rückgängig, Einfügemarke nach rechts)
Vorteile
[Bearbeiten]- Auslösender und Ausführender sind entkoppelt. Dadurch können Erstellung und Ausführung der Befehle zu anderen Zeiten oder in anderen Kontexten (Threads, Prozesse, Rechner) stattfinden.
- Ausführenden können zur Laufzeit dynamisch neue Befehle übergeben werden.
- Befehlsobjekte können wie andere Objekte auch manipuliert werden (Verändern, Filtern, Zwischenspeichern, etc.).
- Befehlsobjekte können zu komplexen Befehlen bzw. Befehlsketten kombiniert werden (Makros, realisiert als Kompositum).
- Aufrufer sind (syntaktisch) nur von der Befehls-Basisklasse abhängig, nicht von deren konkreten Implementierung. Dies steigert die Wartbarkeit und Flexibilität.
Nachteile
[Bearbeiten]- Es wird für jedes Kommando eine neue Klasse benötigt. Dies kann sehr schnell zu einer großen Menge von Klassen führen.
- Bei sehr "kleinen" (schnell ausführbaren) Befehlen dominieren die Kosten für die Befehlsverwaltung im Gegensatz zu den Kosten der eigentlichen Befehle. Dies kann bei hohen Lasten und performancekritischen Anwendungen evtl. problematisch sein.
Implementation
[Bearbeiten]- Die Kommando-Klasse sollte als eine (abstrakte) Basisklasse implementiert werden (je nach sprachspezifischen Eigenschaften auch als Interface). Siehe UML-Diagramm. Die Applikation arbeitet prinzipiell nur mit der Basisklasse, während die einzelnen (konkreten) Kommando-Klassen nur bei der Instanzierung verwendet werden.
- Die Kommando-Basisklasse stellt je nach Applikation verschiedene Methoden bereit z.B.:
- execute() Diese Methode ist zwingend erforderlich und führt das Kommando aus.
- undo() Macht das bereits ausgeführte Kommando wieder rückgängig
- Von dieser allgemein gängigen Implementierung kann aber auch abgewichen werden. So kann die execute()-Funktion auch als Rückruffunktion (Delegate, callback function) implementiert werden.
- Dieses Muster kann in verschiedener Hinsicht erweitert werden:
- Einführen von Kommando-Typen zur Kategorisierung (zum Beispiel bei unterschiedlichen Abarbeitungsumgebungen verschiedener Kommandotypen). Vor diesem Schritt ist zu überprüfen, ob sich das gewünschte Verhalten auch durch Polymorphismus erreichen lässt!
- Einführung von Kommando-Prioritäten, so dass Kommandos in der die Warteschlange nach Priorität sortiert eingefügt und dementsprechend abgearbeitet werden
Beispiele
[Bearbeiten]Verwandte Muster
[Bearbeiten]- Makro-Befehle können als Kompositum implementiert werden.
- Das Memento-Entwurfsmuster kann die Objektzustände speichern, um eine Rückgängigfunktion zu vereinfachen.
Mediator
Vermittler
[Bearbeiten]Ein Vermittler (engl. Mediator) ist ein Software-Design-Pattern und wird in die Gruppe der Verhaltensmuster (Behavioral Patterns) eingeordnet, da es das Ablaufverhalten eines Programmes beeinflussen kann. Das Muster dient zum Steuern des kooperativen Verhaltens von Objekten, wobei Objekte nicht direkt kooperieren, sondern über einen Vermittler. Es stellt einer Reihe von Interfaces eines Subsystems ein vereinheitlichtes Interface zur Verfügung.
Verwendung
[Bearbeiten]Der Vermittler findet Anwendung, wenn
- Objekte in einem System auf komplexe Art und Weise kooperieren,
- die Wiederverwendung von Objekten durch den Bezug auf viele andere Objekte erschwert wird oder
- Objekte die Objekte, mit denen sie kooperieren nicht kennen können oder sollen.
Vereinheitlicht lässt sich zusammenfassen, dass Programme aus mehreren Klassen bestehen, die die Logik und das Informationsverhalten bestimmen. Vor dem Hintergrund der steigenden Anzahl der Klassen während der Entwicklung und des Refaktorierens ist zu Beachten, dass die Komplexität der Kommunikation zwischen den Klassen steigt. Und genau hier wird die Mediator-Klasse eingesetzt, da nur er detailierte Informationen über Methoden anderer Klassen hat und somit Anfragen von Klassen entgegen nimmt. Nach Annahme von Anfragen andere Klassen entscheidet der Mediator auf Grundlage seines Informationsstandes, welche anderen Klassen zusätzlich "benachrichtigt" werden müssen.
UML-Diagramm
[Bearbeiten]Akteure
[Bearbeiten]- Vermittler
- definiert Schnittstelle zur Kommunikation mit Kollegen
- KonkreterVermittler
- implementiert das kooperative Verhalten durch Koordination der beteiligten Kollegen
- kennt und verwaltet beteiligte Kollegen
- Kollege
- kennt seinen Vermittler
- kommuniziert mit seinem Vermittler statt mit anderen Kollegen
Vorteile
[Bearbeiten]- Die Koordination des kooperativen Verhaltens wird zentral verwaltet.
- Die Veränderung des kooperativen Verhaltens kann unabhängig von den beteiligten Kollegen umgesetzt werden. Solche Änderungen des Verhaltens können durch neue konkrete Vermittler erreicht werden. Dadurch wird die Unterklassenbildung eingeschränkt, da die Änderungen nicht in den verschiedenen konkreten Kollegen vorgenommen und dementsprechend viele neue Unterklassen gebildet werden müssen.
- Das Muster unterstützt eine lose Kopplung zwischen den Kollegen.
- Das Protokoll der Kollegen wird vereinfacht.
Nachteile
[Bearbeiten]Da der Vermittler ein Verhalten kapselt, das andernfalls auf mehrere Klassen verteilt wird, ist er selbst komplexer als die einzelnen Komponenten es gewesen wären. Es besteht die Gefahr, dass ein monolithischer Programmkomplex entsteht, der schwer wart- und erweiterbar ist.
Beispiel
[Bearbeiten]Ein typisches Beispiel für einen Vermittler ist ein Chatraum. Chatter melden sich beim Chatraum an und ab, um mit anderen Chattern kommunizieren zu können. Sie kommunizieren nicht direkt miteinander, sondern über den Chatraum. Verschiedene konkrete Chaträume können nun unterschiedliche Kommunikationsarten ermöglichen. So kann ein Chatraum Mitteilungen eines Chatters an alle Chatter weiterleiten, ohne dass der Chatter diese kennen muss. Ein anderer Chatraum kann hingegen nur die Kommunikation zwischen einzelnen Chattern ermöglichen. Möchte man die Kommunikation um eine Protokollierung erweitern, erweitert man dazu einen konkreten Chatraum, während die Chatter nicht betroffen sind.
Verwandte Entwurfsmuster
[Bearbeiten]- Kollegen können den Vermittler beobachten und umgekehrt.
Memento
Speichere den internen Zustand (Attribute) eines Objekts, um ihn gegebenenfalls zurückzusetzen.
Zweck
[Bearbeiten]Wenn man Attribute eines Objekts zurücksetzen muss, beispielsweise zur Realisierung einer "Rückgängig"-Taste oder zum Zurücksetzen der Position einer Spielfigur, wenn diese falsch gezogen wurde (z.B. beim Schach), ist es nötig den Original-Zustand vorher zu speichern. Das Memento-Muster bietet die Möglichkeit solch einen Mechanismus zu implementieren.
Funktionsweise
[Bearbeiten]UML
[Bearbeiten]Entscheidungshilfen
[Bearbeiten]Das Memento-Muster sollte eingesetzt werden, wenn man Attribute eines Objekts zurücksetzen muss, beispielsweise zur Realisierung einer "Rückgängig"-Taste oder zum Zurücksetzen der Position einer Spielfigur, wenn diese falsch gezogen wurde (z.B. beim Schach)
TemplateMethod
Schablonenmethode (TemplateMethod)
[Bearbeiten]Bestimme den Ablauf eines Algorithmus und delegiere die konkrete Ausgestaltung an eine Unterklasse.
Zweck
[Bearbeiten]Ziel dieses Musters ist es, die Struktur eines Algorithmus bereits festzulegen, während die konkrete Ausgestaltung einzelner Schritte noch offen gelassen wird; es bleibt dann Unterklassen vorbehalten, diese Teile des Algorithmus nachzureichen. Somit können Unterklassen einzelne Schritte beeinflussen - nicht aber die Struktur des Algorithmus.
Dadurch hilft das Schablonenmethoden-Muster auch, eine Verdopplung von Code zu vermeiden: Die invarianten Teile einer Familie von ähnlichen Algorithmen werden genau einmal festgelegt und somit gemeinsames Verhalten aus den Unterklassen "herausfaktorisiert".
Ein sehr einfaches Beispiel aus der Mathematik soll dies verdeutlichen: Für zwei natürliche Zahlen a und b berechnet der folgende Algorithmus das Produkt a*b:
erg = a wiederhole (b-1) mal: erg = erg + a
und der nun folgende die Potenz a^b:
erg = a wiederhole (b-1) mal: erg = erg * a
(Der interessierte Leser wird vielleicht an einer Lektuere des Wikipedia-Artikels zur Ackermann-Funktion Gefallen finden.) Offensichtlich unterscheiden sich diese beiden Algorithmen lediglich durch die im Schleifenkörper ausgeführte Operation; dies ermöglicht die Verwendung des Schablonenmethoden-Musters. Die gemeinsame Struktur lautet:
erg = a wiederhole (b-1) mal: erg = operation(erg, a)
Unterklassen dürfen für "operation()
" dann entweder eine Addition, eine Multiplikation oder eine gänzlich andere Funktion (z.B. Potenzieren, hyper4, ...) einsetzen.
UML
[Bearbeiten]Entscheidungshilfen
[Bearbeiten]Eine Schablonenmethode kann eingesetzt werden, wenn einzelne Teilschritte eines übergeordneten Algorithmus nicht bekannt sind (oder absichtlich offen gelassen werden sollen) aber Vorbedingungen und jene übergeordente Struktur festgelegt werden sollen.
Implementation
[Bearbeiten]Für die Umsetzung muss folgendes implementiert werden:
- Eine Klasse mit mindestens einer abstrakten und mindestens einer konkreten (Schablonen)Methode.
- Innerhalb der konkreten Methode wird die abstrakte aufgerufen.
- Eine oder mehrere Unterklassen, die das Verhalten der abstrakten Funktion festlegen.
Konkrete Hinweise zur Implementation finden sich zu den Programmiersprachen.
Verwandte Muster
[Bearbeiten]Fabrikmethoden werden oft von Schablonenmethoden aufgerufen.
Schablonenmethoden verwenden Vererbung, um Teile eines Algorithmus zu variieren; das Muster Strategie verwendet Delegation, um den gesamten Algorithmus zu variieren.
Strategy
Die Strategie gehört zur Kategorie der Verhaltensmuster (Behavioural Patterns). Sie definiert eine Familie austauschbarer Algorithmen.
Verwendung
[Bearbeiten]Strategie-Objekte werden ähnlich wie Klassenbibliotheken verwendet. Im Gegensatz dazu handelt es sich jedoch nicht um externe Programmteile, die als ein Toolkit genutzt werden können, sondern um integrale Bestandteile des eigentlichen Programms, die deshalb als eigene Objekte definiert wurden, damit sie durch andere Algorithmen ausgetauscht werden können.
Meistens wird eine Strategie durch Klassen umgesetzt, die eine bestimmte Schnittstelle implementieren. In Sprachen wie Smalltalk, in denen auch der Programmcode selbst in Objekten abgelegt werden kann, kann eine Strategie aber auch durch solche Code-Objekte realisiert werden.
Die Verwendung von Strategien bieten sich an, wenn
- viele verwandte Klassen sich nur in ihrem Verhalten unterscheiden.
- unterschiedliche (austauschbare) Varianten eines Algorithmus benötigt werden.
- Daten innerhalb eines Algorithmus vor Klienten verborgen werden sollen.
- verschiedene Verhaltensweisen innerhalb einer Klasse fest integriert sind (meist über Mehrfachverzweigungen) aber
- die verwendeten Algorithmen wiederverwendet werden sollen, bzw.
- die Klasse flexibler gestaltet werden soll.
UML-Diagramm
[Bearbeiten]Akteure
[Bearbeiten]- Strategie
- definiert eine einheitliche Schnittstelle für alle unterstützten Algorithmen
- KonkreteStrategie
- implementiert einen Algorithmus
- Verwendung über Strategieschnittstelle
- eventuell Zugriff auf Daten des Kontexts über entsprechende Methoden
- Kontext
- Konfiguration mit konkretem Strategieobjekt
- hält Referenz auf konkretes Strategieobjekt
- nutzt konkretes Strategieobjekt über Strategieschnittstelle
- definiert eventuell Schnittstelle für Kontextinformationen
Vorteile
[Bearbeiten]- Es wird eine Familie von Algorithmen definiert.
- Strategien bieten eine Alternative zur Unterklassenbildung.
- Strategien helfen, Mehrfachverzweigungen zu vermeiden, und verbessern dadurch die Wiederverwendbarkeit.
- Strategien ermöglichen die Auswahl aus verschiedenen Implementationen und erhöhen dadurch die Flexibilität.
Nachteile
[Bearbeiten]- Klienten müssen die unterschiedlichen Strategien kennen, um zwischen ihnen auswählen und den Kontext initialisieren zu können.
- Gegenüber der Implementation der Algorithmen im Kontext erzeugen Strategien zusätzlichen Kommunikationsaufwand zwischen Strategie und Kontext.
- Des Weiteren wird die Anzahl von Objekten erhöht.
Beispiel
[Bearbeiten]Als Beispiel kann ein Steuerprogramm dienen, das die Berechnung von Steuersätzen in Strategie-Objekte auslagern soll, um einfach länderabhängig konfigurierbar zu sein. Ein anderes Beispiel wäre die Speicherung eines Dokuments oder einer Grafik in verschiedenen Dateiformaten. Auch ein Packer, der verschiedene Kompressionsalgorithmen unterstützt, kann mit Hilfe von Strategie implementiert sein. Bei Java wird das Entwurfsmuster zum Beispiel zur Delegierung des Layouts von AWT-Komponenten an entsprechende LayoutManager (BorderLayout,FlowLayout etc.) verwendet.
Verwandte Entwurfsmuster
[Bearbeiten]
State
State / Zustand
[Bearbeiten]Der Zustand (engl. state) wird verwendet, um Änderungen des Verhaltens eines Objekts abhängig von seinem Zustand zu ermöglichen. Auch bekannt als Objekte für Zustände (objects for states).
Verwendung
[Bearbeiten]- Das Verhalten eines Objekts ist abhängig von seinem Zustand.
- Die übliche Implementierung soll vermieden werden, die Zustände eines Objekts und das davon abhängige Verhalten in einer großen Switch-Anweisung (basierend auf enumerierten Konstanten) zu kodieren. Jeder Fall der Switch-Anweisung soll in einer eigenen Klasse implementiert werden, so dass der Zustand des Objektes selbst wieder ein Objekt ist, das unabhängig von anderen Objekten ist.
Problem
[Bearbeiten]Zustandsautomat
[Bearbeiten]Für ein Objekt sind verschiedene Zustände, die möglichen Übergänge zwischen diesen Zuständen und das davon abhängige Verhalten zu definieren. Dies ist hier in Form eines endlichen Automaten dargestellt. Dabei zeigt der schwarze Kreis auf den Startzustand und der schwarze Kreis mit der weißen Umrandung auf den Endzustand. Die gerichteten Kanten (Pfeile) zwischen den Zuständen Closed, Open und Deleted definieren den Zustandswechsel.
Hierarchische Zustandsautomat
[Bearbeiten]Ein einzelner Zustand eines Objektes kann wiederum in eine Anzahl verschiedener Zustände aufgeteilt werden. Den Zustand Open kann man beispielsweise unterteilen in Read und Write. Sie bilden einen zusammengesetzten Zustand Open. Closed sowie Deleted betrachtet man unabhängig vom zusammengesetzten Zustand Open. Diese Zustände kann man in einer Hierarchie anordnen. Open, Closed und Deleted sind in der ersten Ebene. In der zweiten Ebene befinden sich Read und Write, die dem Zustand Open zugeordnet sind.
Lösung
[Bearbeiten]Einfache Zustände
[Bearbeiten]Das zustandsabhängige Verhalten des Objekts wird in separate Klassen ausgelagert, wobei für jeden möglichen Zustand eine eigene Klasse eingeführt wird, die das Verhalten des Objekts in diesem Zustand definiert. Damit das ursprüngliche Objekt die separaten Zustandsklassen einheitlich behandeln kann, wird eine gemeinsame Abstrahierung dieser Klassen definiert. Bei einem Zustandsübergang tauscht das ursprüngliche Objekt das von ihm verwendete Zustandsobjekt aus.
Implementierung
[Bearbeiten]Für das Ausprogrammieren des Zustandsmusters sollte vorher überlegt werden, ob
- der Kontext die Zustände als Attribute hält.
- bei jedem Zustandswechsel der vorherige Zustand gelöscht und ein neuer Zustand instantiiert wird, was zwar rechentechnisch aufwendiger aber sauberer für den Programmablauf ist. Das heißt, dass alle Membervariablen eines jeden Zustands gelöscht werden und ein späteres Debuggen von Anfang an ausgeschlossen wird. Man kann dann auch diesen Zustand mehrmals in ähnlichen Kontexten verwenden. (Z. B. können zwei Dateien eines Dateisystems zur gleichen Zeit geöffnet sein. Dabei befinden sich beide Dateien im Zustand Open. Vom Open-Zustand existieren daher zwei Instanzen, jeweils eine Instanz für jede Datei.)
- jeder Zustand nur einmal erzeugt wird - dann wenn er zum ersten Mal gebraucht wird - und daher alle möglichen Zustände parallel im Speicher existieren (hier wird das Singleton-Erzeugermuster verwendet).
Akteure
[Bearbeiten]- Kontext
- Definiert die klientseitige Schnittstelle.
- Verwaltet die separaten Zustandsklassen und tauscht diese bei einem Zustandsübergang aus.
- (Abstrakter) Zustand
- Definiert eine einheitliche Schnittstelle aller Zustandsobjekte.
- Implementiert gegebenenfalls ein Standardverhalten. Beispielsweise kann in der abstrakten Zustandsklasse die Ausführung jeglichen Verhaltens gesperrt werden. Ein bestimmtes Verhalten kann dann nur ausgeführt werden, wenn es vom konkreten Zustand durch Überschreiben der entsprechenden Methode freigeschaltet wurde.
- Konkreter Zustand
- Implementiert das Verhalten, das mit dem Zustand des Kontextobjekts verbunden ist.
Vorteile
[Bearbeiten]- Komplexe und schwer leserliche Bedingungsanweisungen können vermieden werden.
- Neue Zustände und neues Verhalten können auf einfache Weise hinzugefügt werden. Die Wartbarkeit wird erhöht.
- Zustandobjekte können wiederverwendet werden.
Nachteile
[Bearbeiten]- Bei sehr einfachem zustandsbehaftetem Verhalten rechtfertigt unter Umständen der Nutzen den Implementierungsaufwand nicht.
Beispiele
[Bearbeiten]Prinzipiell kann jedes zustandsabhängige Verhalten durch dieses Entwurfsmuster abgebildet werden. Einige Beispiele für zustandsbehaftete Problemstellungen sind
- Verwaltung von Sessions
- Verwaltung von Ein- und Ausgabeströmen
- Zustandbehaftete Bedienelemente einer grafischen Benutzeroberfläche
- Parkautomaten
Pseudokode
[Bearbeiten]Als Beispiel nehmen wir ein Zeichnungsprogramm mit einem Mauszeiger, dass zu jedem Zeitpunkt als eines von mehreren Werkzeugen agieren kann. Statt zwischen mehreren Zeigerobjekten zu wechseln behält der Cursor einen internen Zustand, der das derzeit verwendete Werkzeug representiert. Wird eine werkzeugabhängige Methode wie z. B. als ein Resultat eines Mausklicks aufgerufen, wird der Methodenaufruf dem Cursorzustand übergeben.
Jedes Werkzeug entspricht einem Zustand. Die geteilte abstrakte Klasse heißt AbstractTool
.
class AbstractTool is function moveTo(point) is input: Die Position point, wohin die Maus bewegt wurde (Diese Funktion muss von Unterklassen implementiert werden.) function mouseDown(point) is input: Die Position point, wo die Maus ist (Diese Funktion muss von Unterklassen implementiert werden.) function mouseUp(point) is input: Die Position point, wo die Maus ist (Diese Funktion muss von Unterklassen implementiert werden.)
Gemäß dieser Definition muss jedes Werkzeug die Cursorbewegung behandeln und auch den Anfang und das Ende jedes Klicks oder Verschiebens.
Mit dieser Basisklasse sehen der einfache Stift und die Auswahlwerkzeuge wie folgt aus:
subclass PenTool of AbstractTool is last_mouse_position := invalid mouse_button := up function moveTo(point) is input: Die Position point, wohin die Maus bewegt wurde if mouse_button = down (draw a line from the last_mouse_position to point) last_mouse_position := point function mouseDown(point) is input: Die Position point, wo die Maus ist mouse_button := down last_mouse_position := point function mouseUp(point) is input: Die Position point, wo die Maus ist mouse_button := up
subclass SelectionTool of AbstractTool is selection_start := invalid mouse_button := up function moveTo(point) is input: Die Position point, wohin die Maus bewegt wurde if mouse_button = down (select the rectangle between selection_start and point) function mouseDown(point) is input: Die Position point, wo die Maus ist mouse_button := down selection_start := point function mouseUp(point) is input: Die Position point, wo die Maus ist mouse_button := up
Für dieses Beispiel wurde die Klasse für den Kontext Cursor
benannt. Die Methoden in der abstrakten Zustandsklasse heißen
(AbstractTool
in diesem Falle) werden auch im Kontext implementiert. Die Kontextklasse rufen diese Methoden die entsprechenden Methoden des derzeitigen Zustands auf, die durch current_tool
dargestellt werden.
class Cursor is current_tool := new PenTool function moveTo(point) is input: Die Position point, wohin die Maus bewegt wurde current_tool.moveTo(point) function mouseDown(point) is input: Die Position point, wo die Maus ist current_tool.mouseDown(point) function mouseUp(point) is input: Die Position point, wo die Maus ist current_tool.mouseUp(point) function usePenTool() is current_tool := new PenTool function useSelectionTool() is current_tool := new SelectionTool
Beachten Sie, wie ein Cursor
-Objekt sowohl als PenTool
als auch als SelectionTool
an unterschiedlichen Punkten agieren kann, indem es die passende Methodenaufrufe entsprechend dem gerade aktiven Werkzeug übergibt. Dies ist das Grundlegende des Zustands (state pattern). In diesem Falle könnte man das Objekt mit dem Zustand kombinieren, indem man eine PenCursor
- und eine SelectCursor
-Klasse kreiert, wodurch die Lösung zu einer einfachen "Vererbungslösung" werden würde. Aber in der Praxis könnte Cursor
Daten beinhalten, die bei jeder neuen Auswahl eines Werkzeugs zu aufwendig oder unelegant zu kopieren wären.
Java
[Bearbeiten]Hier ist ein Beispiel für das Verhaltensmuster Zustand:
/**
* Class that comprises of constant values and recurring algorithms.
*/
public class Common {
/**
* Changes the first letter of the string to upper case
* @param WORDS
* @return Requested value
*/
public static String firstLetterToUpper(final String WORDS) {
String firstLetter = "";
String restOfString = "";
if (WORDS != null) {
char[] letters = new char[1];
letters[0] = WORDS.charAt(0);
firstLetter = new String(letters).toUpperCase();
restOfString = WORDS.toLowerCase().substring(1);
}
return firstLetter + restOfString;
}
}
interface Statelike {
/**
* Writer method for the state name.
* @param STATE_CONTEXT
* @param NAME
*/
void writeName(final StateContext STATE_CONTEXT, final String NAME);
}
class StateA implements Statelike {
/* (non-Javadoc)
* @see state.Statelike#writeName(state.StateContext, java.lang.String)
*/
@Override
public void writeName(final StateContext STATE_CONTEXT, final String NAME) {
System.out.println(Common.firstLetterToUpper(NAME));
STATE_CONTEXT.setState(new StateB());
}
}
class StateB implements Statelike {
/** State counter */
private int count = 0;
/* (non-Javadoc)
* @see state.Statelike#writeName(state.StateContext, java.lang.String)
*/
@Override
public void writeName(final StateContext STATE_CONTEXT, final String NAME) {
System.out.println(NAME.toUpperCase());
// Change state after StateB's writeName() gets invoked twice
if(++count > 1) {
STATE_CONTEXT.setState(new StateA());
}
}
}
Die Kontextklasse hat eine Zustandsvariable, die sie hier als StateA
in einem Anfangszustand instanziiert. In seinen Methoden verwendet sie die entsprechenden Methoden des Zustandsobjekts.
public class StateContext {
private Statelike myState;
/**
* Standard constructor
*/
public StateContext() {
setState(new StateA());
}
/**
* Setter method for the state.
* Normally only called by classes implementing the State interface.
* @param NEW_STATE
*/
public void setState(final Statelike NEW_STATE) {
myState = NEW_STATE;
}
/**
* Writer method
* @param NAME
*/
public void writeName(final String NAME) {
myState.writeName(this, NAME);
}
}
Der Test unten soll auch die Verwendung veranschaulichen:
public class TestClientState {
public static void main(String[] args) {
final StateContext SC = new StateContext();
SC.writeName("Montag");
SC.writeName("Dienstag");
SC.writeName("Mittwoch");
SC.writeName("Donnerstag");
SC.writeName("Freitag");
SC.writeName("Samstag");
SC.writeName("Sonntag");
}
}
Gemäß dem Code oben ist die Ausgabe der main()
-Methode von TestClientState
:
Montag DIENSTAG MITTWOCH Donnerstag FREITAG SAMSTAG Sonntag
Java: NullPattern
Null Muster
[Bearbeiten]Implementiere einen leeren Algorithmus.
Implementierung (1)
[Bearbeiten] package de.bastie.eHunter.gui.action; <br>
import java.awt.event.ActionEvent;
import javax.swing.Icon;<br>
/**
* Null Pattern Implementation
* @author Bastie - Sebastian Ritter
* @licence FDL für Wikibooks
* @version 1.0
*/
class DefaultMenuAction extends ModelDrivenAbstractAction {
public DefaultMenuAction (final String text) {
this (text, null);
}
public DefaultMenuAction (final String text,
final Icon icon) {
super (text, icon);
}
// Hier ist die null action.
public void actionPerformed (final ActionEvent ignore) {
}
}
package de.bastie.eHunter.gui.action;<br>
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Icon;<br>
/**
* Diese Klasse dient dazu für alle anderen Action Objekte
* vordefinierte Methoden zur Verfügung zu stellen. Insbesondere
* sollen diese Zugriff auf das zugrundeliegende BO haben.
*
* Die <code><strong>~</strong></code> wird hierbei als Descriptor für
* den Mnemonic Key verwendet.
* @author Bastie - Sebastian Ritter
* @licence FDL für Wikibooks
* @version 1.0
*/
abstract class ModelDrivenAbstractAction extends AbstractAction {
public ModelDrivenAbstractAction (final String text) {
this (text, null);
}
public ModelDrivenAbstractAction (String menuText,
final Icon icon) {
super (menuText, icon);
final int mnemonicKeyDescriptor = menuText.indexOf('~');
if (mnemonicKeyDescriptor != -1) {
// Rausschneiden von ~
menuText = menuText.substring(0,mnemonicKeyDescriptor)
.concat(menuText.substring(mnemonicKeyDescriptor+1));
this.putValue(Action.MNEMONIC_KEY, new Integer (menuText.charAt(mnemonicKeyDescriptor)));
this.putValue(Action.NAME, menuText);
}
}
}
Erläuterungen
[Bearbeiten]Das vorstehende Beispiel zeigt Dir das NullPattern anhand einer einfachen Implementation für Swing Action Objekte für Menüs. Die eigentlich abstrakte Methode
public void actionPerformed (final ActionEvent e) {}
Anmerkung (1): Als kleines Bonbon habe ich noch die Basisklasse hinzugefügt, welche eine einfache Möglichkeit der Steuerung des Mnemonic-Zeichens anbietet.
Anmerkung (2): Die eigentliche Initialisierung erfolgt bei mir über einen Reflection Mechanismus zusammen mit einen Klassenfabrik, so dass beide Klassen entsprechend auf default
Sichtbarkeit eingeschränkt sind.
Anmerkung (3): Ich habe nicht den kompletten Quelltext hier dargestellt, da er zu umfangreich ist.
Java: Singleton
Singleton
[Bearbeiten]Erzeuge genau eine Instanz eines Objektes für diese Klasse eines Klassenladers (Sofern nicht durch Reflection erzeugt). Zur Sicherung einer Instanz in allen Klassenladern einer Java Virtual Maschine [JVM] bedarf es mehr Arbeit. (siehe [1] (vgl. WikiBook Muster)
Implementierung (1)
[Bearbeiten]package org.wikibooks.de.java.pattern;
public class Singleton {
// Eine (versteckte) Klassenvariable vom Typ der eigenen Klasse
private static Singleton instance;
// Verhindere die Erzeugung des Objektes über andere Methoden
private Singleton () {}
// Eine Zugriffsmethode auf Klassenebene, welches dir '''einmal''' ein konkretes
// Objekt erzeugt und dieses zurückliefert.
public static Singleton getInstance () {
if (Singleton.instance == null) {
Singleton.instance = new Singleton ();
}
return Singleton.instance;
}
}
Achtung: Diese Implementation ist einfach und wird für die meisten Aufgaben auch reichen. Sie ist jedoch nicht Thread-sicher, da die Erzeugung kein atomarer Vorgang in der JVM ist.
-> Hier ist es möglich, dass mehrere Threads gleichzeitig diese Methode aufrufen. Im Falle von
erforderlichen Initialisierungen im Konstruktor kann es vorkommen, dass ein Thread diese gerade ausführt und der andere schon die erstellte Instanz erhält, diese jedoch noch nicht komplett initialisiert ist. Solche Fehler treten meist erst im Realbetrieb unter Last auf und sind sehr schwer zu finden. Es sollte daher immer die synchronisierte Variante verwendet werden (siehe unten).
Nähere Infos siehe: [2]
Hier die Thread-sichere Version
[Bearbeiten]package org.wikibooks.de.java.pattern;
public class Singleton {
// Eine (versteckte) Klassenvariable vom Typ der eigene Klasse
private static Singleton instance;
// Verhindere die Erzeugung des Objektes über andere Methoden
private Singleton () {}
// Eine Zugriffsmethode auf Klassenebene, welches dir '''einmal''' ein konkretes
// Objekt erzeugt und dieses zurückliefert.
// Durch 'synchronized' wird sichergestellt dass diese Methode nur von einem Thread
// zu einer Zeit durchlaufen wird. Der nächste Thread erhält immer eine komplett
// initialisierte Instanz.
public static synchronized Singleton getInstance () {
if (Singleton.instance == null) {
Singleton.instance = new Singleton ();
}
return Singleton.instance;
}
}
Diese Version hat allerdings den Nachteil, dass jeder Zugriff auf die getInstance-Methode synchronisiert ist und dadurch bei sehr vielen Zugriffen zum potentiellen Flaschenhals wird.
Implizit synchronisierte Variante
[Bearbeiten]package org.wikibooks.de.java.pattern;
public class Singleton {
// Innere private Klasse, die erst beim Zugriff durch die umgebende Klasse initialisiert wird
private static final class InstanceHolder {
// Die Initialisierung von Klassenvariablen geschieht nur einmal
// und wird vom ClassLoader implizit synchronisiert
static final Singleton INSTANCE = new Singleton();
}
// Verhindere die Erzeugung des Objektes über andere Methoden
private Singleton () {}
// Eine nicht synchronisierte Zugriffsmethode auf Klassenebene.
public static Singleton getInstance () {
return InstanceHolder.INSTANCE;
}
}
Das Initialisieren der Klassenvariablen wird vom ClassLoader implizit synchronisiert. Durch die Verwendung der inneren Klasse wird der Singleton-Konstruktor erst bei der Initialisierung der inneren Klasse, also in der getInstance-Methode, aufgerufen. Wäre die Variable INSTANCE eine Variable des Singletons, würde sie schon beim Zugriff auf die Klasse (z. B. Class.forName("org.wikibooks.de.java.pattern.Singleton")) initialisiert werden.
Java: TemplateMethod
Schablonenmethode
[Bearbeiten]Lege den Algorithmus fest und überlasse die Ausgestaltung von einzelnen Teilen Unterklassen. (vgl. WikiBook Muster)
Implementierung (1)
[Bearbeiten] package org.wikibooks.de.java.pattern;
public abstract class AbstractSchablonenMethodeFenster {
public AbstractSchablonenMethodeFenster () {
this.initGUI ();
}
// Eine Klasse mit mind. einer abstrakten ...
protected abstract void initAction ();
// ... und mind. einer konkreten (Schablonen)Methode.
public void initGUI () {
// Den Aufruf von abstrakten Methoden innerhalb der konkreten (Schablonen)Methode.
this.initAction ();
this.initMenu ();
// ...
}
}
package org.wikibooks.de.java.pattern;
public class DefaultSchablonenMethodeFenster extends AbstractSchablonenMethodeFenster {
private Action exitOnCloseAction;
private JMenu fileMenu;
private JMenuItem exitApplication;
protected void initAction () {
exitOnCloseAction = new AbstractAction ("Beenden", null) {
public void actionPerformed (final ActionEvent exit) {
System.exit (0);
}
}
}
protected void initMenu () {
JMenuBar menueLeiste = new JMenuBar ();
fileMenu = new JMenu ("Datei");
exitApplication = new JMenuItem (this.exitOnCloseAction);<br>
fileMenu.add (exitApplication);
menueLeister.add (fileMenu);
this.setJMenuBar (menueLeiste);
}
}
Erläuterungen
[Bearbeiten]Das vorstehende Beispiel zeigt Dir die Schablonenmethode anhand einer einfachen grafischen Oberfläche mit Swing. Gehen wir das Beispiel von hinten her an.
exitApplication = new JMenuItem (this.exitOnCloseAction);
Hiermit weisen wir dem Menüeintrag ein Action Objekt zu. Damit dies funktioniert muss das Action Objekt jedoch vorher initialisiert werden, da Du sonst eine NullPointerException
bekommst.
Wir haben zur besseren Übersichtlichkeit die Initialisierung unserer Action Objekte in eine eigene Methode ausgelagert.
protected void initAction ()
Nun müssen wir sicherstellen, dass vor der Zuweisung zu unserem Menüeinträgen die Action Objekte auch initalisiert sind. Natürlich könnten wir einfach in der Dokumentation dies so beschreiben - aber wir können es besser. Wir definieren eine Superklasse (AbstractSchablonenMethodeFenster
) in der wir eine Schablonenmethode anlegen.
public void initGUI ()
In dieser Schablonenmethode definieren wir die genauer Abfolge der Aufrufe und stellen somit den korrekten Programmablauf sicher.
Anmerkung (1): Als kleines Bonbon nutzen wir gleich noch eine Einschränkung der Sichtbarkeit (protected
) damit kein DAP (dümmster anzunehmender Programmierer) die Methoden separat in der falschen Reihenfolge aufruft.
Anmerkung (2): Als Schmanckl haben wir zusätzlich noch den Aufruf der initGUI
Methode in den einzigen Konstruktor der Superklasse gepackt. Die Umgehung unseres Algorithmus ist nun schon auf versierte Programmierer eingeschränkt.
Java: Observer
Das Muster Observer dient als „Beobachter“ und informiert über Änderungen eines Objektes (vergleiche WikiBook Muster).
Implementierung (1) – do it yourself
[Bearbeiten]import java.util.ArrayList;
import java.util.List;
public class Subjekt {
private List<Beobachter> beobachter = new ArrayList<Beobachter>();
private Object zustand = null;
private void benachrichtigen() {
for (Beobachter b : beobachter)
b.aktualisiere();
}
public void entferne(Beobachter b) {
beobachter.remove(b);
}
public Object gibZustand() {
return zustand;
}
public void registriere(Beobachter b) {
beobachter.add(b);
}
public void setzeZustand(Object neu) {
zustand = neu;
benachrichtigen();
}
}
public interface Beobachter {
public void aktualisiere();
}
public class KonsolenBeobachter implements Beobachter {
protected Subjekt subjekt = null;
public void setzeSubjekt(Subjekt s) {
if (this.subjekt != null)
this.subjekt.entferne(this);
this.subjekt = s;
if (this.subjekt != null)
this.subjekt.registriere(this);
}
public void aktualisiere() {
System.out.println(this.subjekt.gibZustand());
}
}
public class GeschwaetzigerKonsolenBeobachter extends KonsolenBeobachter {
public void aktualisiere() {
System.out.println("Das Subjekt hat seinen Zustand geaendert; jetzt: " + this.subjekt.gibZustand());
}
}
public class BeobachterTest {
public static void main(String[] args) {
Subjekt s = new Subjekt();
KonsolenBeobachter b1 = new KonsolenBeobachter();
KonsolenBeobachter b2 = new GeschwaetzigerKonsolenBeobachter();
b1.setzeSubjekt(s);
b2.setzeSubjekt(s);
s.setzeZustand("Hallo Welt");
b2.setzeSubjekt(null);
s.setzeZustand("Hallo schoene Welt");
}
}
Implementierung (2) – java.util.Observer, java.util.Observable
[Bearbeiten] public class Beobachter implements Observer {
private static int NR = 1;
private int nr;
public Beobachter() {
this.nr = Beobachter.NR++;
}
public void update(Observable beobachtbarer, Object text) {
System.out.println("Beobachter " + nr + " meldet: Text=" + text);
}
}
public class Beobachtbarer extends Observable {
private String text;
public Beobachtbarer() {
super();
}
public void setText(String text) {
this.text = text;
super.setChanged(); // Markierung, dass sich der Text geändert hat
super.notifyObservers(text); // ruft für alle Beobachter die update-Methode auf
}
public String getText() {
return text;
}
}
public class Beobachtungen {
public static void main(String[] args) {
Beobachter[] bArray = {new Beobachter(), new Beobachter(), new Beobachter()};
Beobachtbarer bb = new Beobachtbarer();
for (Beobachter b: Arrays.asList(bArray)) {
bb.addObserver(b);
}
bb.setText("Ich");
bb.setText("werde");
bb.setText("beobachtet.");
}
}
/*
Ausgabe:
********
Beobachter 3 meldet: Text=Ich
Beobachter 2 meldet: Text=Ich
Beobachter 1 meldet: Text=Ich
Beobachter 3 meldet: Text=werde
Beobachter 2 meldet: Text=werde
Beobachter 1 meldet: Text=werde
Beobachter 3 meldet: Text=beobachtet.
Beobachter 2 meldet: Text=beobachtet.
Beobachter 1 meldet: Text=beobachtet.
*/
Erläuterungen
[Bearbeiten]Ein Observer (Beobachter) kann über seine update-Methode auf Änderungen eines Observable (Beobachtbarer) reagieren. Das passiert jedoch nur, wenn sich Observer an Observable registrieren.
In unserer Beispiel-Implementierung wird in der update-Methode von Beobachter lediglich eine Info auf die Konsole geschrieben. Die Klasse Beobachtbarer besitzt nur eine Set-Methode, über die ein Text verändert werden kann. Dabei wird der geänderte Text immer mittels der setChanged-Methode als "geändert" markiert und über die notifyObservers-Methode wird bei allen registrierten Observer-Objekten die update-Methode aufgerufen.
Die zentrale Klasse Beobachtungen vereint nun Observer und Observable. Wir sind maßlos und haben gleich drei Beobachter bei dem Beobachtbarer registriert. Jeder Aufruf der setText-Methode bewirkt nun den Aufruf der update-Methode bei allen drei Beobachter-Objekten.
Implementierung (3) – java.beans.PropertyChangeListener / java.beans.PropertyChangeSupport
[Bearbeiten]public class AktualisierungsListener implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent pce) { System.out.println("Bei der " + pce.getSource() + " wurde der Parameter \"" + pce.getPropertyName() + "\" von \"" + pce.getOldValue() + "\" auf \"" + pce.getNewValue() + "\" geaendert."); } }
public class Quelle { private String text; private int zahl; public void setText(String text) { this.text = text; } public void setZahl(int zahl) { this.zahl = zahl; } public String getText() { return text; } public int getZahl() { return zahl; } public String toString() { return "Quell-Bean"; } }
public class Aktualisierbarer extends PropertyChangeSupport { private Quelle quelle; public Aktualisierbarer(Quelle quelle) { super(quelle); this.quelle = quelle; } public void setText(String text) { super.firePropertyChange("text", quelle.getText(), text); quelle.setText(text); } public void setZahl(int zahl) { super.firePropertyChange("zahl", quelle.getZahl(), zahl); quelle.setZahl(zahl); } public String getText() { return quelle.getText(); } public int getZahl() { return quelle.getZahl(); } }
public class Aktualisierungen { public static void main(String[] args) { Aktualisierbarer aktu = new Aktualisierbarer(new Quelle()); aktu.addPropertyChangeListener(new AktualisierungsListener()); aktu.setZahl(7); aktu.setText("vorher"); aktu.setText("jetzt"); aktu.setZahl(13); } }
/* Ausgabe: ******** Bei der Quell-Bean wurde der Parameter "zahl" von "0" auf "7" geaendert. Bei der Quell-Bean wurde der Parameter "text" von "null" auf "vorher" geaendert. Bei der Quell-Bean wurde der Parameter "text" von "vorher" auf "jetzt" geaendert. Bei der Quell-Bean wurde der Parameter "zahl" von "7" auf "13" geaendert. */
Erläuterungen
[Bearbeiten]Über den AktualisierungsListener werden die Inhaltsänderungen von Objekten (hier: Quell-Bean) überwacht und es kann darauf reagiert werden. In unserem Beispiel wird hier lediglich eine Ausgabe auf die Konsole gemacht.
Unsere Quelle (Quell-Bean) besitzt 2 Attribute, die gesetzt werden können. Mit dem Aktualisierbarer werden diese Setz-Möglichkeiten überwacht. In jeder seiner Set-Methoden wird ein Attribut-Änderungs-Event ausgelöst.
Die zentrale Klasse Aktualisierungen vereint nun AktualisierungsListener und Aktualisierbarer, damit die Attribut-Änderungen der Quell-Bean auch bemerkt werden.
Java: Proxy
Erläuterungen
[Bearbeiten]Demonstriert werden soll ein synchronisierender Stellvertreter (engl. synchronizing proxy) anhand des folgenden Szenarios: Aus einem Lager (Interface Lager
) können Verbraucher eine bestimmt Anzahl von Gegenständen entnehmen (entnehme(int anzahl)
); das Entnehmen dezimiert natürlich den im Lager verbliebenen Vorrat (this.vorrat -= anzahl
). Nun haben wir es aber mit mehreren Verbrauchern zu tun, die gleichzeitig (als Threads implementiert) auf das Lager zugreifen wollen - race-conditions können die Folge sein; hier ein Beispiel:
vorrat <- 100 -- Thread von Verbraucher 1 -- entnehme(10) lokale variable: temp1 temp1 <- vorrat // temp1 = 100 temp1 <- temp1 - 10 // temp1 = 90 -- Unterbrechung durch Scheduler -- -----> -- Thread von Verbraucher 2 -- entnehme(20) lokale variable: tmp2 temp2 <- vorrat // temp2 = 100 temp2 <- temp2 - 20 // temp2 = 80 vorrat <- temp2 // vorrat = 80 -- Thread von Verbraucher 1 -- <----- -- Unterbrechung durch Scheduler -- vorrat <- temp1 // vorrat = 90
Bei diesem Beispielablauf wird der erste Thread unterbrochen bevor der verringerte Wert von 90 zurückgeschrieben wurde und stattdessen startet Thread 2 seine Berechnung (man beachte, dass es sich bei vorrat -= anzahl
nicht um eine atomare Operation handelt). Wenn Thread 1 schließlich wieder an der Reihe ist, fährt er genau an der Stelle fort, an der er zuvor unterbrochen wurde: Beim Zurückschreiben der temporären, lokalen Variable, in der natürlich immer noch der Wert 90 steht - als Endergebnis erhalten wir für vorrat
also den Wert 90, obwohl insgesamt 30 Einheiten entnommen wurden! Ein Vorgang, mit dem der Besitzer des Lagers vielleicht einverstanden sein könnte (immerhin konnte er 30 Einheiten in Rechnung stellen, musste aber nur 10 von seinem Vorrat abgeben), der aber nicht eben unserer Vorstellung von der Realität genügt...
Gelöst werden kann dieses Problem nur, indem der Zugriff auf die entnehme()
-Methode synchronisiert wird: Es muss sichergestellt sein, dass eine Entnahme-Operation vollständig abgeschlossen ist, bevor mit der nächsten begonnen wird. Die Synchronisation könnte zwar in der EchtesLager
-Klasse selbst stattfinden, wir wollen hier jedoch einen anderen Weg gehen und den Zugriff durch einen vorgelagerten Stellvertreter (SynchronisiertesLager
) reglementieren: Dieser Stellvertreter reicht Anforderungen unverändert an das echte Lager weiter, sorgt aber dafür, dass sich immer nur ein Thread innerhalb der entnehme()
-Methode des echten Lagers befindet (er tut dies durch die Anweisung synchronized(this.lager)
).
Da das echte Lager und sein synchronisierender Stellvertreter eine gemeinsame Schnittstelle aufweisen, bekommen Verbraucher von diesem Vorgang nichts mit: Ob sie direkt auf das echte Lager zugreifen, oder indirekt über einen Stellvertreter - es macht für sie keinen Unterschied.
Code
[Bearbeiten] interface Lager {
public void entnehme(int anzahl);
}
class EchtesLager implements Lager {
public int vorrat;
public void entnehme(int anzahl) {
this.vorrat -= anzahl;
}
}
/**
* Synchronisierender Stellvertreter
*/
class SynchronisiertesLager implements Lager {
Lager lager;
public SynchronisiertesLager(Lager lager) {
this.lager = lager;
}
public void entnehme(int anzahl) {
synchronized(this.lager) { // erlaube immer nur einem Thread den Zugriff auf das tatsächliche Lager
this.lager.entnehme(anzahl);
}
}
}
class Verbraucher extends Thread {
int wieoft, anzahl;
Lager lager;
/**
* Ein Verbraucher entnimmt <wieoft>-mal jeweils <anzahl> Elemente aus dem Lager
*/
public Verbraucher(int wieoft, int anzahl, Lager lager) {
this.wieoft = wieoft;
this.anzahl = anzahl;
this.lager = lager;
}
public void run() {
for (int i = 0; i < this.wieoft; i++) {
this.lager.entnehme(anzahl);
this.yield();
}
}
}
public class StellvertreterTest {
public static void main(String[] args) {
int maxWieoft = 1000;
int maxAnzahl = 80;
int anzahlVerbraucher = 100;
Verbraucher[] verbraucher = new Verbraucher[anzahlVerbraucher];
int anzahlGesamt = 0;
EchtesLager echtesLager = new EchtesLager();
SynchronisiertesLager synchronisiertesLager = new SynchronisiertesLager(echtesLager);
java.util.Random rand = new java.util.Random();
for (int i = 0; i < anzahlVerbraucher; i++) {
int wieoft = rand.nextInt(maxWieoft + 1);
int anzahl = rand.nextInt(maxAnzahl + 1);
anzahlGesamt += wieoft*anzahl;
verbraucher[i] = new Verbraucher(wieoft, anzahl, echtesLager); // hier passiert es, dass am Ende das Lager nicht ganz leer ist
// verbraucher[i] = new Verbraucher(wieoft, anzahl, synchronisiertesLager); // so wird das Lager immer vollständig geleert.
}
echtesLager.vorrat = anzahlGesamt;
for (int i = 0; i < anzahlVerbraucher; i++) {
verbraucher[i].start();
}
for (int i = 0; i < anzahlVerbraucher; i++) {
try {
verbraucher[i].join();
} catch (InterruptedException e) { }
}
System.out.println(echtesLager.vorrat);
}
}
Java: Command
Das Command-Pattern erkläre ich anhand eines kleinen Beispieles. Es soll ein Fenster erstellt werden, welches bei einem Klick mit der linken Maustaste die Hintergrundfarbe ändert und bei einen Rechtsklick die Position wechselt. Wenn man das Mausrad drückt, soll die jeweils letzte Aktion rückgängig gemacht werden.
Dazu bauen wir das Programm in mehrere Klassen auf. Zunächst brauchen wir das Fenster. Hier passiert aber nichts spannendes. Der JFrame wird sichtbar gemacht und ein MouseListener wird hinzugefügt.
package wiki.pattern.command;
import javax.swing.JFrame;
public class MyFrame extends JFrame{
private CommandHandler handler;
public MyFrame(){
handler = new CommandHandler();
this.setSize(200, 200);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setVisible(true);
this.addMouseListener(new MyListener(this));
}
public CommandHandler getHandler() {
return handler;
}
}
Auf die Klasse CommandHandler geh ich weiter unten ein. In der Klasse MyListener wird nur auf einen Mausklick mit einen entsprechendem Befehl reagiert:
package wiki.pattern.command;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
public class MyListener implements MouseListener{
private MyFrame frame;
public MyListener(MyFrame frame){
this.frame = frame;
}
@Override
public void mouseClicked(MouseEvent arg0) {
if (arg0.getButton() == MouseEvent.BUTTON1) {
frame.getHandler().doIt(new ColorCommand(frame));
} else if (arg0.getButton() == MouseEvent.BUTTON3){
frame.getHandler().doIt(new ColorCommand(frame));
} else {
frame.getHandler().undoIt();
}
}
@Override
public void mouseEntered(MouseEvent arg0) {
// TODO Auto-generated method stub
}
@Override
public void mouseExited(MouseEvent arg0) {
// TODO Auto-generated method stub
}
@Override
public void mousePressed(MouseEvent arg0) {
// TODO Auto-generated method stub
}
@Override
public void mouseReleased(MouseEvent arg0) {
// TODO Auto-generated method stub
}
}
Die Befehle brauchen eine gemeinsame Schnittstelle, damit wir sie gleichartig behandeln können. Dazu definieren wir folgendes Interface:
package wiki.pattern.command;
public interface Command {
public void doIt();
public void undoIt();
}
Die Implementierung der beiden Methoden wird von den konkreten Befehlen übernommen. Am Beispiel des Kommando für den Farbwechsel kann das so aussehen:
package wiki.pattern.command;
import java.awt.Color;
public class ColorCommand implements Command{
private MyFrame frame;
private Color oldCol;
private Color newCol;
public ColorCommand(MyFrame frame){
this.frame = frame;
}
@Override
public void doIt() {
oldCol = frame.getContentPane().getBackground();
newCol = new Color((int) (Math.random() * 255), (int) (Math.random() * 255), (int) (Math.random() * 255));
frame.getContentPane().setBackground(newCol);
}
@Override
public void undoIt() {
frame.getContentPane().setBackground(oldCol);
}
}
Wenn auf das Kommando die doIt Methode aufgerufen wird, wird der alte Wert gespeichert (der Befehl soll sich ja auch rückgängig machen lassen), und die neue Hintergrundfarbe wird gesetzt. Ob ein Befehl jetzt ausgeführt oder wieder rückgängig gemacht wird, hängt natürlich davon ab welche Maustaste gedrückt wurde. Die eigentliche Arbeit wird aber an den CommandHandler weiter gegeben.
package wiki.pattern.command;
import java.util.Stack;
public class CommandHandler {
private Stack<Command> commands;
public CommandHandler(){
commands = new Stack<Command>();
}
public void doIt(Command command){
commands.push(command);
command.doIt();
}
public void undoIt(){
if (!commands.isEmpty()) {
commands.pop().undoIt();
}
}
}
Die Klasse hat einen Stack über alle Befehle. Die Klasse Stack bietet sich hier besonders an, da das, was zu erst rein kommt ganz unten liegt und neues oben drauf kommt (first in - last out). Mit push(Command) wird also ein neuer Befehl auf den Stack gelegt. Und wenn es an der Zeit ist einen Befehl rückgangig zu machen, liefert die Methode pop() das oberste Element vom Stack und entfernt dieses gleichzeitig. Auf die von pop() gelieferte Referenzen rufen wir unsere undoIt()-Methode auf.