Tutorial Softwareentwicklung2

Aus DGL Wiki
Wechseln zu: Navigation, Suche

Grundlagen der Softwareentwicklung
Finden der richtigen Struktur

Vorwort

Hallo und willkommen zum zweiten Teil der Reihe "Grundlagen der Softwareentwicklung". Dieses Tutorial ist das größte und auch wichtigste Tutorial der Reihe. Wir werden uns mit Klassen, Klassendiagrammen, Paketen und Beziehungen zwischen Klassen bzw. Paketen beschäftigen.

Die meisten von euch werden Klassen "On Demand" gebaut haben. Also immer dann wenn ihr das Gefühl hattet, dass man diese und jene Eigenschaft schön zusammen in eine Klassen stecken könnte. Das Gefühl täuscht. Da wir uns in dieser Tutorialreihe etwas von den Bauchentscheidungen weg, und etwas mehr zu den Kopfentscheidungen hin bewegen wollen, wird ein zentraler Punkt auch sein, was Klassen eigentlich machen dürfen, und was nicht. Wenn ihr eure Projekte einmal unter diesen Gesichtspunkten betrachtet werden ihr mit unter schockiert sein, wie offensichtlich so manche Probleme ihren Anfang nehmen.

Auch dieser Teil baut auf dem Buch "Visual Modeling with Rational Rose 2000 and UML" (ISBN 0201699613) von Terry Quatrani auf.

Info DGL.png Nicht alle beschriebenen Funktionen sind in kostenlosen Tools implementiert. Da auch ich nicht tausende Euros für Projektmanagement Software ausgeben kann, sind einige Bilder in einem Bildbearbeitungsprogramm entstanden. Grundlage für die Bilder waren die Abbildungen im oben genannten Buch.

Voraussetzungen

Ihr solltet natürlich das erste Tutorial gelesen haben. Weiterhin ist Grundlagenwissen im Bezug auf Objekt orientierte Programmierung erforderlich. D.h. ihr solltet wissen, was Methoden und Attribute sind und ihr solltet den Unterschied zwischen einer Klasse und einem Objekt zumindest erahnen können.


Ziele

Ihr werdet in diesem Tutorial lernen was Klassendiagramme sind und was in ihnen dargestellt wird.

Klassendiagramme zeigen Klassen, Pakete und deren Beziehungen untereinander. Aus diesem Grund werden wir sehen, wie man diese Elemente aus den gemachten Vorarbeiten (siehe Tutorial Softwareentwicklung1) extrahieren kann.

Außerdem werdet ihr lernen welche Klassentypen es gibt, was sie können und was eine gute Klasse ausmacht.


Aber nun sollten wir endlich anfangen.


Klassen finden

In allen Vorarbeiten die wir bisher gemacht haben ist von Klassen keine Spur. Wieso?

Der Punkt ist, dass die Vorarbeiten dazu dienen das Wissen des Kunden zu sammeln, und so zu strukturieren, dass die Informatiker daraus ein Programm machen können. Mit Klassen kann der Kunde nichts anfangen, deshalb lässt man ihn mit so was in Frieden. Allerdings hat er, ohne es bewusst zu tun, in der Aufgabenspezifikation uns bereits gesagt, welche Klassen wir wahrscheinlich brauchen werden. Ganz recht, der Kunde hat uns das mitgeteilt. Denn die Projektspezifikation ist voll mit Beschreibungen von Vorgängen und Substantiven. Diese können für uns eine Grundlage sein, unsere Klassen zu planen.

Doch der Reihe nach.


Grundlegendes

Was ist ein Objekt, und was ist eine Klasse?

Laut Buch ist "ein Objekt ein Konzept, Abstraktion oder allgemeiner alles was abgrenzbar und für die Anwendung von Bedeutung ist."

Das heißt, alle Dinge und Vorgänge sind prinzipiell Objekte. Der Rechner vor dem ihr sitzt, die Hausaufgaben die ihr hättet machen sollen, der Produktionsprozess in einer Maschinenfirma und auch ihr selbst.

OK...Ich hab's verstanden...glaub ich. - Und was sind Klassen?

Nun...ihr habt z.B. den einen Rechner an dem ihr gerade sitzt. Aber es gibt ja noch andere Rechner auf der Welt. Große, kleine, alte, neue, umgebaute, Multiprozessorrechner, Cluster, Superrechner, Mainframes, ... ne Menge! Alle diese Rechner sind Rechner. Sie haben gemeinsame Eigenschaften und können das selbe, und doch sind sie nicht ein Objekt, sondern viele.

Das Konzept der Klasse beschreibt genau diese Tatsache: Euer PC zuhause ist ein Objekt der Klasse Computer.

Ein Objekt ist also eine Repräsentation oder auch Instanz einer Klasse. Wenn man von Eigenschaften einer Klassen spricht, dann spricht man über Eigenschaften die alle Objekte dieser Klasse haben. Wenn man über Eigenschaften von Objekten redet, dann bezieht man sich darauf, wie diese Eigenschaften bei dem speziellen Objekt ausgeprägt (z.B. mit Werten gefüllt) sind.


Vorerst haben wir es mit "Klassenkandidaten" zu tun. Das sind Klassen die wahrscheinlich im späteren System benutzt werden. Allerdings kann es auch passieren, dass man Sie noch aufteilt, zusammenpackt oder gänzlich weg lässt. Erst im Schritt "Klassen strukturieren" hat man einen Basissatz an Klassen zur Verfügung, welche später in der Implementation auch benutzt werden.


Klassendiagramme

Nachdem ich euch jetzt etwas verwirrt habe bewerfe ich euch nun mit Diagrammen, damit ihr wieder zu euch kommt. ;)

Das Klassendiagramm ist das wichtigste Diagramm welches UML für uns Informatiker zur Verfügung stellt. Denn hier manifestiert sich, was wir bisher (glauben) heraus gefunden (zu) haben. Das Klassendiagramm ist quasi Endstation aller Erkenntnisse. Aus Klassendiagrammen können viele UML-Editoren Quellcode generieren. D.h. ihr bekommt leere Hüllen (z.B. leere Funktionen) und ihr müsst Sie nur noch mit Code füllen.

Auch der umgekehrte Weg führt zu Klassendiagrammen: Wenn man einen bestehenden Quellcode analysieren lässt erstellt der Editor euch die entsprechenden Klassendiagramme. (Nicht jeder Editor kann das. Aber vor allem, bei den kommerziellen Tools findet man diese Funktion.)


Was macht eine gute Klasse aus?

Zuerst ein paar grundlegende aber enorm wichtige Regeln.

1. Die goldene Regel des Objekt orientierten Entwurfs ist: Eine Klasse hat eine und nur diese eine Aufgabe!

Nehmen wir einmal an wir haben ein Klasse 'Fahrzeug'. Fahrzeuge haben verschiedene Eigenschaften. Unter anderem Gehen sie auch gerne mal kaputt. Deshalb schickt man sie zur Wartung. Was bei der letzten Wartung gemacht wurde, und wann die Wartung war speichert man natürlich in der Klasse Fahrzeug ab, es gehört ja mit dazu. - FALSCH! Die Klasse Fahrzeug enthält all die Dinge die unmittelbar zum Fahrzeug gehören. Die Wartung, wann sie war und was gemacht wurde ist ein eigenes Thema. Es ist "abgrenzbar" und gehört somit in eine eigene Klasse 'Fahrzeugwartung'. Das 'Fahrzeug' selbst besitzt eine Beziehung (was das ist werden wir noch sehen) zur Fahrzeugwartung.


2. Klassen werden mit Begriffen benannt wie sie im Einsatzgebiet der Software gängig sind.

Wieso das? Der Kunde sieht doch den Code nicht!
Durch diese Regelung schafft man es leicht Missverständnisse zwischen Programmierer (Code) und Auftraggeber (Anforderung) zu vermeiden. Auch auf die Frage des Auftraggebers hin, was gerade so gemacht wird, ist es weitaus besser ihm einen Begriff zu liefern den er kennt, als irgend eine seltsame Bezeichnung die nur Informatiker verstehen (was vor allem für Informatiker schwer einzuschätzen ist). Auch bei Gesprächen über die Funktionalität von Programmteilen ist die Kommunikation leichter, wenn von beiden Seiten die selben Begriffe benutzt werden. (Dies gilt nebenbei auch für die Dokumentation.)
Wenn die Software für ein deutsches Unternehmen geschrieben wird, und man deutsch kann, ist es zulässig den Code mit deutschen Bezeichnern zu versehen. Falls man es mit einer international operierenden Firma zu tun hat, sollte man englisch bevorzugen. Da der Code einer Software meist zum Lieferumfang gehört (also nicht im Besitz des Programmierers bleiben muss), ist es besser sich mit dem Auftraggeber über solche Punkte abzustimmen.


3. Synonyme werden als Namen nur verwendet, wenn sie für alle Beteiligten eindeutig sind:

Synonyme - Wörter die das gleiche bedeuten - haben mitunter für die verschiedenen Beteiligten unterschiedliche Bedeutungen. Z.B. könnte es in einer Firma ein Produkt mit dem Namen "A.U.T.O." geben. Wenn innerhalb der Software ein Firmenfahrzeug als "Auto" bezeichnet werden, kann das schnell zu Missverständnissen führen.


4. Klassennamen sind in der Einzahl.

Dieser Punkt sollte unstrittig sein und eigentlich von jedem Programmierer intuitiv so gemacht werden.


Wenn man Klassen findet werden diese (bzw. ihre Aufgaben) sofort dokumentiert.

Über die Namensvergabe und Aufgabendefinition kann man herausfinden, ob die gefundene Klasse tatsächlich etwas taugt. Man muss sich dazu nur folgende Fragen stellen:

Ich kann problemlos einen Namen finden und eine knappe präzise Definition geben. 

-> Es wurde ein guter Klassenkandidat gefunden

Ich kann einen Namen finden, aber die Definition entspricht der einer anderen Klasse. 

-> Beide Klassenkandidaten sollten kombiniert werden

Ich kann einen Namen finden, aber die Definition ist ein kleiner Roman. 

-> Die Klasse sollte aufgeteilt werden

Ich kann weder Name noch Definition angeben. 

-> Wird die Klasse wirklich gebraucht? Wenn ja: Analysiere das Modell genauer um die nötigen Eigenschaften und Aufgaben zu finden.

Das B-C-E Prinzip

B-C-E steht für Boundary, Control und Entity und beschreibt die 3 Klassentypen die es gibt. (Natürlich gibt es verschiedene Ansätze welche Klassen es in einer SW gibt, und welche Funktionen sie haben. Doch das B-C-E Prinzip ist leicht verständlich und gut umsetzbar.) Man nennt dies auch das Model-View-Controler-Prinzip, was letztlich das selbe ist.

Tutorial SWE2 BCEprinzip.jpg

Man kann eine Software mit diesem Prinzip wie eine Zwiebel aufbauen. Sie besteht aus den 3 Schichten die jeweils aus Klassen eines Typs(Boundary, Control bzw. Entity) bestehen. Die Grafik rechts zeigt diesen Ansatz.

Wie wir weiter oben gesehen haben, gibt es für Klassen bereits ein UML Symbol. Dieses wird auch in anderen Diagrammen benutzt. Für die 3 genannten Klassentypen bietet aber UML noch eigene Symbole, damit man diese in Diagrammen leichter unterscheiden kann. (Man nennt Boundary, Control und Entity-Klassen auch Stereotypen von Klassen.)


Boundary-Klassen - Das Tor zur Außenwelt

UML Symbol für Boundary- Klassen

Boundary ist englisch und steht für Grenze. Boundary-Klassen grenzen die Software von der Umgebung ab. Boundary-Klassen sind das Interface der Software.

Kein Akteur (siehe Tutorial1 "Use-Case-Diagramme") kann mit einem System kommunizieren, ohne eine Boundary-Klasse zu nutzen.

Jeder Weg in oder aus der Software muss über eine Boundary-Klasse führen. Alle Formulare und GUI Elemente sind Boundary-Klassen. Ebenso wie Schnittstellen zu Datenbanken, Importschnittstellen oder Schnittstellen zu anderer Software.

Boundary-Klassen sind keine Datenspeicher. Sie enthalten die eingegebenen bzw. angezeigten Daten zwar, aber niemand kann sich darauf verlassen, dass sie unverändert bleiben. Für die Speicherung von Daten ist ein anderer Typ zuständig.

Die GUI ist meist sehr programmspezifisch. Nur wenige Boundary-Klassen werden wieder verwendet. Aber da keine Daten und Funktionen in den Boundary-Klassen untergebracht sind, steigt die Wahrscheinlichkeit zumindest die allgemeinen Teile der GUI weiter zu verwenden.


Finden von Boundary-Klassen

Aus jedem Akteur-Szenario-Paar kann eine Boundary-Klasse extrahiert werden. Wir werden später noch zum Thema Szenarios kommen, da man Sie benötigt um Objektinteraktionen zu finden.

Die GUIs werden Anfangs nur grob umrissen. Es geht hier noch nicht darum alle Dialogfelder zu bestimmen. Das Design kann durch GUI-Prototypen erarbeitet werden. Es ist aber wichtig, sobald man eine Boundary-Klasse gefunden hat, deren Funktion/Aufgabe zu dokumentieren.

Da wir wissen, dass die Erarbeitung von Klassen ein iterativer Prozess ist, kann es passieren, dass im weiteren Verlauf Klassen hinzu kommen, oder mehrere Klassen zusammengefasst werden. Deshalb wäre es völlig verfrüht, jetzt schon zuviel Energie in das GUI Design zu stecken.

Entity-Klassen - Datenspeicher

UML Symbol für Entity- Klassen

Entity-Klassen haben nur eine Aufgabe: Sie speichern eine ganz bestimmte Information. Sie verfügen meist über sehr wenige Funktionen neben den "Set"- und "Get"-Funktionen für die gespeicherten Daten. Alle Funktionen die Sie besitzen haben unmittelbar mit den gespeicherten Daten zu tun (das schließt durchaus Datenmanipulation ein). Sobald aber andere Klassen gerufen werden müssen, ist die Funktion mit großer Sicherheit bei der Entity-Klasse falsch postiert.

Entity-Klassen modellieren sozusagen Daten und Informationen samt dem dazugehörigen Verhalten.


Ein Beispiel:

Die Entity-Klasse 'Mitarbeiter' speichert Informationen über Mitarbeiter einer Firma wie z.B. Name, Anschrift, Telefonnummer, Krankenkasse, Gehalt ... .

Eine Funktion 'getName' gehört ganz klar zu 'Mitarbeiter'. Auch 'getTelefonNummerAsString' ist OK, denn es werden nur die Informationen der Entity-Klasse in veränderter Form zurückgegeben. Eine Funktion 'setAnschrift' kann OK sein, wenn nur die Anschrift geändert wird. Falls aber im Zuge der Anschriftsänderung auch die Personalabteilung über die Änderung benachrichtigt werden soll, ist es besser die Methode als 'AendereAnschrift' einer Control-Klasse zu geben, und die "Set"-Funktion für die Anschrift so einfach wie möglich zu halten.


Ein zweites Beispiel: In der Computergrafik arbeitet man ja öfters mit Texturen. Texturklassen könnten z.B. den OpenGL Namen und die Daten sowie den Dateinamen speichern. Zusätzlich wäre es aber auch günstig Funktionen wie "flipHorizontal" (horizontale Spiegelung der Textur) oder ähnliches zu implementieren. Dies ist ohne weiteres in einer Entity Klasse möglich, denn es werden nur die lokal gespeicherten Daten manipuliert. Noch wichtiger: Es werden keine anderen Klassen benötigt.


Entity-Klassen sind unabhängig von der Umgebung bzw. wie die Umgebung mit der Software kommuniziert, denn niemand kann direkt auf sie Zugreifen. Alle Aktionen von außen werden von Boundary-Klassen aufgenommen, an Control-Klassen weitergeleitet und erst dann an die Entity-Klassen übergeben. Entity-Klassen sind meist auch sehr allgemein gehalten. Spezielle Funktionalitäten werden (wie oben gezeigt) an Control-Klassen übergeben. Durch diese Einfachheit und die Kapselung sind Entity-Klassen sehr gut wieder verwendbar.


Finden von Entity-Klassen

Um Entity-Klassen zu finden untersucht man zuerst einmal die Ereignisflüsse nach zu erledigenden Aufgaben (Verantwortlichkeiten). Entity-Klassen sind dann die Objekte die benötigt werden um die Aufgaben zu erledigen.

Weiterhin untersucht man die Aufgabenbeschreibung nach Substantiven und Substantivierungen. Diese deuten meist auch auf Entity-Klassen hin.

Dabei müssen natürlich die Substantive ausgeschlossen werden welche

  • nicht zur Aufgabe gehören
  • redundant sind (das selbe Objekt bezeichnen)
  • Sprachbedingt sind
  • den Aufbau von Klassen beschreiben (also die Attribute bezeichnen).

Die restlichen Substantive sind vermutlich Entity-Klassen.


Control-Klassen - Herz des Systems

UML Symbol für Control- Klassen

Control-Klassen sind das Steuerwerk eurer Software. Meistens steuert eine Control-Klasse die Abläufe in einem Use-Case. Hin und wieder können sie auch mehrere steuern. Doch sollte immer darauf geachtet werden, dass die "goldene Regel" (Jede Klasse hat genau eine Aufgabe) nicht verletzt wird.

Control-Klassen speichern keine Daten. Sie leiten Daten von Boundary-Klassen zu Entity-Klassen weiter (und umgekehrt), sie verarbeiten Daten und rufen Funktionen anderer Klassen auf.

Eine Control-Klasse die mehr tut als steuern ist eine schlechte Control-Klasse. Control-Klassen beauftragen andere Klassen ihnen Infos zu liefern oder Infos zu speichern. Sie holen/speichern niemals selbst die Daten!

Diese Eigenschaften bringen es mit sich, dass Control-Klassen im Gegensatz zu den anderen beiden Klassentypen Anwendungsabhängig sind, und schlecht oder gar nicht wieder verwendet werden können.

(Das ist auch der Grund warum viele Programmierer selten Code wieder verwenden. Sie vermischen Control- und Entity-Klassen, und teilweise auch alle 3 Typen. Dadurch geht die Wiederverwendbarkeit garantiert verloren.)


Ein Beispiel:

  1. Durch einen Klick auf den OK Button einer Eingabemaske (Boundary-Klasse) startet der Controller seine Abarbeitung.
  2. Er lässt sich nacheinander die Daten von der Maske schicken.
  3. Er speichert die Werte lokal zwischen.
  4. Wenn alle Daten entgegengenommen wurden, und vom Controller ins richtige Format gebracht wurden (z.B. Strings -> Integer) ruft er die entsprechenden "Set"-Funktionen des Datenspeichers (Entity-Klasse) auf. Wie die Daten gespeichert werden ist nun Sache der Entity- und nicht der Control-Klasse.
  5. Je nachdem ob der Vorgang erfolgreich war, sendet er eine Bestätigung an die Eingabemaske welche diese dann anzeigt.

Typkonvertierungsaufgaben sollten immer von Control-Klassen erledigt werden, da hier leicht Fehler auftreten können. Control-Klassen sind dazu gemacht worden, um auf Ereignisse wie diese zu reagieren. Man sollte die GUI nicht mit so etwas belasten. Datenspeicher (Entity-Klassen) sollte zumindest bei eingehenden Daten von Konvertierungsarbeit verschont bleiben. "Get"-Funktionen welche Daten in anderen Formaten ausgeben können (wenn sie von verschiedenen Control-Klassen immer wieder benötigt werden) Entity-Klassen zugemutet werden. Wenn nur eine Control-Klasse die Konvertierung benötigt sollte sie es selbst machen.


Finden von Control-Klassen

Aus jedem Akteur-Use-Case-Paar wird anfänglich eine Control-Klasse.

Wann immer ihr Funktionen findet, welche nicht eindeutig zu einer Entity-Klasse zugeordnet werden können, kann für diese Funktionen eine Control-Klasse erstellt werden.

Im späteren Verlauf können Control-Klassen zusammen gefasst, geteilt oder gelöscht werden.

Die Control-Klassen, welche einen Use-Case steuern sollten aber von Datenverarbeitungsaufgaben befreit bleiben. Für diese Zwecke sollte man ihnen eine Control-Klasse zur Seite stellen, welche von der ersteren bei Bedarf gerufen wird.


Denkt daran, dass die neu gefundenen Klassen sofort dokumentiert werden, d.h. ihre Aufgaben die sie haben, bzw. was sie darstellen/speichern kurz aufzuschreiben. Es kommt leider in dieser Phase recht oft vor, dass man den Namen eines Klassenkandidaten sieht, und sich dann fragt, wozu der eigentlich gut ist (weil man mittlerweile bereits neue Klassen gefunden hat, die doch eigentlich das selbe machen). Wenn man dann keine Dokumentation gemacht hat, muss man noch mal prüfen wo die Klasse her kam, und was sie machen soll. Das kostet Zeit, und die ist bekanntlich Geld wert.

Pakete

links das UML Symbol für Pakete, rechts eine Klasse die einem Paket zugeordnet wurde

Ein Konzept was in Delphi durch die Units umgesetzt wird. (Im Gegensatz zu Sprachen wie Java enthält ein Delphi-Code-File (Unit) durchaus mehrere Klassen.) In C/C++ ("Namespaces") und in Java sind Pakete ebenfalls vorhanden.

Solange man relativ wenige Klassen zur Verfügung hat, ist die Verwaltung kein Problem. Sobald die Anzahl aber größer wird kann man sich leicht im Wirrwarr verlieren.

Das Paket-Prinzip soll folgendes erreichen:

  • leichtere Benutzbarkeit durch schnelleres wieder finden.
  • leichtere Wartbarkeit
  • leichtere Wiederverwendbarkeit

Durch die Einteilung der gefundenen Klassen in Pakete erreicht man eine bessere Übersicht über das System. Man hat sozusagen einen Draufsicht auf die logische Gliederung des Systems.

Die Einteilung in Pakete zeigt auch die Repräsentation von Architekturentscheidungen, da man leicht sieht, was zusammen gehören soll, und was nicht.

Jedes Paket besitzt ein Interface. Das Interface, sind all die Klassen welche von anderen Klassen, anderer Pakete benutzt werden können. Neben diesen öffentlichen Klassen besitzt jedes Paket noch private Klasse, welche Funktionalität innerhalb des Pakets bereitstellen.


Bei sehr komplexen Softwaresystem ist es ratsam bereits frühzeitig mit der Einteilung in Paketen zu beginnen. Bei kleineren Systemen liegen meist (zumindest am Anfang) alle Klassen in einem Paket.

Ein Paket was fast immer vorhanden ist, ist das User-Interface-Paket. Alle GUI Elemente können hier gefunden werden. Ansonsten sollte man versuchen die Klassen nach Hauptthemen zu sortieren (in einem Strategiespiel z.B.: KI, Einheiten, Gebäude, Ressourcen, Spieldaten).


Interaktionen finden

All unsere gefundenen Klassen bringen nichts, wenn sie nicht miteinander in Verbindung stehen. 'Interaktionen' bedeutet Nachrichtenaustausch. In der OOP sagt man "Klasse A schickt Klasse B eine Nachricht" genau dann, wenn Klasse A eine Funktion der Klasse B aufruft. Klingt komisch - is aber so. ;)

Im Moment sind unsere Klassen noch ziemlich leer. Weder Eigenschaften, noch Methoden haben wir gefunden. Um letztere kümmern wir uns jetzt.


Szenarios

UML Symbol für Szenarios ("Use-Case Realisierungen")

Ein probates Mittel um Interaktionen zu finden, ist es Szenarios (werden auch "Use-Case Realisierungen" genannt) zu bauen. Ein Szenario ist ein möglicher Pfad durch den Ereignisfluss eines Use-Cases.

Szenarios werden benutzt um die Klassen, Objekte und Objektinteraktionen zu bestimmen, welche benötigt werden um einen Teil der Funktionalität zu realisieren, die der Use-Case vorgibt.

Jeder Use-Case besteht aus einem Netz von Szenarios. Diese werden wiederum gegliedert in Primär- und Sekundärszenarios. Die Primärszenarios stellen den Hauptfluss durch den Use-Case dar, also die Ereignisabfolge die man eigentlich erreichen will.

Die Sekundärszenarios sind Flüsse welche Sonderfälle, Fehler und Abbrüche behandeln.


In der frühen Analysephase ist es ausreichend sich auf die Primärszenarios zu konzentrieren. Die Szenario-Analyse ist abgeschlossen, wenn ca. 80% aller Systemfunktionen über Primärszenarios dargestellt werden können, und eine repräsentative Menge von Sekundärszenarios gefunden wurden. (Das klingt jetzt recht schwammig aber wenn man diesen Punkt erreicht hat merkt man, dass kaum neue Klassen gefunden werden, bzw. dass sich die neuen Szenarios kaum noch von vorhandenen Szenarios unterscheiden.)

In "Rational Rose 2000" werden Szenarios im logischen View (Auf die Views wird im 3. Teil der Tutorailreihe eingegangen) als Use-Case Diagramm gespeichert. Szenarios sollte möglichst den selben Namen wie der Use-Case haben den sie realisieren. Als Symbol wird ein Oval mit gestricheltem Rand verwendet. Um die Verbindung zwischen Use-Case und Szenario zu kennzeichnen, kann man den betreffenden Use-Case mit ins Diagramm einzeichnen und markieren.

Diese "Szenario-Use-Case-Diagramme" zeigen wie ihr sicherlich bemerkt noch keine Interaktionen. Sie dienen der Orientierung, welche Szenarios wie miteinander "verwandt" sind.

Szenarios dokumentieren

Ihr wisst ja bereits, dass man alle Objekte die man irgendwann einmal in irgendein Diagramm zeichnet auch dokumentiert. Das besondere an der Dokumentation von Szenarios ist, dass dies nicht in schriftlicher Form (wie beim Ereignisfluss eines Use-Cases) passiert sondern in Form eines Interaktionsdiagramms. Es gibt 2 verschiedene Typen, Sequenzdiagramme und Kollaborationsdiagramme, die gleich noch genauer beschrieben werden.

Man sollte sich für eines der beiden entscheiden. Welches Diagramm man verwendet hängt vom Szenario ab. Beide Diagramme anzulegen ist nur dann sinnvoll, wenn man daraus wirklich neue Erkenntnisse gewinnt.

Die beiden Diagrammtypen unterschieden sich nicht im Inhalt, wohl aber in der Darstellung. Sequenzdiagramme zeigen zeitliche Abläufe und können vom Kunden gut verstanden werden. Deshalb werden sie auch verstärkt in der frühen Analysephase benutzt. Kollaborationsdiagramme zeigen hingegen das "Große Ganze". Sie sind eher für die Designphase nützlich wenn es darum geht Beziehungen zu implementieren.

Bevor wir die beiden Diagramme etwas genauer ansehen will ich noch einen Begriff erklären, welchen man häufig in diesem Zusammenhang ließt.

Verantwortlichkeit

Ein wichtiger Begriff beim bauen von Szenarios ist "Verantwortlichkeit" (Responsebility). Wenn man eine Methode einer Klasse aufruft (ihr eine Nachricht schickt) übergibt man dieser Klasse zeitweilig die Verantwortung für die Abarbeitung. Die aufgerufene Klasse ist dann für alle die Schritte verantwortlich, die sie machen muss um die gewünschte Aufgabe zu erledigen. Wenn wir zurück denken fällt uns ein, dass nur Control-Klassen komplexe Funktionen besitzen. Wenn man also beim bauen von Szenarios einer vermutlichen Entity-Klasse die Verantwortung für eine komplexe Aufgabe gibt (also etwas anderes als "liefere die XYZ Daten"), dann hat man wahrscheinlich einen konzeptionellen Fehler gemacht.


Sequenzdiagramme

Wie bereits erwähnt zeigt dieser Diagrammtyp Objektinteraktionen in Zeitabschnitten. Dargestellt werden dabei die Objekte zw. Klassen sowie die Nachrichten, die zwischen diese ausgetauscht werden, um die Funktionalität des Use-Case zu erreichen.

Sequenzdiagramme gehören jeweils zu einem Szenario und werden dementsprechend auch im logischen View des Projekts gespeichert. (Manche UML-Projektverwaltungstools wie z.B. Rational Rose 2000 haben die Möglichkeit diese Diagramme an die Szenarios "anzuhängen", was hier durchaus angebracht ist.)

Tutorial SWE2 SeqDiagObjekte.jpg

Rechts sieht man die 3 verschiedenen Darstellungsformen von Szenarios in Sequenzdiagrammen. Objektnamen können spezifisch ("HerrMeier") oder allgemein ("EinePerson") sein. Anonyme Objekte (solche wo nur der Klassenname steht) werden benutzt um alle Objekte einer Klasse darzustellen.

Zu jedem Objekt gehört außerdem noch seine Lebenslinie (Timeline). Nachrichten zwischen Objekten werden als Pfeil vom Sender zum Empfänger symbolisiert. Die Diagramme die dadurch entstehen, haben folgende Form:

Tutorial SWE2 SeqDiagBeispiel.jpg


Sequenzdiagramme und Boundary-Klassen

Boundary-Klassen werden in Sequenzdiagrammen benutzt um Interaktionen mit Akteuren darzustellen. In der frühen Analysephase versucht man mittels Boundary-Klassen heraus zu finden was das Interface der SW können muss. Es ist ausdrücklich nicht interessant wie das Interface funktioniert. Im späteren Verlauf kann sich das natürlich ändern. Dann werden die Diagramme entsprechend den Anforderungen des Interface angepasst.

Ein Beispiel für Sequenzdiagramme mit Boundary-Klassen könnte so aussehen:

Tutorial SWE2 SeqDiagBoundary.jpg


Komplexität in Sequenzdiagrammen

Das schöne an Sequenzdiagrammen ist ihre Einfachheit. Man kann leicht Objekte, Klassen und Nachrichten erkennen. Allerdings stellt sich die Frage was mit Bedingungen (If Then Else) passieren soll. Wenn es sich um wenige und einfache Bedingungen handelt, können diese sicherlich im Diagramm verzeichnet werden. Bei komplexer Logik sollte man dafür ein extra Diagramm anlegen (und ,falls der Editor es erlaubt, diese miteinander verknüpfen). Wenn die Bedingungen in einen komplett anderen Fluss führen kann durchaus auch ein neues Szenario dafür angefertigt werden. Auf jeden Fall ist das aufteilen gerechtfertigt, denn dadurch bleiben die einzelnen Diagramme übersichtlich und damit leichter lesbar.


Kollaborationsdiagramme

Kollaborationsdiagramme sind ebenfalls recht einfach zu verstehen, aber nicht so schön linear lesbar. Objektinteraktion wird über Verbindungen zwischen Objekten symbolisiert. Dazu besitzt dieser Diagrammtyp die folgenden 3 Symbole:

Tutorial SWE2 KollDiagObjekte.jpg

Ein kleines Beispiel sollte als Erklärung genügen:

Tutorial SWE2 KollDiagBsp.jpg


Beziehungen zwischen Klassen

Die Interaktionen haben wir gefunden. Um interagieren zu können, müssen die Klassen eine Beziehung zu einander haben. Man unterscheidet 2 verschiedene Typen.


Assoziation

Tutorial SWE2 Assoziation.jpg

Assoziationen sind bidirektionale Verbindungen zwischen Klassen. Sie Kennzeichnen keinen speziellen Datenfluss, d.h. Daten können beliebig über die Assoziation "hindurch" fließen.

Eine Assoziation zwischen Klassen bedeutet, dass eine Verbindung zwischen Instanzen der Klasse besteht.

Assoziationen werden in UML als einfache Linie zwischen Klassen dargestellt.


Aggregation

Tutorial SWE2 Aggregation.jpg

Die Aggregation ist eine Spezialisierung der Assoziation. Sie zeigt an, dass Objekte Teil von anderen Objekten sind. D.H. ein Objekt beinhaltet ein anderes.

Für Aggregationen wird auch der Begriff "Teil-Ganzes-Beziehung" (engl.: part whole relationship) benutzt.

Die UML-Notation für Aggregationen ist eine Linie zwischen 2 Klassen, mit einem Karo (Diamant) bei der Klasse welche "das Ganze" darstellt (also die Klasse welche die andere als Teil besitzt).


Reflexive Beziehungen

Tutorial SWE2 Reflexiv.jpg

Es kann natürlich vorkommen, dass Objekte einer Klasse mit anderen Objekten der selben Klasse interagieren müssen. Um dies darzustellen, benutzt man reflexive Beziehungen. Das ist auch gar nichts kompliziertes, sondern wird einfach als Aggregation bzw. Assoziation dargestellt welche bei der Klasse endet, bei der Sie auch begann.


Beziehungen beschriften

Eine Linie zwischen zweit Klassen ist bekanntlich wenig aussagekräftig. Man könnte jetzt, wie bei Klassen, auch die Beziehungen dokumentieren. Allerdings würde das bedeuten, dass man immer zwischen dem Diagramm und der Dokumentation blättern müsste, um die Beziehungen die dargestellt werden zu verstehen. Deshalb werden Beziehungen direkt im Diagramm beschriftet.

Der "klassische" Weg ist ein Verb oder einen Ausdruck an die Beziehung zu schreiben, welche die Handlung/Wirkung der Beziehung möglichst gut beschreibt. Um so eine Beschriftung korrekt zu lesen ist eine Leserichtung nötig, die im Ganzen Diagramm, besser noch in der gesamten Doku gleich ist. Es hat sich durchgesetzt die Beschriftung so zu wählen, dass die Leserichtung Links->Rechts bzw. Oben->Unten den korrekten Sinn widerspiegelt.

Tutorial SWE2 LeserichtungBsp.jpg

Klar wird dies an folgendem Beispiel:

Man kann davon ausgehen, dass der Gärtner (Links) die Blumen (Rechts) pflanzt (Verb) und nicht umgekehrt. Sollte bei euch in der Gegend der Vorgang umgekehrt ablaufen schreibt ihr mir bitte eine Mail. Ich korrigiere das dann in meinem Tutorial. ;)


Die zweite Möglichkeit zur Beschriftung ist Rollen zu verwenden. Damit sind nicht die drehbar gelagerten zylindrischen Dinger gemeint die sich an Bürostühlen befinden sondern Rollen wie sie im Theater, Film und Fernsehen zu finden sind.

Tutorial SWE2 Rollen.jpg

Wie man im Bild rechts sehen kann, sind Rollen fast das selbe wie die klassische Beschriftung. Doch es gibt Unterschiede:

Erster Unterschied ist, wo die Beschriftung steht. Sie wird nämlich nicht mehr "irgendwo" angebracht, sondern an dem Ende der Beziehung, welche die Rolle ausfüllt.

Außerdem werden nicht mehr Verben sondern Substantive verwendet die eine Art "Berufsbezeichnung" darstellen.


Es gibt keine Regeln, wann man welche Beschriftungsmethode benutzt. Mittlerweile tendiert man mehr zu Rollen, da diese auch bei bidirektionalen Beziehungen noch klar machen, was die Beziehung bedeutet. Auch bei den Reflexiven Beziehungen werden Rollen der klassischen Beschriftung vorgezogen.


Es gibt keinen Zwang jede Beziehung zu beschriften. Man sollte sogar nur dann etwas beschriften, wenn es nötig ist. Wenn schon die Klassennamen selbst die Beziehung erklären ist eine Beschriftung überflüssig. Aggregationen werden im allgemeinen nicht beschriftet, da die Beschriftung "gehört zu" bzw. "Ist Teil von" auch so aus der Verbindung ablesbar ist.


Multiplizität

Multipli- Was?

Multiplizität ist ein seltsames Wort, aber nicht komplizierter als der Rest. Bekanntlich können Objekte mit mehreren Instanzen anderer Klassen zu tun haben. Man denke nur an einen Texturmanager. Dieser wird nicht nur eine Textur sondern potentiell beliebig viele verwalten. Um dies in Diagrammen sichtbar zu machen gibt es die Multiplizitäts-Indikatoren. Das sind kleine Zahlen, welche an den Enden der Beziehungen stehen und sich jeweils auf die Klassen beziehen, welche an dem Ende der Beziehung "hängt".

Die gebräuchlichsten Indikatoren sind folgende:

Indikator Bedeutung
1 exakt einer
0..* Null oder mehr ("Kann es geben oder nicht.")
1..* Mindestens einer
0..1 Einer oder Keiner ("Wenn dann nur einer.")
5..8 präzise Vorgabe (entspricht hier 5,6,7,8)
4..7,9 Kombination (entspricht hier 4,5,6,7,9)
Info DGL.png Manche UML Editoren zeigen den Indikator "1" nicht an, da keine Beschriftung automatisch eine Multiplizität von 1 bedeutet.

Wem das jetzt noch nicht klar ist, der brauch sich nur einmal folgendes Beispiel ansehen:

Tutorial SWE2 Miltiplizität.jpg

Folgendes ist dargestellt:

  • Eine Vorlesung wird von exakt einem Professor gehalten welcher die Rolle des Dozenten übernimmt.
  • Ein Professor spielt die Rolle des Dozenten für bis zu 4 Vorlesungen.


Klassen strukturieren

Wir wissen mittlerweile welche Klassen es geben soll (unsere Klassenkandidaten) und wie diese miteinander in Beziehung stehen bzw. wie sie miteinander interagieren. Weiterhin wissen wir, in welche Pakete wir die Klassen gliedern wollen.

Es ist jetzt an der Zeit dieses Wissen in Methoden und Attribute umzusetzen. Dabei gibt es wieder einige Dinge zu beachten.

Das einfachste zuerst: Die Benennung von Attributen und Methoden sollte einer Stilvorgabe folgen. Das bedeutet, dass man sich selbst (oder innerhalb des Teams) vorgaben macht, wie die Bezeichnungen aussehen sollen. Eine einfache Stilvorgabe könnte lauten:

- alle Attribute beginnen mit kleinen Buchstaben. Attributnamen bestehen nach Möglichkeit aus einem Wort. Bei zusammengesetzten Wörtern wird wie bei Methoden verfahren. (Beispiel: adresse) - Methodenbezeichner beginnen mit kleinen Buchstaben und besitzen keine Sonderzeichen. Jedes Teilwort beginnt mit einem Grossbuchstaben. (Beispiel: dasIstEineMethode() ) - Getter-Methoden für Attribute setzen sich aus "get" und dem Attributnamen zusammen. (Beispiel: getAdresse() ) - Setter-Methoden für Attribute setzen sich aus "set" und dem Attributnamen zusammen. (Beispiel: setAdresse() )

Es ist für den Umgang mit den entstehenden Klassen wichtig, dass sich alle beteiligten Entwickler an die vereinbarten Stilvorgaben halten. Dies gestaltet das spätere Implementieren der Software und vor allem die Arbeit mit fremden Code um einiges angenehmer.


Methoden finden

Methoden sind Unterprogramme welche die Daten (Attribute) der Klasse zu der sie gehören kennen, und somit bearbeiten können. Die Methoden einer Klasse sind für das Verhalten der Klasse zuständig.

Um viele Methoden zu finden reicht es, einen Blick auf die zuvor erstellten Interaktionsdiagramme zu werfen. Die Nachrichten der Interaktionsdiagramme werden zu Methoden der Klasse, welche die Nachricht erhalten soll.

Natürlich trifft dies auf Boundary-Klassen und Akteure nicht zu. Das man Akteuren keine Methoden zuweisen kann sollte klar sein. Wenn Boundary-Klassen Nachrichten von Akteuren entgegen nehmen sollen, so geschieht dies über eine GUI (Graphische Nutzer Oberfläche) und nicht über Methoden.

Der Name für die gefundenen Methoden sollte immer aus der Sicht der ausführenden Klasse und nicht aus Sicht der aufrufenden Klasse gewählt werden. Die Sprache (deutsch, englisch, suaheli, etc.) ist Teil der Stilvorgaben. Der Name sollte ebenso frei von Hinweisen auf die Implementation der Methode sein. Z.B. berechneXYZ() : Vielleicht ist es im Moment so, dass XYZ berechnet wird, aber eventuell wird später die Funktion durch eine ersetzt, welche die Daten aus einer Datenbank ausließt. Dann ist der Name unzutreffend. Besser wäre hier getXYZ().

Neben den Methoden mit zentraler Bedeutung, die aus Interaktionsdiagrammen ablesbar sind, gibt es natürlich auch noch solche die dort nicht verzeichnet sind. Methoden die sehr umfangreiche Aufgaben zu erledigen haben, sollten die Teilaufgaben welche ständig wiederkehren in Hilfsfunktionen auslagern. Diese Funktionen werden meist erst während der Implementation entdeckt. Es ist dann wichtig diese auch im Modell zu hinterlegen und zu dokumentieren, da sonst das Modell nicht mehr das Programm beschreibt (was ja das zentrale Anliegen des ganzen Modellierungsprozesses ist).


Neue Beziehungen

Falls ihr eine Methode findet die als Parameter eine andere Klasse erwartet, dann ist dies ein sicheres Zeichen dafür, dass es eine Beziehung zwischen den beiden Klassen besteht. Ihr solltet euer Modell daraufhin untersuchen, ob diese Beziehungen ebenfalls eingetragen sind, und wenn nicht dies nachhohlen. Auch die Beziehungen der übergeordneten Pakete sollte darauf überprüft werden.

Wenn man im späteren Verlauf der Entwicklung (zum Beispiel, wenn die nachfolge Version implementiert werden soll) Klassen austauschen will, ist es sehr ärgerlich, wenn plötzlich undokumentierte Beziehungen auftauchen, die das austauschen von Klassen erschweren.


Methoden dokumentieren

Methoden zu dokumentieren ist wohl die wichtigste Form der Dokumentation überhaupt. Die Dokumentation sollte so sein, dass der Leser problemlos die Aufgabe der Funktion verstehen kann.

Die Ein- und Ausgabevariablen gehören ebenso in die Dokumentation. Es ist wahrscheinlich, dass sich diese im Laufe der Implementation ändern. In diesem Falle muss die Dokumentation angepasst werden (und zwar sofort! Solche Änderungen werden gern vergessen -> inkonsistente Doku).

Attribute finden

Viele Klassen-Eigenschaften können in der Problembeschreibung, der Anforderungsbeschreibung und aus dem Ereignisfluss abgelesen werden. Einige werden sicherlich auch beim schreiben der Klassen (bei der "Definition") auftauchen. Meist ist auch Wissen bezüglich des Einsatzgebietes hilfreich.

Attribute dokumentieren

Egal woher und egal wann ihr ein neues Attribut zu einer Klasse hinzufügt, in jedem Fall sollte es dokumentiert werden. Wie üblich geht es bei der Dokumentation primär um das was, und erst in zweiter Instanz um das wie. Das heißt: Zuerst sollte kurz und knapp beschrieben werden warum es das Attribut gibt, und was es bedeutet. Danach kann man beschreiben was die einzelnen Teile bedeuten (z.B. bei einem Werte-Array kann jede Spalte/Zeile eine spezielle Bedeutung haben). Was definitiv nicht in die Dokumentation gehört ist eine Aussage wie "Ein String der Länge x" denn das sieht der Programmierer auch am Code.

Attribute und Methoden in Diagrammen

Häufig werden Klassendiagramme benutzt um nur Attribute und Methoden von Klassen eines Paketes zu visualisieren. Diese Diagramme enthalten meist keine Beziehungen. Solltet ihr also einmal in der Dokumentation eines bestehenden Projektes Einsicht nehmen, und so etwas vorfinden, dann ist dies kein Fehler sondern nur eine weitere spezielle Ansicht auf das Paket.

Assoziations-Klassen

Was soll denn das sein? Assoziationen sind doch Beziehungen! - Exakt!

Aber hin und wieder kann eine Beziehung ganz schon kompliziert werden - auch wenn wir es hier nur mit Software zu tun haben. ;)

Wenn man auf Informationen/Daten stößt, die nicht zu einer Klasse zugeordnet werden können, sondern vielmehr zu der Beziehung zwischen den Klassen gehören, dann ist das ein Zeichen dafür, Assoziations-Klassen zu benutzen. Die "gewachsenen OOPler", also die, die OOP gelernt haben nachdem sie prozedurales Denken verinnerlicht haben, werden in solchen Fällen sicherlich den Wert der einen, oder der anderen Klasse zuordnen. "Gelernte OOPler" nehmen dazu lieber eine Klasse, und verbinden Sie mit den beteiligten Klassen. Assoziations-Klassen sind nämlich nichts magisches, sondern auch nur normale Klassen. Allerdings repräsentieren sie keine Entität, sondern eine Beziehung zwischen Entitäten.

Ein Beispiel zur Verdeutlichung (Aus dem zugrunde liegendem Buch):

Tutorial SWE2 AssoziationsKlasse.jpg

Angenommen man hat eine Klasse "Student" und eine Klasse "Vorlesung". Am ende eines Semesters erhält jeder Student eine Note pro Vorlesung. Wo wird diese gespeichert? Ein Student kann 1 bis 4 Vorlesungen besuchen. Eine Vorlesung kann maximal 200 Studenten aufnehmen (aber brauch mindestens 4 um statt zu finden).

Die Lösung ist eine Assoziations-Klasse welche die Note enthält, und mit dem entsprechenden Studenten und der dazugehörigen Vorlesung verbunden ist. Am Ende des Semesters kann dann jeder Student ein Zeugnis in Form der zusammengeketteten Assoziations-Klassen bekommen.

Ein Wort an die "gewachsenen OOPler": Der ein oder andere hätte für diesen Fall vielleicht eine Hash-Table oder ähnliches gebaut, bevor er mit puren Referenzen auf Klassen arbeitet. "Gelernte OOPler" haben damit allerdings keine Berührungsängste. Für sie sind Klassenreferenzen genauso elementare Datentypen wie Integer oder Chars. Vor allem in Java, da es dort ja keine Pointer gibt. Wer von z.B. Delphi kommt hat ja mitunter Angst seine Daten zu verlieren und nur noch einen toten Zeiger in der Hand zu haben, wenn irgendwas mit der Klasse schief läuft. Das muss man einfach ausblenden und Klassenreferenzen als einfachen Datentyp ansehen. Auch ich habe damit hin und wieder Probleme, aber Klassenreferenzen sind im Endeffekt in ihren Verwendungsmöglichkeiten weniger begrenzt wie die angesprochene Hash-Table.


Ausblick

Puh... Das war ja ganz schön heftig, oder? Ziemlich viel Stoff für ein Tutorial, aber ich wollte das Thema Klassen nicht völlig zerreißen. Ihr solltet jetzt wissen wie das 3-Schichten-Modell (Border-, Control- und Entity- Klassen) funktioniert, und wieso es sinnvoll ist dieses Modell in seinen Programmen umzusetzen. Außerdem hoffe ich, dass einige der Lösungsvorschläge euch weiterhelfen wenn ihr mal wieder vor einer Designentscheidung bezüglich eurer Klassenhierarchie steht.

Im nächsten Tutorial geht es dann um die

  • Vererbungshierarchien
  • Zustände von Klassen
  • Homogenisierung von Klassen
  • System Architektur
  • Planung der Iterativen Implementation

Ich hoffe ihr seid auch dann wieder mit dabei. Das nächste Tutorial fällt auf jeden Fall kürzer aus. ;)


Euer

Kevin Fleischer aka Flash




Vorhergehendes Tutorial:
Tutorial Softwareentwicklung1
Nächstes Tutorial:
Tutorial Softwareentwicklung3

Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com.
Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen.