lernen:baustein
no way to compare when less than two revisions
Unterschiede
Hier werden die Unterschiede zwischen zwei Versionen angezeigt.
— | lernen:baustein [2020-07-27 09:32] (aktuell) – angelegt - Externe Bearbeitung 127.0.0.1 | ||
---|---|---|---|
Zeile 1: | Zeile 1: | ||
+ | ====== Bausteine, Aggregate, Assoziationen ====== | ||
+ | > We eat the night, we drink the time | ||
+ | > Make our dreams come true | ||
+ | > And hungry eyes are passing by | ||
+ | > On streets we call the zoo | ||
+ | >> | ||
+ | |||
+ | ===== Zielstellung ===== | ||
+ | |||
+ | Objektbeziehungen des Systementwurfes werden Implementierungstechniken in C++ | ||
+ | zugeordnet und katalogisiert. | ||
+ | |||
+ | ===== Auf der Baustelle zwischen UML-Entwurf und Umsetzung in C++ ===== | ||
+ | |||
+ | Ein objektorientierter Entwurf ist irgendwann auch zu implementieren. | ||
+ | Die Umsetzung in die Programmiersprache ist aber keinesfalls ein mechanischer Prozess. Man kann dies mit einem Case-Tool wie Janus, Objektif oder Rational Rose tun. Case Tools sind dazu nur in der Lage, weil sie dem Bediener mehr abverlangen als nur einen UML-Entwurf. Der schöpferische Teil der Implementationsphase wird teils unmerklich in den Entwurf verlagert. Oder man kann Programmierer direkt mit der Implementierung beauftragen. Diese stehen dann vor der Frage: Wie setze ich diesen Entwurf um? Viele Aspekte sind zu bedenken. Dieser Text konzentriert sich auf Klassendiagramme, | ||
+ | |||
+ | ===== Beziehungen als Entwurf ===== | ||
+ | |||
+ | Die grafische Sprache UML (Unified Modeling Language) stellt Ausdrucksmöglichkeiten für den objektorientierten Entwurf bereit. Das Ergebnis des Entwurfsprozesses ist eine Sammlung von " | ||
+ | |||
+ | {{lernen: | ||
+ | |||
+ | Abb. 1. Assoziation, | ||
+ | |||
+ | Dabei werden drei Gruppen von Beziehungen unterschieden: | ||
+ | |||
+ | * Assoziationen: | ||
+ | * Aggregationen: | ||
+ | * Kompositionen: | ||
+ | |||
+ | Existenzabhängig ist ein Teil dann, wenn es mit dem Ungültigwerden des Ganzen seine Existenz(berechtigung) ebenfalls verliert. Grady Booch [Booch] unterscheidet Beziehungen durch textuelle Beschreibungen wie "A nutzt ein B", "A hat ein B", um Assoziationen (Nutzen) von Teil-Ganzes-Beziehungen (Haben, Enthaltensein) zu unterscheiden. Auf der Seite des Ganzen endet die Linie in einem Rhombus, der bei Existenzabhängigkeit gefüllt ist. | ||
+ | |||
+ | Zusätzlich können Angaben über die Anzahl miteinander in Beziehung stehender Objekte verzeichnet werden (Kardinalitäten): | ||
+ | |||
+ | * 1: | ||
+ | * 1: | ||
+ | * 1: | ||
+ | * 1: | ||
+ | |||
+ | Ein Nachteil der grafischen Notation UML ist, dass sie in elektronischer Form ein Datenformat benutzen muss, für das es bisher keinen Standard gibt, entweder als proprietäre Binärdatei eines Case-Tools, oder schwer modifizierbar als Vektor- oder Bitmap-Grafik. Mit Quelltexten lässt sich diese Darstellung schlecht verbinden. Für einfache Sachverhalte lässt sich vielleicht eine abgerüstete Variante finden, die auch in Quelltexten als Textgrafik darstellbar ist: | ||
+ | |||
+ | | ||
+ | [A]------[B] | ||
+ | |||
+ | | ||
+ | [A]<> | ||
+ | |||
+ | | ||
+ | [A]< | ||
+ | |||
+ | Selbstverständlich kann das keine ganze technische Zeichnung ersetzen. Feinheiten sind damit nicht darstellbar. Im Quelltext werden aber die Sachverhalte nacheinander modelliert, so dass kleine Ausschnitte aus dem UML-Diagramm zur Kommentierung ausreichen. " | ||
+ | |||
+ | 1 | ||
+ | | ||
+ | 1 | | 4 | ||
+ | [Brett]--------[Position]--------------------------< | ||
+ | < | ||
+ | | ||
+ | |||
+ | Abb. 2. Klassendiagramm mit Klassenbeziehungen eines Mensch-Ärger-Dich-nicht-Spiels. Eine solche Darstellung als Textgrafik ist im Quelltext zur Dokumentation integrierbar. | ||
+ | |||
+ | ===== Klassendiagramme sind nicht alles ===== | ||
+ | |||
+ | Die aus einer solchen Konstellation von Objekten entstehenden Verwicklungen sollten im Entwurf festgehalten sein. Es braucht Klassenschnittstellen, | ||
+ | |||
+ | Das Klassendiagramm ist nur eine notwendige Sicht auf das zu entwerfende System. Diese Sicht ist statisch. Sie stellt die Beziehungen dar, die irgendwann im Programmverlauf von Bedeutung sind. Für den richtigen Aufbau der Beziehungen müssen eine Reihe von Entscheidungen getroffen werden (Abb. 3). | ||
+ | |||
+ | Objekte begegnen sich nur in Methoden / Funktionen? | ||
+ | / | ||
+ | ja: Assoziation: | ||
+ | | | | ||
+ | Muss jede der Klassen mit Objekten | ||
+ | der anderen Klasse umgehen? | ||
+ | / | ||
+ | | ||
+ | gerichtete | ||
+ | Assoziation | ||
+ | | ||
+ | | ||
+ | | ||
+ | | ||
+ | / | ||
+ | ja | ||
+ | |||
+ | Abb. 3. Design-Entscheidungen. | ||
+ | |||
+ | Für einige Problemstellungen haben sich Standard-Lösungen herausgeschält, | ||
+ | |||
+ | * Erzeugungsmuster (Fabrik, Singleton) | ||
+ | * Strukturmuster (Adapter, Brücke, Proxy, Kompositum - Vorsicht: nicht mit dem Begriff Komposition verwechseln!), | ||
+ | * Verhaltensmuster (Beobachter, | ||
+ | |||
+ | Dieser Musterkatalog ist nicht vollständig, | ||
+ | |||
+ | Nicht alle Entscheidungen des Entwurfs sind im Klassendiagramm dargestellt. So sind Beziehungen im UML-Diagramm gewöhnlich bidirektional, | ||
+ | |||
+ | ===== Ausdrucksmittel der Implementierungs-Sprache ===== | ||
+ | |||
+ | Klassen sind die Grundbausteine des objektorientierten Programmierens: | ||
+ | |||
+ | <code cpp> | ||
+ | class Klassenname : public Basisklasse | ||
+ | { | ||
+ | public: | ||
+ | Ergebnistyp methode(Parametertyp parameter); | ||
+ | protected: | ||
+ | // bzw. | ||
+ | private: | ||
+ | Datentyp attribut; | ||
+ | }; | ||
+ | </ | ||
+ | |||
+ | Die in den Klassendiagrammen dargestellten Beziehungen zwischen den Objekten entscheiden darüber, wie Daten-Attribute und Methoden-Parameter zu deklarieren sind. Andere Gesichtspunkte werden im folgenden ausgeblendet. | ||
+ | |||
+ | Objekte werden über Bezeichner angesprochen. In C++ kann das eine Variable, ein Feldelement, | ||
+ | |||
+ | <code cpp> | ||
+ | int nummer = 1; // Objekt mit Speicherplatz, | ||
+ | int* zeiger = & | ||
+ | int& referenz = nummer; | ||
+ | std:: | ||
+ | std:: | ||
+ | </ | ||
+ | |||
+ | Für das Umsetzen des Entwurfs zählt, welche Möglichkeiten die Implementierungssprache anbietet. Dazu lohnt ein Blick über den Gartenzaun. | ||
+ | |||
+ | ===== C++ != Java ===== | ||
+ | |||
+ | Java baut sein Objektmodell auf der automatischen Speicherbereinigung (garbage collection) auf. Bis auf Grunddatentypen werden alle Objekte über Referenzen verwaltet. Um Missverständnissen vorbeugen, muss der Unterschied zwischen einer Referenz in Java und einer Referenz in C++ deutlich gemacht werden. | ||
+ | |||
+ | Eine Referenz in Java ist eine lösbare Verbindung. Eine Java-Referenz kann nacheinander auf verschiedene Objekte zeigen. Die Java-Referenz ist implementierbar als versteckter Zeiger mit einer Referenzzähler-Semantik: | ||
+ | |||
+ | Eine Referenz in C++ wird für die Existenzdauer der Referenz an ein Objekt gebunden. Sie kann nicht zu einem anderen Objekt wechseln. Das referenzierte Objekt muss die ganze Zeit über vorhanden sein. So ist die Rückgabe einer Referenz auf ein lokales Objekt ein gemeiner Fehler (dangling reference problem). | ||
+ | |||
+ | Wechselnde Objekt-Beziehungen können in C++ mit Zeigern modelliert werden. Als eine Art " | ||
+ | |||
+ | <code cpp> | ||
+ | class A | ||
+ | { | ||
+ | //... | ||
+ | private: | ||
+ | B* zeiger_auf_objekt; | ||
+ | }; | ||
+ | </ | ||
+ | |||
+ | Aus dem Ausschnitt der Klassenschnittstelle kann nicht geschlossen werden, | ||
+ | |||
+ | * ob es sich um ein Komposit, ein Aggregat oder eine " | ||
+ | * ob das verwiesene Objekt zeitweilig oder immer vorhanden ist, | ||
+ | * ja noch nicht einmal, ob es sich um ein Objekt oder um mehrere (ein Feld) handelt. | ||
+ | |||
+ | Weitere Informationen liegen in der Klasse verstreut, in den Konstruktoren, | ||
+ | |||
+ | Wie Schweizer Taschenmesser haben Zeiger den Nachteil, das man sich verletzen kann, wenn man das Werkzeug falsch einsetzt. Zeiger können auf falsche Adressen verweisen, bei Zeigern in Felder bleiben die Feldgrenzen ungeprüft, die anderswo zu verwaltende Feldgröße muss stets mitgeführt werden. Und für manchen Zweck gibt es Spezial-Werkzeuge, | ||
+ | |||
+ | * Containerklassen (vector< | ||
+ | * " | ||
+ | |||
+ | Den guten Handwerker erkennt man an seinem Werkzeug. Auch der professionelle Programmierer sollte gute Werkzeuge kennen und nutzen. Notfalls muss er sie sogar selbst schaffen. | ||
+ | |||
+ | ===== Referenzzählende Zeiger ===== | ||
+ | |||
+ | Die referenzierten Objekte schwanken zwischen existenzunabhängig und existenzabhängig: | ||
+ | |||
+ | Solche Verweise bergen in sich die Gefahr der zirkulären Referenz: B verweist auf A und A verweist auf B. Das Programm hat jeglichen Verweis auf A und B verloren. Obwohl beide Objekte nicht mehr genutzt werden, kann der Speicher nicht freigegeben werden. Die Verweiszähler der Objekte werden nicht null. Diese höhere Form der Speicherlecks hat ihre Entsprechung in der Systemverklemmung parallel arbeitender Programme. | ||
+ | |||
+ | ===== Der Zoo der Möglichkeiten ===== | ||
+ | |||
+ | Je nach Art, Kardinalität und Lösbarkeit lassen sich unterschiedliche Umsetzungen finden, die nachfolgend angedeutet werden. Der Baustellencharakter wird durch die Unvollständigkeit des {{zoo.pdf|Katalogs}} deutlich. Ein '' | ||
+ | |||
+ | ==== Existenzabhängige Teile eines Ganzen (Komposition) ==== | ||
+ | |||
+ | < | ||
+ | ---------------------------------- | ||
+ | |||
+ | [A]< | ||
+ | | ||
+ | |||
+ | class A class A | ||
+ | { { | ||
+ | B bauteil; | ||
+ | }; | ||
+ | ~A() { delete bauteil; } | ||
+ | B* bauteil; | ||
+ | }; | ||
+ | |||
+ | Komposition 1:1 Komposition 1:0/1 | ||
+ | unlösbare Verbindung | ||
+ | ---------------------------------- | ||
+ | |||
+ | [A]< | ||
+ | | ||
+ | |||
+ | class A class A | ||
+ | { { | ||
+ | B bauteile[anzahl]; | ||
+ | }; | ||
+ | ~A() { delete [] bauteile; } | ||
+ | int anzahl; | ||
+ | B* bauteile; | ||
+ | }; | ||
+ | |||
+ | Komposition 1:n Komposition 1:* | ||
+ | unlösbare Verbindung | ||
+ | ---------------------------------- | ||
+ | |||
+ | [A]< | ||
+ | | ||
+ | |||
+ | class A | ||
+ | { | ||
+ | vector< | ||
+ | }; | ||
+ | |||
+ | Komposition 1:* | ||
+ | lösbare Verbindung | ||
+ | ---------------------------------- | ||
+ | </ | ||
+ | |||
+ | ==== Existenzunabhängige Teile eines Ganzen (Aggregation) ==== | ||
+ | < | ||
+ | ---------------------------------- | ||
+ | |||
+ | [A]<> | ||
+ | | ||
+ | |||
+ | class A class A | ||
+ | { { | ||
+ | A(B& ref) : bestandteil(ref) {} B* bestandteil; | ||
+ | B& bestandteil; | ||
+ | }; | ||
+ | |||
+ | Aggregation 1:1 Aggregation 1:0/1 | ||
+ | unlösbare Verbindung | ||
+ | ---------------------------------- | ||
+ | |||
+ | [A]<> | ||
+ | | ||
+ | |||
+ | class A class A | ||
+ | { { | ||
+ | B* bestandteile[anzahl]; | ||
+ | }; }; | ||
+ | |||
+ | |||
+ | Aggregation 1:n Aggregation 1:0/1 | ||
+ | lösbare Verbindung | ||
+ | ---------------------------------- | ||
+ | |||
+ | [A]<> | ||
+ | | ||
+ | |||
+ | class A | ||
+ | { | ||
+ | vector< | ||
+ | }; | ||
+ | // oder vector< | ||
+ | |||
+ | Aggregation 1:* | ||
+ | lösbare Verbindung | ||
+ | ---------------------------------- | ||
+ | </ | ||
+ | ==== Gleichberechtigte Klassen (Assoziation) ==== | ||
+ | < | ||
+ | ---------------------------------- | ||
+ | : Interface | ||
+ | nutzt> | ||
+ | [A]------> | ||
+ | : | ||
+ | class A class A | ||
+ | { { | ||
+ | void nutzt(B server); | ||
+ | }; { | ||
+ | | ||
+ | void f() } | ||
+ | { }; | ||
+ | A client; | ||
+ | B server; | ||
+ | client.nutzt(server); | ||
+ | } | ||
+ | |||
+ | Assoziation (gerichtet) | ||
+ | ---------------------------------- | ||
+ | </ | ||
+ | |||
+ | ===== Was nicht im Katalog steht ===== | ||
+ | |||
+ | Ein Katalog bildet nur ab, was es so Schönes gibt. Er sagt nichts darüber, wann und wie die abgebildeten Dinge eingesetzt werden. Nicht aufgeführt ist die Vergabe von Zugriffsrechten (private / protected / public), Schreibschutz (const), ebenso nicht, woher die Angaben über die einzelnen Objekte kommen. Es muss sichergestellt sein, dass die Daten initialisiert werden: | ||
+ | |||
+ | <code cpp> | ||
+ | class A | ||
+ | { | ||
+ | public: | ||
+ | A(B& b) : bauteil(b) {} | ||
+ | private: | ||
+ | B& bauteil; | ||
+ | }; | ||
+ | </ | ||
+ | |||
+ | Zeiger sind zu " | ||
+ | |||
+ | Kaffeekanne k; | ||
+ | Kaffeemaschine m(k); // Beziehung während Zubereitung nicht zerbrechen! | ||
+ | |||
+ | Für gegenseitige Bekanntmachungen ist ein Protokoll (Handshaking) zu vereinbaren, | ||
+ | |||
+ | <code cpp> | ||
+ | void A:: | ||
+ | { | ||
+ | A:: | ||
+ | b.eigentuemer(this); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Solche Beziehungen (double dispatch) sind nicht nur moralisch bedenklich: | ||
+ | |||
+ | <code cpp> | ||
+ | void A:: | ||
+ | { | ||
+ | domina.kommandiert(*this); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Die Technik tritt bevorzugt dort auf, wo es wirklich komplex und abstrakt wird. Sie ist mit Bedacht einzusetzen. Ein hoher Kohäsionsgrad der Komponenten behindert die Zerlegung des Entwurfs, erschwert die Implementierung, | ||
+ | |||
+ | Oftmals reicht jedoch eine einseitige Beziehung aus, ist aber im Klassendiagramm nicht markiert. Der Befehlsempfänger b einer Objekt-Hierarchie muss nichts über die Auftraggeber-Klasse A wissen, um seine Aufgabe zu erfüllen (Client-Server-Architektur): | ||
+ | |||
+ | <code cpp> | ||
+ | void A::nutzt(B b) | ||
+ | { | ||
+ | b.do_it(); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Dabei kann b eine Wegwerf-Kopie sein, ein Verweis auf ein gemeinsam genutztes Objekt oder eine ganze Sammlung von Objekten (eine Server-Farm? | ||
+ | |||
+ | ==== Der Einfluss der Kardinäle ==== | ||
+ | |||
+ | Irgendwo müssen die Vielheiten (0/1, 1, n, *) gekapselt werden. Welches Sprachmittel dazu eingesetzt wird, hängt ab von den Besitzverhältnissen, | ||
+ | |||
+ | * (1) Einzelne Bausteine sind als Objekt, Referenz und über (gültige) Zeiger handhabbar. | ||
+ | * (0/1) Ist ein Objekt nur manchmal vorhanden, kann nur ein Zeiger eingesetzt werden. Vor jedem Zugriff ist zu prüfen, ob das Objekt da ist. Mehrere Objekte erfordern Felder oder Container von Objekten, Referenzen oder Zeigern. | ||
+ | * (n) Die maximale Anzahl der Objekte ist bei statischen Feldern begrenzt. | ||
+ | * (*) Dynamische Felder und Container können ihre Größe anpassen. | ||
+ | |||
+ | ==== Dauerhaftigkeit von Beziehungen ==== | ||
+ | |||
+ | Objektvariablen und Referenzen begründen lebenslange, | ||
+ | |||
+ | ==== Besitzverhältnisse ==== | ||
+ | |||
+ | [[eigentum|Eigentum verpflichtet.]] Die Verantwortung für die referentielle Integrität liegt beim Besitzer der Objekte. | ||
+ | |||
+ | Über Zeiger angeforderter Speicher ist spätestens im Destruktor zurückzugeben. Dies ist offensichtlich. Gefährlich wird es, wenn die Klasse mit den Zeigern nicht regelt, was beim Kopieren und Zuweisen von Objekten geschieht. Dann hat man die Kontrolle über die Speicherbereinigung aufgegeben. Speichererschöpfung und Schutzverletzungen sind die Folge. Was zu geschehen hat, ist nicht einheitlich für alle Fälle festlegbar. Es gibt mehrere Ansätze: Verbieten, tiefes Kopieren (Klonen), flaches Kopieren (mit Referenzzählung), | ||
+ | |||
+ | Referenzzählende Zeiger und Container kapseln diese Verantwortung und entlasten ihre Nutzer. Das Programm wird so auch im Fehlerfall sicherer, wenn durch das Auslösen von Ausnahmen der normale Programmlauf unterbrochen werden muss. | ||
+ | |||
+ | Für die Gültigkeit von Referenzen ist nicht die Klasse, sondern deren Umgebung (der Nutzer) verantwortlich. | ||
+ | |||
+ | ==== Gemeinsame genutzte Ressourcen ==== | ||
+ | |||
+ | Nicht jeder muss alles alleine besitzen, nur um irgendwann mal damit umzugehen: Meine Kinder stritten sich, als sie kleiner waren, regelmäßig um ein Auto mit Beinantrieb, | ||
+ | |||
+ | std:: | ||
+ | std:: | ||
+ | |||
+ | Es kommt auf die Formulierung an. Erkennen Sie den Unterschied? | ||
+ | |||
+ | ==== Der Umgang mit Erbschaften ==== | ||
+ | |||
+ | Objekte abgeleiteter Klassen sind nur über Referenzen und Zeiger vom Basistyp ansprechbar. Ein existenzabhängiges, | ||
+ | |||
+ | <code cpp> | ||
+ | class B // B = Baustein = Basis | ||
+ | { | ||
+ | public: | ||
+ | virtual ~B() {} // virtueller Destruktor - siehe nächste Seite | ||
+ | virtual B* clone(); | ||
+ | // ... | ||
+ | }; | ||
+ | |||
+ | class C : public B // C = child = Kind | ||
+ | { | ||
+ | public: | ||
+ | virtual C* clone(); // Rückgabetyp ist kovariant | ||
+ | // ... | ||
+ | }; | ||
+ | |||
+ | class A // A = Aggregat, hier Komposit | ||
+ | { | ||
+ | public: | ||
+ | A(B* bptr) : b(bptr) {} | ||
+ | A(A const& original) : b(original.b-> | ||
+ | A& operator=(A const& rhs) | ||
+ | { | ||
+ | if (this != &rhs) | ||
+ | { | ||
+ | A tmp(rhs); | ||
+ | std:: | ||
+ | } | ||
+ | return *this; | ||
+ | } | ||
+ | ~A() { delete b; } | ||
+ | // ... | ||
+ | private: | ||
+ | B* b; // existenzabhängig | ||
+ | }; | ||
+ | </ | ||
+ | |||
+ | Am besten kapselt man das Kopierverhalten der Klasse A in einer '' | ||
+ | |||
+ | Es kann gefährlich sein, von solchen polymorphen Objekten Objekt-Kopien vom Basistyp zu erzeugen. Solchen Objekten geht es dann wie Aschenputtels Stiefschwestern. Da das Basisobjekt meist weniger Speicherplatz zur Verfügung hat, werden Informationen abgeschnitten (object slicing) und die Objekte überlappen sich dann im Speicher: " | ||
+ | |||
+ | ==== Echte Zerstörer ==== | ||
+ | |||
+ | Werden polymorphe Objekte über Zeiger verwaltet, muss ihre Basisklasse über einen virtuellen Destruktor verfügen, damit beim Abbauen immer die tatsächlichen (virtual = engl. eigentümlichen) Massnahmen, vielleicht weitere Freigaben, ergriffen werden. Hat eine Klasse keinen solchen virtuellen Destruktor, ist sie für Vererbung nicht ernsthaft einsetzbar. Nichts und niemand hindert den Programmierer in C++, von einer Basisklasse ohne virtuellen Destruktor abzuleiten. Die Compiler geben nicht einmal einen Hinweis, dass ein solcher Destruktor erforderlich ist. Lediglich in größeren, länger laufenden Programmen verliert man nach und nach immer wieder Speicher, der nicht ans System zurückgegeben wird. | ||
+ | |||
+ | Zu allem Unglück steht der Hinweis auf die Notwendigkeit des virtuellen Basis-Destruktors in den (Lehr-)Büchern und Texten zur Objektorientierung in C++ sehr weit hinten, wenn überhaupt. Vorher müssen die Begriffe dynamische Speicherverwaltung, | ||
+ | |||
+ | <code cpp> | ||
+ | class Basis | ||
+ | { | ||
+ | public: | ||
+ | virtual ~Basis() {} // <== virtual muss da stehen! | ||
+ | // ... | ||
+ | }; | ||
+ | |||
+ | class Abgeleitet : public Basis | ||
+ | { | ||
+ | // ... | ||
+ | }; | ||
+ | </ | ||
+ | |||
+ | Prüfen Sie Ihre Bücher, wenn möglich vor dem Kauf! Ein schlechtes Beipiel liefert Jesse Liberty ("C++ in 21 Tagen" und "Jetzt lerne ich C++"). Er erwähnt virtuelle Destruktoren, | ||
+ | |||
+ | ===== Quellen ===== | ||
+ | |||
+ | * [Meyers] Scott D. Meyers: Effective C++. 50 ways to improve your programs. Addison-Wesley (1992). | ||
+ | * [Booch] | ||
+ | |||
+ | Weiterführende Literatur: | ||
+ | |||
+ | * [Balzert] Helmut Balzert: Lehrbuch der Softwaretechnik. Spektrum (2000). | ||
+ | * [GoF] Erich Gamma et al.: Entwurfsmuster. Addison-Wesley (1996). | ||
+ | * [Herold] Herold, Klar, Klar: GoTo Objektorientierung. Addison-Wesley (2001). | ||
+ | * [Horstmann] Cay S. Horstman: Object-Oriented Design in C++ and Java. Wiley (1997). | ||
+ | |||
lernen/baustein.txt · Zuletzt geändert: 2020-07-27 09:32 von 127.0.0.1