Artikel

Juli 2003 | Artikel

Rollenspiele

(Link zum Artikel: http://www.it-republik.de/dotnet/artikel/0389)

Einsatz des Role Object Patterns in der Anwendungsentwicklung

Text: von Dominik R. Tornow
  • Teilen
  • kommentieren
  • empfehlen
  • Bookmark and Share
Objekt-orientierte Informationssysteme beruhen im Allgemeinen auf einer begrenzten Menge von First-Class Entitäten. First-Class Entitäten werden für gewöhnlich durch eine Klasse modelliert. Dies ist ausreichend für kleinere Anwendungen, doch größere Applikationen müssen in der Lage sein, mit kontextspezifischen Sichten auf eine Entität umzugehen. In diesem Artikel möchte ich Ihnen die Konzepte verschiedener Lösungsvorschläge erläutern und auf ausgewählte Schlüsselpunkte der Implementierung eingehen.

Stellen Sie sich vor, Sie sind beauftragt, eine Applikation zu entwickeln, die zur Unterstützung der Personalwirtschaft einer Firma eingesetzt werden soll. Unsere - doch recht simpel gestrickte - Anwendung kennt verschiedene Positionen, die ein Mitarbeiter wahrnehmen kann; er kann als Manager, Ingenieur und Verkäufer im Unternehmen tätig sein. Abhängig von seiner Rolle sind verschiedene Eigenschaften des Mitarbeiters zu unterschiedlichen Zeiten interessant. Einem Manager sind beispielsweise verschiedene Projekte zugeordnet, für die er verantwortlich ist, während bei einem Ingenieur möglicherweise die Zahl seiner angemeldeten Patente von Interesse ist. Bei einem Verkäufer wiederum sind die von ihm erreichten Verkaufszahlen ausschlaggebend, z.B. um ihm eine angemessene Prämie zukommen zu lassen. Denkbar ist auch, dass alle Rollen im Unternehmen eine gemeinsame Eigenschaft besitzen, ihr konkreter Wert jedoch von einer Rolle abhängig ist. Denken Sie z.B. an ein Budget, das einem Mitarbeiter zugeordnet ist. So kann ein Manager bzw. Abteilungsleiter sicherlich über größere Geldbeträge verfügen als ein Ingenieur oder Verkäufer. Abpictureung 1 zeigt die eben geschilderte Situation.

Nun ist es typischerweise so, dass ein Angestellter einer Firma mehrere Aufgaben zur selben Zeit wahrnimmt. In einer Abteilung unserer fiktiven Unternehmung ist es also durchaus möglich, dass ein Ingenieur die Aufgaben des Abteilungsleiters wahrnimmt; außerdem kann er ja selbst Präsentationen durchführen, Beziehungen zu Kunden pflegen und somit diesen als Verkäufer gegenübertreten. Wie Sie sehen, lässt sich eine Person nicht unbedingt auf eine Rolle im Unternehmen festnageln. In Abpictureung 1 sind daher auch exemplarisch die konkreten Werte, die einem Mitarbeiter zugeordnet werden können, aufgelistet (zu sehen im Personalstammblatt).

Unsere Anwendung muss folglich in der Lage sein, mit verschiedenen Aspekten eines Angestellten umzugehen. Fassen wir die zentralen Anforderungen an eine Rolle eines Mitarbeiters zusammen, so ergeben sich folgende Schlüsselpunkte:
  • Ein Mitarbeiter kann zu einem Zeitpunkt verschiedene Rollen im Unternehmen spielen.
  • Die Rollen eines Mitarbeiters können über einen Zeitraum wechseln.
Wir wollen nun im Folgenden verschiedene Möglichkeiten betrachten, den gestellten Anforderungen gerecht zu werden.
Die Trivialmethode
Die einfachste Vorgehensweise wird es wohl sein, alle kontextspezifischen Anforderungen in einer Schnittstelle zu verpacken - Einen entsprechenden Vorschlag sehen Sie in Abpictureung 2. Derartig entworfene Schnittstellen sind schwer zu verstehen und noch schwerer zu pflegen und zu unterhalten. Unvorhergesehene Änderungen werden höchstwahrscheinlich Änderungen in großen Teilen der Applikation nach sich ziehen.
Ich möchte nicht sagen, dass diese Lösung nicht funktioniert. Nach meiner Auffassung ist sie aber nur in den seltensten Fällen adäquat und sollte nur genutzt werden, wenn es sich nur um wenige Rollen und um wenige kontextspezifische Informationen handelt.
Vererbung
Ein weiterer Ansatz bestünde darin, den Typ Person mittels Vererbung zu erweitern und neue Typen Manager, Ingenieur und Verkäufer hinzuzufügen, die kontextspezifisches Verhalten definieren. Dies ist in Abpictureung 3 dargestellt. Nun stellt sich jedoch ein neues Problem ein: Eine Person, die sowohl als Manager als auch Ingenieur im Unternehmen auftritt, wird dann in der Anwendung von zwei unterschiedlichen Objekten repräsentiert, obwohl es sich eindeutig um ein und dieselbe Person handelt. Identität könnte dann nur durch zusätzliche Mittel simuliert werden - z.B. müsste ein Exemplar der Manager-Klasse alle anderen Exemplare der Klassen Ingenieur und Verkäufer über eine Änderung des Namens oder Vornamens informieren, um Datenintegrität zwischen den verschiedenen Objekten sicherzustellen. Dass die Implementierung eines solchen Mechanismus' alles andere als trivial ausfallen wird, muss kaum hinzugefügt werden. Wiederum ist zu sagen, dass auch dieser Ansatz durchaus seine Berechtigung hat. Sie können ihn verwenden, wenn Sie sicher sind, dass zu einem Zeitpunkt nur ein Exemplar einer Rolle verwendet wird - genau dann müssen Sie sich keine Gedanken machen, ob und wie Zustandsänderungen die Datenintegrität beeinflussen.
State Pattern
Das State Pattern ermöglicht es einem Objekt, sein Verhalten während der Laufzeit zu ändern, indem sich sein interner Zustand dem jeweiligen Kontext anpasst. Ein Client dieses Objekts wird den Eindruck haben, dass das Objekt seine Implementierung (aber nicht seinen Typ!) gewechselt habe. Das Klassendiagramm des Zustandsmusters, angewandt auf unser Problem, ist in Abpictureung 4 zu sehen - eine ausführliche Diskussion des State Pattern findet sich in [1]
Der Typ Person definiert die Schnittstelle einer Person, die für alle Klienten von Interesse ist. Ein Personen-Objekt verwaltet ein Exemplar eines Person-Role-Objekts, also ein Exemplar vom Typ Manager, Ingenieur oder Verkäufer. Kontextspezifisches Verhalten wird dadurch realisiert, dass eine Anfrage an ein Personen-Objekt an die gerade aktive Rolle weitergeleitet wird, wie in Abpictureung 4 angedeutet. Möchte ein Client das Budget eines Mitarbeiters erfragen, so ist der zurückgelieferte Betrag von der Rolle abhängig, in der sich diese Person befindet. Dazu hat der Client mittels der Operation SetRole die Möglichkeit, ein Personen-Objekt in die gewünschte Rolle zu versetzen.

Ich möchte an dieser Stelle besonders betonen, dass rollenspezifisches Verhalten durch ein und dasselbe Interface angesprochen werden kann und muss. Dies hat für Ihre Applikation folgende Konsequenzen: Sie können in der Applikation neue Rollen definieren oder die Implementierung bestehender Rollen verändern, ohne dass dies Auswirkungen auf die ursprüngliche Anwendung hätte. Andererseits können Sie keine Attribute oder Operationen definieren, die nur von einer Untermenge aller Rollen angeboten wird. In unserem Fall heißt das, dass das Budget, das ja sehr wohl von der Rolle eines Mitarbeiters abhängig ist, mit Hilfe dieses Musters implementiert werden kann; Attribute wie Projekte, Patente oder Verkaufszahlen haben jedoch im gemeinsamen Interface keinen Platz und müssen daher mit Hilfe anderer Muster angeboten werden.
Role Object
Kern dieses Artikels pictureet das - im Folgenden beschriebene - Role Object Pattern. Eine ausführliche Beschreibung verschiedener Varianten dieses Musters findet sich in [2].
Der Aufbau des Role Object Patterns ähnelt sehr dem Aufbau des eben besprochenen State Patterns. Der wesentliche Unterschied zum Zustandsmuster liegt im Umgang mit den verschiedenen Schnittstellen begründet. Werden Rollen durch interne Zustände modelliert, sind die konkreten Objekte Manager, Ingenieur und Verkäufer vor dem Client verborgen. Wenn der Wert eines Budgets erfragt werden soll, dann wird diese Anfrage nicht an eine Rolle, sondern an das Personen-Objekt gerichtet; dieses wird den Aufruf an die gerade aktive Rolle weiterleiten und somit das passende Ergebnis liefern. Wenn das Role Object Pattern verwendet wird, erhält der Client eine Referenz auf eine konkrete Rolle - wie Sie ja unschwer in Abpictureung 5 erkennen - und arbeitet fortan mit dieser Rolle weiter. Anfragen richtet der Client nun nicht mehr an ein Exemplar einer Person, sondern direkt an ein Objekt vom Typ Manager, Ingenieur oder Verkäufer. Dieses Muster ist dann bestens geeignet, wenn First-Class Entitäten in verschiedenen Kontexten auftreten können und das kontextspezifische Verhalten nicht durch eine Schnittstelle abgedeckt werden kann. Zwei Ausprägungen dieses Musters sind besonders interessant, weshalb kurz auf diese eingegangen werden soll.
Static Roles
Diese Variante des Role Object Patterns verlangt, dass Sie sich bereits zur Compile-Zeit der Menge aller Rollen, die eine Entität spielen kann, bewusst sind. Das heißt, dass eine Person nur in den genannten Rollen des Managers, Ingenieurs oder Verkäufers auftreten kann; andere Rollen wie z.B. die Rolle eines Conultants können von einem Personen-Objekt nicht übernommen werden. Dies mag zwar im ersten Augenblick als ein gravierender Nachteil erscheinen, ist aber in vielen Fällen ausreichend. Der große Vorteil dieses Musters liegt darin begründet, dass bereits der Compiler prüfen kann, dass Sie Objekte nur in diejenigen Rollen versetzen, die das Objekt auch wirklich spielen kann. Um dieses Feature zu verwenden, werde ich Ihnen im Abschnitt Implementierung zeigen, wie Sie den Mechanismus der Operatorüberladung nutzen können.
Dynamic Roles
Die dynamische Vergabe von Rollen stellt zweifelsohne die mächtigste Variante aller besprochenen Muster dar. Sie erlaubt es Ihnen, Objekten zur Laufzeit verschiedene Rollen hinzuzufügen und diese auch wieder zu entfernen. Dazu müssen im Personen-Objekt Operationen definiert werden, die die Verwaltung von Rollen ermöglicht (Abb. 6). Dieser Ansatz birgt natürlich höhere Komplexität in sich und versteckt noch weitere Tücken: Sie müssen sicher gehen, dass Objekten keine Rollen zugewiesen werden, die sie nicht ausführen können - beim dynamischen Ansatz ist es nicht möglich, dass Sie der Compiler vor derartigem Missbrauch bewahrt.
Implementierung
Bisher haben wir ja sehr viel Zeit mit der Diskussion der konzeptionellen Grundlagen verbracht - nun wollen wir der Realisierung unserer Ideen nachgehen. Ich möchte mich hier speziell, wie angekündigt, dem Static Roles-Muster widmen. C# und das .NET Framework geben uns einen für diesen Zweck sehr eleganten Mechanismus an die Hand: Operatorüberladung, speziell die Überladung des cast-Operators. Wenn Sie sich näher mit den Möglichkeiten der Operatorüberladung des .NET Frameworks beschäftigen möchten, kann ich Ihnen den Artikel Als die Klassen rechnen lernten [3] empfehlen. Eine exemplarische Implementierung der Klassen Person, Manager, Ingenieur und Verkäufer finden Sie in Listings 1 bis 4. Lenken Sie Ihre Aufmerksamkeit bitte auf folgende Punkte: Das Dictionary Roles der Klasse Person hält eine Referenz auf alle Rollen-Exemplare, die ein Objekt vom Typ Person spielen kann. Die Überladung des cast-Operators erlaubt es uns, sehr elegant auf die einzelnen Aspekte eines Angestellten zuzugreifen. In anderen Sprachen, wie z.B. Java wären dagegen weitere Methoden erforderlich, die uns die gewünschte Funktionalität zur Verfügung stellen. Die Handhabung des cast-Operators soll Ihnen der Code-Auszug in Listing 5 vermitteln, der das Personalstammblatt aus Abpictureung 1 erzeugt.

Listing 1
  1. public class Person
  2. {
  3. Dictionary Roles = new Dictionary();
  4. //
  5. // Überladung des cast-Operators
  6. //
  7. public static explicit operator Manager(Person person)
  8. {
  9. return (Manager)person.Roles["Manager"];
  10. }
  11. public static explicit operator Ingenieur (Person person){
  12. return (Ingenieur)person.Roles["Ingenieur"];
  13. }
  14. public static explicit operator Verkäufer (Person person)
  15. {
  16. return (Verkäufer)person.Roles["Verkäufer"];
  17. }
  18. public static Person GetPersonByName(String Name, String Vorname)
  19. {
  20. // Suchfunktion ..
  21. }
  22. public Person()
  23. {
  24. Roles.add("Manager", new Manager());
  25. Roles.add("Ingenieur", new Ingenieur());
  26. Roles.add("Verkäufer", new Verkäufer());
  27. }
  28. public String Name
  29. {
  30. // ..
  31. }
  32. public String Vorname
  33. {
  34. // ..
  35. }
  36. }
Listing 2
  1. public class Manager
  2. {
  3. public Manager()
  4. {
  5. // ..
  6. }
  7. public int Projekte
  8. {
  9. // ..
  10. }
  11. }
Listing 3
  1. public class Ingenieur
  2. {
  3. public Ingenieur()
  4. {
  5. // ..
  6. }
  7. public int Patente
  8. {
  9. // ..
  10. }
  11. }
Listing 4
  1. public class Verkäufer
  2. {
  3. public Verkäufer()
  4. {
  5. // ..
  6. }
  7. public int Verkaufszahlen
  8. {
  9. // ..
  10. }
  11. }
Listing 5
  1. // Mitarbeiter Dominik Tornow im Datenbestand suchen
  2. Person person = Person.GetPersonByName ("Dominik", "Tornow");
  3. ..
  4. // Allgemeine Informationen
  5. Console.Out.WriteLine ("Name: " + person.Name);
  6. Console.Out.WriteLine ("Vorname: " + person.Vorname);
  7. <u>try</u><a>[Author ID0: at ]</a>
  8. {
  9. // Informationen, die einen Manager betreffen
  10. Manager manager = (Manager) person;
  11. Console.Out.WriteLine ("Budget: " + manager.Budget);
  12. Console.Out.WriteLine ("Projekte: " + manager.Projekte);
  13. // Informationen, die einen Ingenieur betreffen
  14. Ingenieur ingenieur = (Ingenieur) person;
  15. Console.Out.WriteLine ("Budget: " + ingenieur.Budget);
  16. Console.Out.WriteLine ("Patente: " + ingenieur.Patente);
  17. // ..
  18. }
  19. catch(NotInRoleException r)
  20. {
  21. // ..
  22. }
  23. catch(Exception e)
  24. {
  25. // ..
  26. }
Fazit
Die hier vorgestellten Möglichkeiten geben Ihnen ein Werkzeug an die Hand, mit kontextspezifischen Anforderungen an First-Class Entitäten - auch über Anwendungsgrenzen hinweg - umzugehen. Dies ist jedoch noch keine erschöpfende Diskussion der verwendbaren Mittel. Gerade das hier diskutierte Rollenspiel lässt sich in einer Unzahl von Variationen wiederfinden. Ich hoffe, ich konnte Ihnen eine verständliche Einführung in das Thema bieten und Ihnen einen ersten Eindruck der unterschiedlichen Möglichkeiten verschaffen. Der interessierte Leser sei an dieser Stelle auf den - wie ich persönlich finde - ausgezeichneten Artikel Dealing with Roles von Martin Fowler [2] verwiesen.
Links und Literatur


Anzeige

Kommentare

zurück zum Seitenanfang