06. Design Patterns
Entwurfsmuster – was ist das?
Entwurfsmuster helfen dabei, wiederverwendbare Software zu schreiben. Dabei werden für häufig wiederkehrende Probleme bewährte Lösungsmuster angeboten. Entwurfsmuster sind aus der objektorientierten Programmierung nicht mehr wegzudenken.
Die Entwurfsmuster können in drei Hauptklassen eingeteilt werden.
Erzeugungsmuster
Sie beschreiben Strukturen, die den Prozess der Objekterzeugung enthalten. Das Anwendungsprogramm wird von der konkreten Realisation der Objekterzeugung entkoppelt und delegiert die Erzeugung der Objekte und den Aufbau des Systems an die Erzeugungsstrukturen.
Beispiel: Abstrakte Fabrik, Erbauer, Singleton
Strukturmuster
Sie zeigen auf, auf welche Art und Weise Klassen bzw. Objekte zu größeren Strukturen zusammengefasst werden können. Die von Strukturmustern beschriebenen Strukturen entstehen zur Laufzeit.
Beispiel: Adapter, Fassade, Proxy
Verhaltensmuster
Sie beschreiben Strukturen, die am Kontrollfluss innerhalb der Anwendung beteiligt sind. Sie konzentrieren sich also auf Algorithmen und die Delegation von Zuständigkeiten.
Beispiel: Beobachter, Besucher, Strategie
Ein erstes konkretes Entwurfsmuster – das Singleton
Die Absicht
Es soll verhindert werden, dass von einem Objekt mehrere Instanzen erzeugt werden können. Dies kann notwendig werden, wenn ein Objekt eine zentral zur Verfügung stehende Ressource nutzen soll, z. B. Druckerwarteschlange, Protokolldatei.
Lösungsidee
Die Klasse, aus der nur ein einziges Objekt erstellt werden soll, verhindert durch ihren Aufbau selbst das Erzeugen mehrerer Objekte. Dazu ist im Folgenden der prinzipielle Aufbau dieser Klasse dargestellt.
Das Klassendiagramm zum Singleton:
Im Klassendiagramm stellen die unterstrichenen Elemente sogenannte Klassenelemente dar (Klasseneigenschaft,
Klassenmethode). Dazu dient das Schlüsselwort static
.
Interfaces
Wird in einer abstrakten Klasse keinerlei Code implementiert, sondern nur nach außen sichtbare Methoden deklariert, so benutzt man in C# dafür ein Interface. Von einem Interface kann keine Instanz erzeugt werden. Das Interface sichert zu, dass Klassen, die es implementieren, die im Interface enthaltenen Methodendeklarationen implementieren.
Im Gegensatz zur Vererbung können Klassen beliebig viele Interfaces implementieren, aber immer nur von einer Oberklasse erben. Dadurch können Objekten gewisse Rollen anhand der Interfacemethoden zugewiesen werden.
In der UML wird ein Interface wie folgt dargestellt:
Das Interface Arbeitend
kann dabei als vorgegebene Schnittstelle verstanden werden, die zusichert, dass alle Klassen
diese Schnittstelle besitzen, die das Interface implementieren. Hier muss die Klasse Schueler
und die Klasse Lehrer
die Methode arbeite()
implementieren, dass die Zusicherung erfüllt wird.
Im C#-Quellcode wird das Interface wie folgt codiert:
public interface Arbeitend
{
void arbeite();
}
Das Implementieren des Interfaces im Code:
public class Lehrer : Mensch , Arbeitend
{
...
public void arbeite()
{
//Implementierung der Methode arbeite() aus dem Interface
}
}
Gegenüberstellung: Abstrakte Klassen – Interface
Abstrakte Klassen | Interfaces | |
---|---|---|
Methoden | konkrete, virtuelle und abstrakte Methoden | implizit abstrakte Methoden |
Attribute | beliebig | Keine - In manchen Programmiersprachen als static final möglich |
Vererbung | eine Klasse kann nur von einer einzigen abstrakten Klasse erben (keine Mehrfachvererbung) | eine Klasse kann beliebig viele Interfaces implementieren (und zusätzlich von einer Klasse erben) |
Objektbildung | keine Objektbildung möglich. Von einer abstrakten Klasse abstammende Unterklasse können Objekte gebildet werden (alle abstrakten Methoden müssen überschrieben werden!) | keine Objektbildung möglich. Es können von einer ein Interface implementierenden Klasse Objekte gebildet werden (alle im Interface deklarierten Methoden müssen überschrieben werden!) |
Observer
- Die Absicht: Objekte können Daten bei einem Informationsanbieter abonnieren. Bei jeder Änderung der abonnierten Daten werden die Abonnenten automatisch über die Änderung informiert, die sich dann die geänderten Daten abholen.
- Das Problem: Voneinander abhängige Objekte (Info-Anbieter und Info-Konsument) sollen nicht zu stark aneinandergekoppelt werden, was die Wiederverwendbarkeit stark einschränken würde.
- Die Lösungsidee: Trennung von Informationsbereitsteller und einer Menge von Informationsverarbeitern bzw. Darstellern der Information. Die Bereitsteller müssen die Verarbeiter nicht direkt kennen: Keine direkte Kommunikation über Aufrufe, sondern eine indirekte über Benachrichtigungen.
Das Klassendiagramm zum Beobachter:
Erklärungen zum Observer:
- Der Info-Anbieter ist das konkrete Subjekt, der Info-Konsument ist der konkrete Beobachter.
- Es können beliebig viele Observer-Objekte ein Subjekt beobachten, nachdem sie bei ihm registriert wurden
(
Attach()
). - Wenn ein Beobachter-Objekt die Beobachtung des Subjektes beenden möchte, muss es sich bei ihm abmelden
(
Detach()
). - Der Beobachter stellt eine Schnittstelle (
Update()
) zur Verfügung, die vom Subjekt aufgerufen werden kann, wenn dessen Zustand sich geändert hat.
Immer dann, wenn der Zustand des Subjektes geändert wird (setSubjectState()
), muss dessen MethodeNotify()
aufgerufen werden, die dann ihrerseits dieUpdate()
-Methode aller registrierten Beobachter aufruft. - Der konkrete Beobachter implementiert die Aktualisierungsmethode (
Update()
) des abstrakten Beobachters, um über Zustands-Änderungen des Subjektes informiert werden zu können.
Der Beobachter kann dann entweder über eine gespeicherte Subjekt-Referenz den neuen Subjektzustand beim Subjekt abholen (getSubjectState()
), oder das Subjekt übergibt derUpdate()
-Methode des Beobachters einen Parameter.
Man kann das Szenario nun leicht um weitere Observer erweitern, die von einem ganz anderen Typ sind, dennoch aber ohne Änderung des restlichen Programmcodes mitverwaltet werden:
Das Adapter Pattern
TODO - Siehe https://de.wikipedia.org/wiki/Adapter_(Entwurfsmuster)
Das Strategie Pattern
TODO - Siehe https://de.wikipedia.org/wiki/Strategie_(Entwurfsmuster)