Modellierung Chat-Server (von OO ist gescheitert)

knotenpunkt

Mitglied
Jetzt habe ich mal wieder ein praktisches Beispiel:


Gehen wir mal davon aus ich baue einen Chat,

Irgendwo habe ich ein Modul, das sich um die Netzwerkkommunikation kümmert.
Am besten eines, das ein nicht blockierendes reactive Modul, wie Netty nutzt (aber das ist nur nebensächlich).


Der Chat hat jetzt mehrere Chaträume und diese Räume können Unterräume haben (übrigens soll alles dynamisch aufgebaut werden: Die Raumstruktur ist also nicht fix)


****** Das hier als kleine Zusatzanforderung, die jetzt aber nicht Hauptkriterium meiner Frage ist*******

Sollte ein Raum länger unbesucht sein, wird er nicht im Arbeitsspeicher gehalten, sondern soll in der Datenbank verweilen.

Im Falle des Betretens kann er in das Serverprogramm geladen werden. Im Falle der Abfrage von Rauminformationen von Aussen, wo nicht erwartet wird, dass viel mehr mit dem Raum interagiert wird, sollen gezielte Informationen ausschließlich aus der Datenbank geholt werden, ohne dass der Raum in seiner Vollständigkeit in den Server geladen werden muss (performance-Gründe!)
Ich gehe mal davon aus, mittels des Proxypattern zu realisieren???!!!

***************************Ende******************************************************


In den einzelnen Chaträumen können Befehle ausgeführt werden, oder auch nicht ausgeführt werden, je nach Raumtyp und Raumkonfiguration gibt es diverse Befehle oder gibt es nicht.

So jetzt wird das ganze interessant

Je nach Befehlstyp, Raumtyp/config.... Usertyp, vorherige Netwerkbelastung des Users, andere Modulzustände etc pp verhält sich entsprechender Befehl anders.

Beispiele (Wir haben den Raum X)

Der User hat einen globalen Mute -> er wird keine Chatnachricht in Raum X absenden können.
oder vllt doch?. Vllt gibt es Räume die den globalen Mute ignorieren, oder einen noch verschärfteren Globalen Mute vorrraussetzen, dass der User nicht schreiben kann!

Ein Admin, der sich nicht in Raum X befindet, kann auch von Aussen in den Raum schreiben
Alle anderen User müssen sich innerhalb des Raumes X befinden um schreiben zu können.
Ausgenommen sind Speziallräume, die von auch von Moderatoren von Aussen beschrieben/gelesen werden können


In Raum X können User, die mit der IP-Adresse ???. anfangen nur alle 10min eine Nachricht hinterlassen.


Dem Admin stehen in Raum X viele weitere Befehle zur Verfügung


usw usf.


Bei einer Nachricht im Raum X, werden alle sich im raum befindlichen notified
Ein Admin, der in den letzten 10min von Aussen in den Raum geschrieben hat, wird auch benachrichtigt
Sollte Raum X entsprechend konfiguriert sein, werden alle im Raum befindlichen User, die in den letzten 15min nichts geschrieben haben, auch nicht benachrichtigt, oder weil meiner Kreativität keine Grenzen gesetzt sind: Werden diese doch benachrichtigt. Der Chattext soll aber durchgewürfelt bei Ihnen ankommen.


Ausserdem kann es sein, wenn die Räume verschachtelt exisitieren, dass parentRoom, seine Config an tiefer liegenden Raum weitergibt

Wie würdet ihr das objektorientiert umsetzen?



Prozedural ist das relativ einfach (auch wenn jetzt nur kurz shemenhaft angerissen und nicht alle constraints beachtet):



commandData=.....;
welcherRaum=commandData.raum
rconfig=getRaumConfig(welcherRaum);
if(rconfig.isAviableCommand(commandBlob)){abbort;}
switch(command)
{
commandBlob:

ip=....
userRight=......//lese aus der DB herauz welche Rechte der User hat
//.....

if(/*hier wird alles mögliche geprüft*/)

//hier werden die daten zubereitet

//und hier potentielle empfänger benachrichtigt und Nachrichten in Structs/Buffer/Datenbank eingetragen
//Also für Logging, oder im Falle dessen dass eine Nachricht nicht zugestellt werden kann, zum Neusenden

//usw
//..
}


Die Frage bei der objektorientierung ist, wie würdet ihr das ganze aufteilen, wo würde der Code stehen?
Entweder ich baue nen Manager aussen, ein Service der sich alle relevanten Daten holt und diese setzt und weitereicht(notify von den clients)....... das ist aber für mich dann eigentlich sehr prozedural (siehe meinen skizzenhaften Code)
Oder ich schreibe den Code in die Raumklasse?, müsste da aber alles von aussen reinreichen, den ganzen Kontext? (Ausserdem habe ich dann Coderedundanz, da ich vermutlich ähnlichen Code in einem anderen Raumtyp wiederholend schreiben müsste)
Und für den Fall, dass Bspw der User in Raum Y bereits 7 Nachrichten geschrieben hat, so darf er in Raum X gerade keine mehr schreiben, also auch an die Information muss ich irgendwie kommen. Ein ausstehendender Manager tut sich da sicher einfacher, aber ist eben eher prozedural.
Oder ich teile den Code irgendwie auf verschieden Klassen auf, aber wie?



Jetzt bin ich mal auf eure Modellierungen gespannt^^
 
Zuletzt bearbeitet von einem Moderator:

mrBrown

Super-Moderator
Mitarbeiter
Sollte ein Raum länger unbesucht sein, wird er nicht im Arbeitsspeicher gehalten, sondern soll in der Datenbank verweilen.

Im Falle des Betretens kann er in das Serverprogramm geladen werden. Im Falle der Abfrage von Rauminformationen von Aussen, wo nicht erwartet wird, dass viel mehr mit dem Raum interagiert wird, sollen gezielte Informationen ausschließlich aus der Datenbank geholt werden, ohne dass der Raum in seiner Vollständigkeit in den Server geladen werden muss (performance-Gründe!)
Ich gehe mal davon aus, mittels des Proxypattern zu realisieren???!!!
Ja, Proxy-Pattern wäre da das passende.
Solche technischen Anforderungen lässt man aber üblicherweise aus den Anforderungen heraus.

Irgendwo habe ich ein Modul, das sich um die Netzwerkkommunikation kümmert.
Am besten eines, das ein nicht blockierendes reactive Modul, wie Netty nutzt (aber das ist nur nebensächlich).
Das wäre uU einfach nur einfaches Schichtenmodell, der Rest der Anwendung bekäme davon nichts mit.

Der Chat hat jetzt mehrere Chaträume und diese Räume können Unterräume haben (übrigens soll alles dynamisch aufgebaut werden: Die Raumstruktur ist also nicht fix)
Das ist dann einfach nur eine Rekursive Datenstruktur wie ein Baum.
Je nach Anforderung mit entsprechenden Anforderungen (darf jeder Raum Unterräume haben? Oder nur bestimmte Räume? Wenn ja, welche?)

In den einzelnen Chaträumen können Befehle ausgeführt werden, oder auch nicht ausgeführt werden, je nach Raumtyp und Raumkonfiguration gibt es diverse Befehle oder gibt es nicht.
Das ist auch kein wirkliches Problem, die sinnvollste Lösung dürfte da ein entkoppeln von Räumen und dem ausführen von Befehlen sein.
Kommt aber sehr auf die Anforderungen an, lass ich deshalb erstmal raus.
Was sind Befehle? Was sind Raumtypen? Was sind Raumkonfiguration?


Je nach Befehlstyp, Raumtyp/config.... Usertyp, vorherige Netwerkbelastung des Users, andere Modulzustände etc pp verhält sich entsprechender Befehl anders.

Beispiele (Wir haben den Raum X)

Der User hat einen globalen Mute -> er wird keine Chatnachricht in Raum X absenden können.
oder vllt doch?. Vllt gibt es Räume die den globalen Mute ignorieren, oder einen noch verschärfteren Globalen Mute vorrraussetzen, dass der User nicht schreiben kann!

Ein Admin, der sich nicht in Raum X befindet, kann auch von Aussen in den Raum schreiben
Alle anderen User müssen sich innerhalb des Raumes X befinden um schreiben zu können.
Ausgenommen sind Speziallräume, die von auch von Moderatoren von Aussen beschrieben/gelesen werden können

In Raum X können User, die mit der IP-Adresse ???. anfangen nur alle 10min eine Nachricht hinterlassen.

Dem Admin stehen in Raum X viele weitere Befehle zur Verfügung
Das ganze klingt erstmal nach etwas im Rahmen eines hierarchischen Rechte-Managements.
Etwa in der Art: Jeder Nutzer kann Rechte für einen Raum haben, diese können die Rechte des Raumes da drüber überschreiben oder von diesen überschrieben werden.
Moderatoren und Admins sind damit mit abgedeckt.

In Raum X können User, die mit der IP-Adresse ???. anfangen nur alle 10min eine Nachricht hinterlassen.
Das wäre damit auch abgedeckt (wobei die harte Abhängigkeit zur IP das unschön macht)

Bei einer Nachricht im Raum X, werden alle sich im raum befindlichen notified
Auch kein Problem, wenn der Raum die Nutzer im Raum kennt.

Ein Admin, der in den letzten 10min von Aussen in den Raum geschrieben hat, wird auch benachrichtigt
Auch kein Problem (entweder werden Admins gesondert behandelt oder, etwas schöner, sowas in der Art wie eine "Aufräum-Strategie", die in dem Fall für Admins nur ein 10min-Timer ist)

Sollte Raum X entsprechend konfiguriert sein, werden alle im Raum befindlichen User, die in den letzten 15min nichts geschrieben haben, auch nicht benachrichtigt, oder weil meiner Kreativität keine Grenzen gesetzt sind: Werden diese doch benachrichtigt. Der Chattext soll aber durchgewürfelt bei Ihnen ankommen.
Erster Fall ist durch das hier drüber abgehakt.

Zweiter Fall würde (da ich das Model grad iterativ anpasse) eine Anpassung daran erfordern: das "Aufräumen" würde nicht das 'ob', sondern (auch) das 'wie' bestimmen.
Bei Admins nach 10min ein "nichts tun", bei normalen nach 15min ein "durchwürfeln".

Ausserdem kann es sein, wenn die Räume verschachtelt exisitieren, dass parentRoom, seine Config an tiefer liegenden Raum weitergibt
Das ist durch den ersten Punkt schon abgehandelt.



Die Frage bei der objektorientierung ist, wie würdet ihr das ganze aufteilen, wo würde der Code stehen?
Erstmal kommt eine vernünftige Modellierung (sowas sollte man auch in PP machen^^).

Prozedural ist das relativ einfach (auch wenn jetzt nur kurz shemenhaft angerissen und nicht alle constraints beachtet):
Ernstgemeinte Frage: Wie soll da irgendwer nach 2 Tagen noch durchblicken oder das irgendwann mal warten?
Eine Prozedur, die alles tut, ist zwar erstmal einfach, aber führt *immer* zu Problemen.




Ein ganz grobes Domänenmodell:

Blank Diagram - Page 1 (1).png

Siehst du da und in den bisherigen Anmerkungen grobe Fehler? (Wohlgemerkt: Domänenmodell, das ist noch nicht die technische Umsetzung!)
Da du sowas anscheinend noch nie gemacht hast: Ein Domänenmodell ist nicht zu Anfang perfekt, sondern wird im Idealfall zusammen von beiden Seiten iterativ entwickelt - in diesem Fall wärst das auf der einen Seite du als der, der die Anforderungen hat - ich geh also selbst nicht davon aus, dass es passend ist.
 

AndiE

Top Contributor
Ich würde das Chatsystem an eine praktische Anwendung hängen. Für mich wäre das ein "Chatsystem einer Universität". Dabei habe ich am Anfang, wenn das System jungfräulich ist, nur einen Benutzer, den ich mal "root" nenne. Dieser kann nun Benutzer verwalten, Forumräume verwalten, Sitzungen verwalten und selbst auch an solchen Sitzungen teilnehmen(chatten). Bei der Umsetzung würde ich einen thin-Client erstellen, der den Chat und die Menus für die Aufgaben enthält, die aber je nach Status des Nutzers verschieden verfügbar sind. Um das zu bewerkstelligen, würde ich eine extra Nachricht zusammenbauen, die als erstes ein Befehlswort enthält. Die Abarbeitung würde mit "login root <pw>" beginnen, womit sich der Superuser anmeldet(pw- passwort). Diese, hier vorgegebene Anzahl von Befehlen, würden in meinem Falle Controler für die verschiedenen Befehle aufrufen, also hier z.B. LoginControler.message( String s), die dann die entsprechende Handlung durchführen. Hier würde im Linux-Sinne der Server die Nachricht "perm 777 <nr>" zurückgeben, was bedeutet, dass der Nutzer alle Rechte hat(permissions). Die übergebene Nummer wäre dann die Verbindungsnummer Damit ist gewährleistet, dass der Client die Infos bekommt, die er haben will und auch mehrere Chats gleichzeitig aufmachen kann.
 

mihe7

Top Contributor
Ernstgemeinte Frage: Wie soll da irgendwer nach 2 Tagen noch durchblicken oder das irgendwann mal warten?

Ich hatte erst vor kurzem die "Freude" mit einem Pseudo-OO-Projekt (sprich: PP mit OO-Schlüsselwörtern) - in dem genau aus dem Grund nix mehr funktionierte. "static" ist das Zauberwort. Fragilität ist da noch untertrieben. Da wird von überall aus alles aufgerufen. Kein Mensch kann das noch irgendwie nachvollziehen. Funktionen, die dem Namen nach lesen soll(t)en, schreiben nebenher munter in die DB usw. Um das mal fassbar zu machen: Kosten bislang für die "Korrekturen" locker 10.000 € - und das war eine relativ kleine Anwendung. Wenn es nach mir gegangen wäre: neu machen - wäre billiger gewesen.
 

knotenpunkt

Mitglied
Hey,


Ja, Proxy-Pattern wäre da das passende.
Solche technischen Anforderungen lässt man aber üblicherweise aus den Anforderungen heraus.
Ja aber gerade um die geht es mir ja^^


Das wäre uU einfach nur einfaches Schichtenmodell, der Rest der Anwendung bekäme davon nichts mit.
Zum Thema Schichtenmodell habe ich ne allgemeine Frage,
Wenn ich in der Objektorientierung zirkuläre Abhängigkeiten generell vermeide, dann habe ich ja eigentlich immer ein Art Schichtenmodell, oder?

Das ist dann einfach nur eine Rekursive Datenstruktur wie ein Baum.
Je nach Anforderung mit entsprechenden Anforderungen (darf jeder Raum Unterräume haben? Oder nur bestimmte Räume? Wenn ja, welche?)
Naja gehen wir einfach davon aus, dass jeder Raum Unterräume haben darf. Einschränken kann man es ja später noch.
die Raumstruktur könnte man als rekursive Datenstruktur abbilden.
Wenn ich sie jetzt aber einfach in einer relationalen Datenbank halte, dann hat das eigentlich keine Relevanz, oder?

Das ist auch kein wirkliches Problem, die sinnvollste Lösung dürfte da ein entkoppeln von Räumen und dem ausführen von Befehlen sein.
Kommt aber sehr auf die Anforderungen an, lass ich deshalb erstmal raus.
Was sind Befehle? Was sind Raumtypen? Was sind Raumkonfiguration?
Wenn ich diesen Constraint erst später hinzugefügt hätte, wäre das ein grosses Problem geworden in der OO.
Nachträglich Dinge zu entkoppeln, wird sicher nicht so einfach sein.
Von daher würde ich wenn es ginge immer alles von vornherein entkoppelt programmieren.
Die prozedurale Programmierung hat sowieso den höchsten Grad der Entkopplung



Das ist auch kein wirkliches Problem, die sinnvollste Lösung dürfte da ein entkoppeln von Räumen und dem ausführen von Befehlen sein.
Kommt aber sehr auf die Anforderungen an, lass ich deshalb erstmal raus.
Was sind Befehle? Was sind Raumtypen? Was sind Raumkonfiguration?

Naja Befehle könnte man auch als Prozeduren sehen, die irgendendwas in meiner Programmwelt ändern. Die Betonung liegt auf irgendwas^^. Der Befehl kann eventuell nur Raumbezogen zugegriffen werden, wird dann aber nicht unbedingt am State des Raumes etwas ändern, sondern sonst irgendwo im Programm.
Raumtypen/konfigurationen geben vor, ob ein Befehl überhaupt in diesem Kontext abrufbar ist und beeinflussen wie der Befehl funktioniert..... sprichdie IFs in meinem skizzierten Code^^

Das ganze klingt erstmal nach etwas im Rahmen eines hierarchischen Rechte-Managements.
Etwa in der Art: Jeder Nutzer kann Rechte für einen Raum haben, diese können die Rechte des Raumes da drüber überschreiben oder von diesen überschrieben werden.
Moderatoren und Admins sind damit mit abgedeckt.
Naja auch das hier wird über if-kaskaden in Zielzustandsänderung überführt

Ernstgemeinte Frage: Wie soll da irgendwer nach 2 Tagen noch durchblicken oder das irgendwann mal warten?
Eine Prozedur, die alles tut, ist zwar erstmal einfach, aber führt *immer* zu Problemen.
das ist ja keine ganz grosse Prozedur. Auch Prozeduren kann man verkleinern^^

switch(....)
case
prozedur1();
case
prozedur2();


wäre ein Anfang

Warum sollte das nicht mehr durchblickbar sein?

Und das ganze ist eben sehr einfach zu erweitern.
Kommt ein neuer constraint hinzu -> neues if rein
Neuer Befehl -> neues switch
usw


Wenn ich jetzt die Graphenstruktur an mein Modell anpasse und ich ändere was, dann muss ich die Graphenstrukur/Objektstruktur wenns dumm kommt komplett ändern!


Siehst du da und in den bisherigen Anmerkungen grobe Fehler? (Wohlgemerkt: Domänenmodell, das ist noch nicht die technische Umsetzung!)
Da du sowas anscheinend noch nie gemacht hast: Ein Domänenmodell ist nicht zu Anfang perfekt, sondern wird im Idealfall zusammen von beiden Seiten iterativ entwickelt - in diesem Fall wärst das auf der einen Seite du als der, der die Anforderungen hat - ich geh also selbst nicht davon aus, dass es passend ist.
Ne sieht eigentlich gut aus^^
Aber ja ich bin an der technischen Umsetzung interessiert:
Also wo ich dann nacher die Funktionalität hinpacke die ich da so schön in meinem switch case habe
wo nacher der Code steht der in den if(rights.... raum_config.... usw) -> do_sth_with_the_programm();
steht usw....

Weil hier sehe ich irgendwie die größten Probleme, bzw. kA wie man das sinnvoll umsetzen soll.


soweit auch hier mal wieder

lg knotenpunkt
 

mihe7

Top Contributor
Wenn ich jetzt die Graphenstruktur an mein Modell anpasse und ich ändere was, dann muss ich die Graphenstrukur/Objektstruktur wenns dumm kommt komplett ändern!

Richtig. Wenn Du erst Anforderungen bzgl. eines Chat stellst, die Du dann zum Sonnensystem inkl. Motorrad mit Steuerung in Abhängigkeit der Sonnen-Kerntemperatur umbiegst, dann musst Du den Graphen komplett ändern.
 

AndiE

Top Contributor
-PAXP-deijE.gif
-PAXP-deijE.gif
-PAXP-deijE.gif
das wäre eine Idee von mir. Ich habe mir gerade den UML-Designer runtergeladen, daher ist es etwas unübersichtlich. Aber ich denke, das Grundprinzip des Servers ist auch so zu erkennen.
 

Anhänge

  • ChatServer.jpg
    3,2 MB · Aufrufe: 146

Meniskusschaden

Top Contributor
Richtig. Wenn Du erst Anforderungen bzgl. eines Chat stellst, die Du dann zum Sonnensystem inkl. Motorrad mit Steuerung in Abhängigkeit der Sonnen-Kerntemperatur umbiegst, dann musst Du den Graphen komplett ändern.
Das sehe ich auch so. Aber dieses Motorrad-Sonnentemperatur-Beispiel ist für mich eigentlich ohnehin nur das Synonym für die Frage "Was mache ich, wenn ich zur Lösung meines Problems eine Information benötige, die nichts mit der Lösung meines Problems zu tun hat?". Deshalb ist es auch so absurd gewählt.
Und das ganze ist eben sehr einfach zu erweitern.
Kommt ein neuer constraint hinzu -> neues if rein
Neuer Befehl -> neues switch
Das ist das klassische Beispiel für einen Verstoß gegen das Open-Closed-Prinzip. In der OOP erstellst du für den neuen Befehl eine weitere Implementierung des Interfaces und bist fertig.
Bei deinem Ansatz erstellst du eine neue Prozedur und bist noch nicht fertig. Du musst noch an allen Aufrufstellen die switch-Anweisung anpassen.
Obwohl sich für den eigentlichen Algorithmus nichts geändert hat, musst du ihn anfassen. Zum Glück hast du es aber immerhin soweit richtig gemacht, dass es diesen Aufruf nur an einer Stelle gibt. Du bist doch sicher, dass du nicht irgendwann mal an einer zweiten Stelle einen Aufruf eingebaut hast? Auch der Kollege nicht? Ach, prüfe es lieber noch mal nach.;)
Wenn du Pech hast, sind für die beiden Anpassungen sogar zwei unterschiedliche Programmierer zuständig, die es dann noch in der richtigen Reihenfolge einbauen müssen.
 

mrBrown

Super-Moderator
Mitarbeiter
Ja, Proxy-Pattern wäre da das passende.
Solche technischen Anforderungen lässt man aber üblicherweise aus den Anforderungen heraus.
Ja aber gerade um die geht es mir ja^^
Und warum geht es dir um die technischen Details, welche Dinge grad im Arbeitsspeicher liegen (wäre jetzt meine Frage an den Kunden)? (BTW: Kunden (und alle sonstigen außerhalb des eigentlichen Entwicklerteams), die technische Umsetzungen so vorgeben, sind scheiße, freundlich ausgedrückt)

Zum Thema Schichtenmodell habe ich ne allgemeine Frage,
Wenn ich in der Objektorientierung zirkuläre Abhängigkeiten generell vermeide, dann habe ich ja eigentlich immer ein Art Schichtenmodell, oder?
Ja (zirkuläre Abhängigkeiten vermeidet man aber *immer*).

Naja gehen wir einfach davon aus, dass jeder Raum Unterräume haben darf. Einschränken kann man es ja später noch.
die Raumstruktur könnte man als rekursive Datenstruktur abbilden.
Wenn ich sie jetzt aber einfach in einer relationalen Datenbank halte, dann hat das eigentlich keine Relevanz, oder?
Noch mal Verweis auf oben: technische Umsetzungen in den Anforderungen sind scheiße.

Und nein, eher andersrum - das es in einer relationalen Datenbank liegt, hat keine Relevanz. Relevanz hat die Anforderung an die Modellierung, und die Einschränkungen sollten dabei so früh wie möglich kommen.

Wenn ich diesen Constraint erst später hinzugefügt hätte, wäre das ein grosses Problem geworden in der OO.
Nachträglich Dinge zu entkoppeln, wird sicher nicht so einfach sein.
Von daher würde ich wenn es ginge immer alles von vornherein entkoppelt programmieren.
Die prozedurale Programmierung hat sowieso den höchsten Grad der Entkopplung
Nö, hat sie nicht. Bist du es nicht irgendwann Leid, solche haltlosen Behauptungen ohne jegliche Begründung hinzuklatschen? ;)

Aber zu dem eigentlichem: wenn du Constraints nachträglich hinzufügst: ja, dann muss man was ändern. Deshalb sind passende Anforderungen wichtig.
In deinem Beispielcode müsste man btw genauso etwas anpassen, und sogar noch viel schlimmer, weil alles in einer Funktion geklatscht ist.

Naja Befehle könnte man auch als Prozeduren sehen, die irgendendwas in meiner Programmwelt ändern. Die Betonung liegt auf irgendwas^^. Der Befehl kann eventuell nur Raumbezogen zugegriffen werden, wird dann aber nicht unbedingt am State des Raumes etwas ändern, sondern sonst irgendwo im Programm.
Raumtypen/konfigurationen geben vor, ob ein Befehl überhaupt in diesem Kontext abrufbar ist und beeinflussen wie der Befehl funktioniert..... sprichdie IFs in meinem skizzierten Code^^
Dieses if: if(/*hier wird alles mögliche geprüft*/)?

Und bitte mal ernsthaft bleiben, "alles mögliche und je nach Raum anders" ist sicher keine ernsthafte Anforderung. Wie zur Hölle soll das irgendwie konfigurierbar und von Nutzern überblickbar sein?

Also bitte: konkretere Anforderungen oder mindestens Beispiele.

Naja auch das hier wird über if-kaskaden in Zielzustandsänderung überführt
if-kaskaden sind übrigens auch in PP ein Zeichen für schlechten Code.

In OO sind das in diesem Fall keine Kaskade, sondern ist vermutlich nicht tiefer als ein if geschachtelt.


Und das ganze ist eben sehr einfach zu erweitern.
Kommt ein neuer constraint hinzu -> neues if rein
Neuer Befehl -> neues switch
usw
Das wird keineswegs dein "alles kann alles" abdecken und das ist doch statisch und völlig unflexibel ;)

In OO würdest du dafür ein neues Objekt anlegen (wenn nötig sogar völlig zur Laufzeitund den alten Code nicht anfassen. Was ist jetzt flexibler - dynamisch neue Constraints ohne Änderungen zur Compiler-Zeit oder überhaupt an bestehendem Code oder ein "if" einfügen und neu kompilieren?

Wenn ich jetzt die Graphenstruktur an mein Modell anpasse und ich ändere was, dann muss ich die Graphenstrukur/Objektstruktur wenns dumm kommt komplett ändern!
Joa, auch in PP^^

Aber ja ich bin an der technischen Umsetzung interessiert:
Also wo ich dann nacher die Funktionalität hinpacke die ich da so schön in meinem switch case habe
wo nacher der Code steht der in den if(rights.... raum_config.... usw) -> do_sth_with_the_programm();
Gleiches bist du übrigens auch schuldig, dein switch-case deckt da keineswegs überhaupt irgendwas ab ;)

Aber keine Sorge, die kommt noch. Aber wie gesagt, erstmal steht da Modellierung an:

* Was sind Raumtypen?
* Was sind Raumkonfigurationen?
* Was sind Befehle? Welche Beispiele gibt es dafür, was müssen die alles können?
* darf jeder Raum Unterräume haben? Oder nur bestimmte Räume? Wenn ja, welche?

Wenn du dir zu den Punkten keine Gedanken gemacht haben solltest, dann ist jetzt der Zeitpunkt dafür.
Im Idealfall gibt es zu jedem Punkt einen Use-Case oder mindestens eine User-Story.
 

AndiE

Top Contributor
Ich würde mein Uni-System weiterspinnen. Der Professor Meier loggt sich ein. Dann erstellt er eine Sitzung " Zum Liebesleben der Heuschrecke" und lädt dazu das 3. Semester ein. An einem anderen Computer loggt sich der Student Lehmann in das System ein und sieht, dass der Prof. Meier eine Sitzung erstellt hat und ihn dazu eingeladen hat. Den interessiert aber das Thema nicht, sondern er möchte lieber mit einigen Mitstudenten noch Nachhilfe in Schneckenkunde haben. Dies schreibt er dem Professor. Der erstellt nun innerhalb der Gruppe des 3. Semesters eine Gruppe "Schneckenkunde" und fügt die Betreffenden hinzu. Anschließend erstellt er einen Termin für eine Chatsitzung dazu.

Frage: Wo sind hier die Räume? Welchen Vorteil hätten sie?

Wäre die Unterteilung der Nutzer nicht sinnvoller?
 

knotenpunkt

Mitglied
Hey,

So dann gehts auch hier mal weiter:

Das sehe ich auch so. Aber dieses Motorrad-Sonnentemperatur-Beispiel ist für mich eigentlich ohnehin nur das Synonym für die Frage "Was mache ich, wenn ich zur Lösung meines Problems eine Information benötige, die nichts mit der Lösung meines Problems zu tun hat?". Deshalb ist es auch so absurd gewählt.
Nein! Es zeigt wie unflexibel OO ist! Wie bereits im anderen Faden aufgezeigt (Beitrag 89): Unflexibel != Unlösbar


Das ist das klassische Beispiel für einen Verstoß gegen das Open-Closed-Prinzip. In der OOP erstellst du für den neuen Befehl eine weitere Implementierung des Interfaces und bist fertig.
Bei deinem Ansatz erstellst du eine neue Prozedur und bist noch nicht fertig. Du musst noch an allen Aufrufstellen die switch-Anweisung anpassen.
Obwohl sich für den eigentlichen Algorithmus nichts geändert hat, musst du ihn anfassen. Zum Glück hast du es aber immerhin soweit richtig gemacht, dass es diesen Aufruf nur an einer Stelle gibt. Du bist doch sicher, dass du nicht irgendwann mal an einer zweiten Stelle einen Aufruf eingebaut hast? Auch der Kollege nicht? Ach, prüfe es lieber noch mal nach.;)

Das Switch-Case kann ich sehr wohl in ein Foreach(allCommandsList) ummünzen.
Aber auch hier müsste ich neu programmierte Unterprozedur der allCommandsList hinzufügen.
Zudem möchte ich es nicht bei dem vereinfachten switch-case belassen. Auch das kann ich komplexer gestalten. Hier wäre dann diese einfache Transformation auch nicht mehr möglich.
Aber ich glaube du hast dir etwas ganz anderes darunter vorgestellt.
Beschreibe doch mal deine Lösung, wo ich nur erweiternt einen Algorithmus schreiben muss und der dann sofort ohne Änderung an darüberliegender Strukur (in meinem Fall ja das switch-case) verfügbar ist.

Erweiternt dann auch noch unter dem Gesichtspunkt, dass der Algorithmus tatsächlich nicht einfach nur über switch-case aufgerufen wird, sondern erst nach Abarbeitungen komplexer Voralgorithmen (ifs)

Meiner Meinung nach geht das nicht. Klar, es bedarf hier einer Anpassung auch der darüberliegenden Struktur. Aber das ist ja vllt. auch gewollt?

OO steht oft für Typerweiterung. Sprich ich decke eine Art horizontale Erweiterbarkeit mit OO ab.
Ich hab eine Oberklasse Produkt und dann zwei Subklassen Milch und Apfel. Beide haben eine Funktion calculateOwnPrice(). Hier kann ich wirklich ganz einfach eine weitere Produkklasse hinzufügen. Bspw. Banane.
Die Funktion calculateOwnPrice() bei der Banane ist auch sehr schnell geaddet. Die darüberliegende Struktur muss ich in dem Fall tatsächlich nicht anfassen.

Aber es gibt eben auch die vertikale Erweiterbarkeit:
Für das gibt es in der OO lustigerweise das VisitorPattern.

Aber OO bekommt die vertikale und horzontale Erweiterbarkeit nicht unter einen Hut.
Und für mich ist die Vertikale viel wertvoller, vor allem bekomme ich bei einer gut vertikal erweiterbaren Software auch eine direkte Flexibilität geschenkt.

Gut jetzt bin ich etwas vom Thema abgeschweift!
Back to the topic:


Und warum geht es dir um die technischen Details, welche Dinge grad im Arbeitsspeicher liegen (wäre jetzt meine Frage an den Kunden)? (BTW: Kunden (und alle sonstigen außerhalb des eigentlichen Entwicklerteams), die technische Umsetzungen so vorgeben, sind scheiße, freundlich ausgedrückt)
Hier im Forum gehts mir jetzt tatsächlich um die Umsetzung. Ich möchte ja wissen wie es umgesetzt wird.
Wie du es umsetzen würdest!^^


Ja (zirkuläre Abhängigkeiten vermeidet man aber *immer*).
Ok wie würdest ne Datenbankklasse schreiben und ne Loggerklasse.
Sollte ein Query failen, dann gibts ne Request an die Loggerklasse, diese wiederrum verwendet dann wieder die Datenbankklase um die Fehlermeldung zu persistieren. Klar für den Fall, dass die Loggerklasse selbst gerade irgend eine Datenbankfunktionalität verwenden möchte, die aufgrund eines Datenbankdowns failed, muss hier eine Ausnahmeregelung geschaffen werden.

Anderes Beispiel: Bibliothek und Bücher:

Die Direktion Biblothek Richtung Bücher ist ja offensichtlich.
Aber wenn ich jetzt vom Buch aus Richtung Bibliothek wissen möchte, welches Buch dessen Nachbar ist, oder in welcher Bibliothek es selbst sich befindet, dann habe ich hier ja auch eine zirkuläre Abhängigkeit.


Wie würdest du meine beiden oben genannte Fallbeispiele ohne zirkuläre Abhängigkeiten umsetzen?


Und nein, eher andersrum - das es in einer relationalen Datenbank liegt, hat keine Relevanz. Relevanz hat die Anforderung an die Modellierung, und die Einschränkungen sollten dabei so früh wie möglich kommen.
Ja aber wir wollen es hier doch jetzt mal konkret machen.


Und bitte mal ernsthaft bleiben, "alles mögliche und je nach Raum anders" ist sicher keine ernsthafte Anforderung. Wie zur Hölle soll das irgendwie konfigurierbar und von Nutzern überblickbar sein?

Also bitte: konkretere Anforderungen oder mindestens Beispiele.

Naja du wolltest doch Anforderungen: Das ist eben eine^^
Vllt. verhält sich tatsächlich nicht jeder Raum anders, aber es gibt vllt. Ausnahmen, vllt. auch nur kleine Detailunterschiede, oder aber doch ganz Hart, alles ist überall anders^^
Diese Anforderungen interessieren mich insbesondere unter dem Gesichtspunkt, wie du sie jeweils technisch OO umsetzen würdest.



if-kaskaden sind übrigens auch in PP ein Zeichen für schlechten Code.

In OO sind das in diesem Fall keine Kaskade, sondern ist vermutlich nicht tiefer als ein if geschachtelt.
kommt jetzt natürlich drauf an, wie man eine if-kaskade definiert:

procedure1:
if(bla_blub)
{
procedure2();
}
else
{
procedure3();
}

procedure2()
{
if(...)
{
procedure4()
}
}


Ist das für dich eine if-kaskade?
Und wie sieht das hier OO anders aus?


Das wird keineswegs dein "alles kann alles" abdecken und das ist doch statisch und völlig unflexibel ;)

In OO würdest du dafür ein neues Objekt anlegen (wenn nötig sogar völlig zur Laufzeitund den alten Code nicht anfassen. Was ist jetzt flexibler - dynamisch neue Constraints ohne Änderungen zur Compiler-Zeit oder überhaupt an bestehendem Code oder ein "if" einfügen und neu kompilieren?
Siehe Begründung weiter oben: Das Switch-Case kann ich ..............

Und ja ich muss neu kompilieren, aber wenn das mein einziges Problem ist, warum nicht?
Um die Neu-Kompilieren-Problematik mal etwas zu entkräften, was in deiner Argumentation ja doch viel Platz einnimmt.


Gleiches bist du übrigens auch schuldig, dein switch-case deckt da keineswegs überhaupt irgendwas ab ;)

Aber keine Sorge, die kommt noch. Aber wie gesagt, erstmal steht da Modellierung an:

* Was sind Raumtypen?
* Was sind Raumkonfigurationen?
* Was sind Befehle? Welche Beispiele gibt es dafür, was müssen die alles können?
* darf jeder Raum Unterräume haben? Oder nur bestimmte Räume? Wenn ja, welche?

Wenn du dir zu den Punkten keine Gedanken gemacht haben solltest, dann ist jetzt der Zeitpunkt dafür.
Im Idealfall gibt es zu jedem Punkt einen Use-Case oder mindestens eine User-Story.
Doch man bekommt ungefähr eine Idee davon, wo nacher der Code steht. (zu Gleiches bist du üb........)

Diese Idee hab ich aber nicht in der OO-Welt. Von daher frage ich ja, wie du es machen würdest
Ja man sieht viel (Daten)-Struktur, wie auch in den ganzen Diagrammen, die ich oben aufrufen kann.
Aber wo nacher der Code im Konkreten stehen wird, das kann ich da nicht herauslesen.


Die Anforderungen habe ich doch bereits in meinem Eingangsbeitrag erklärt.

Ein Raumtyp gibt schonmal ne gewisse Default-Raumkonfiguration vor.
Diese können aber zur Compilezeit als auch zur Laufzeit angepasst werden.


Zu den Befehlen habe ich doch auch bereits umfangreiche Beispiele gegeben, siehe Eingangsbeitrag.
Und vor allem weil es viele Befehle gibt, die Raumübergreifend, Unterraumübergreifend, Global, oder von sonst wo her aufrufbar sind, ist es schwierig, diese an eine bestimmte Stelle zu modellieren.
Aber gut, du bist ja der OO Spezialist. Ich bin auf deinen Ansatz gespannt^^


Zu der Frage ob jeder Raum Unterräume haben darf, kann ich nur sagen:
Was für eine Frage^^
Du weißt doch, ich will alles so flexibel wie möglich haben: also JA^^
Automatisch hat vllt. nicht gleich jeder Raum je nach Raumtyp und Konfiguration direkt die Möglichkeit Unterräume zu spawnen. Aber spätestens wenn ich als Admin einen entsprechenden Befehl ausführe, kann ich jedem Raum einen Unterraum verpassen - Zur Laufzeit!


Soweit mal hier....


lg knotenpunkt
 

Meniskusschaden

Top Contributor
Das Switch-Case kann ich sehr wohl in ein Foreach(allCommandsList) ummünzen.
Aber auch hier müsste ich neu programmierte Unterprozedur der allCommandsList hinzufügen.
Leider wieder zu unspezifisch für eine konkrete Antwort. Was ist allCommandsList in Bezug auf dein Codebeispiel?
Beschreibe doch mal deine Lösung, wo ich nur erweiternt einen Algorithmus schreiben muss und der dann sofort ohne Änderung an darüberliegender Strukur (in meinem Fall ja das switch-case) verfügbar ist.
Polymorphie. Das switch-case entfällt.

Erweiternt dann auch noch unter dem Gesichtspunkt, dass der Algorithmus tatsächlich nicht einfach nur über switch-case aufgerufen wird, sondern erst nach Abarbeitungen komplexer Voralgorithmen (ifs)
Wird er nicht. Das ist dein Denkfehler. Auf die Frage, wie man schlechtes Design zu gutem macht, ohne es zu ändern, gibt es keine Antwort. Wenn deine aufrufenden Algorithmen schlecht entworfen sind, wird es nicht besser, wenn sie Objekte verwenden. Ich habe ja in einem der beiden Threads schon mal geschrieben, dass man solche Designfehler mit PP oft länger verdecken kann. Vorteil oder Nachteil? Ich finde es besser, wenn man frühzeitig merkt, dass man seine Architektur versemmelt hat. Für mich also pro OOP.

Aber OO bekommt die vertikale und horzontale Erweiterbarkeit nicht unter einen Hut.
Was meinst du mit vertikaler und horizontaler Erweiterbarkeit und welches Problem hat die OO damit?
 

mrBrown

Super-Moderator
Mitarbeiter
OO steht oft für Typerweiterung. Sprich ich decke eine Art horizontale Erweiterbarkeit mit OO ab.
Aber es gibt eben auch die vertikale Erweiterbarkeit
Unter Horizontal verstehst du das erweitern um neue Typen und unter Vertikal das Erweitern um neue Funktionen? (Die Begriffe hab ich in dem Kontext noch nie gehört...)


Aber es gibt eben auch die vertikale Erweiterbarkeit:
Für das gibt es in der OO lustigerweise das VisitorPattern.
Das Visitor-Pattern ist nur nötig, wenn die jeweilige Sprache kein Multi-Dispatch hat. Es gibt durchaus auch OO-Sprachen, die es haben, die dann kein Visitorpattern brauchen.
Genauso kann man das Visitor-Pattern durch "if instanceOf"-Kaskaden ersetzen.

Hauptanwendungsfall für's Visitorpattern: Der Laufzeittyp eines Objektes ist nicht bekannt, für unterschiedliche Typen müssen aber unterschiedliche Methoden aufgerufen werden.

Wie bekommst du dass denn in PP einfacher hin? (zB mit C, so ganz ohne Laufzeit-Typen und Überladung...)

Und für mich ist die Vertikale viel wertvoller, vor allem bekomme ich bei einer gut vertikal erweiterbaren Software auch eine direkte Flexibilität geschenkt.
Unsinn. Keine der Varianten ist deutlich flexibler als die andere (vielleicht das, was du unter "flexibel" verstehst, aber das würde auch niemand sonst so nennen).

Hier im Forum gehts mir jetzt tatsächlich um die Umsetzung. Ich möchte ja wissen wie es umgesetzt wird.
Wie du es umsetzen würdest!^^
Können wir uns drauf einigen, entweder um die konkrete Umsetzung einzelner Teile zu reden oder um die Modellierung eines Gesamtsystems? Beides zu vermischen endet üblicherweise im Chaos.


Ok wie würdest ne Datenbankklasse schreiben und ne Loggerklasse.
Sollte ein Query failen, dann gibts ne Request an die Loggerklasse, diese wiederrum verwendet dann wieder die Datenbankklase um die Fehlermeldung zu persistieren. Klar für den Fall, dass die Loggerklasse selbst gerade irgend eine Datenbankfunktionalität verwenden möchte, die aufgrund eines Datenbankdowns failed, muss hier eine Ausnahmeregelung geschaffen werden.

Alle statischen-Abhängigkeiten laufen nur in eine Richtung:
Code:
----------   ------------
| Logger |-->| Appender | <--|
----------   ------------    |
    ^                        |
____|________________________|___
    |                        |
--------------------      --------------------
| Datenbank-Klasse | <--- | DatabaseAppender |
--------------------      --------------------
Anderes Beispiel: Bibliothek und Bücher:

Die Direktion Biblothek Richtung Bücher ist ja offensichtlich.
Aber wenn ich jetzt vom Buch aus Richtung Bibliothek wissen möchte, welches Buch dessen Nachbar ist, oder in welcher Bibliothek es selbst sich befindet, dann habe ich hier ja auch eine zirkuläre Abhängigkeit.
Die Anforderungen sind offensichtlich ein bisschen Unsinn.

Für ein Buch rausfinden, aus welcher Bibliothek es kommt, kann man noch so halb gelten lassen. Umsetzen würde man das, indem Bücher einfach einen "Besitzer"(?) haben.
Ein Buch fragen, wer sein Nachbar ist, ist aber schon Unsinn. Das ist etwas, was nicht vom Buch selber abhängt, warum sollte das Buch dann diese Information haben. Wenn unbedingt nötig, wäre das aber über den Umweg über den "Besitzer" möglich, Buch würde dann einfach an diesen delegieren.

Code:
--------     ------------
| Buch |---> | Besitzer |
--------     ------------
   ^              ^
   |              |
--------------    |
| Bibliothek |-----
--------------



Naja du wolltest doch Anforderungen: Das ist eben eine^^
Nein. "alles mögliche und je nach Raum anders" ist keine Anforderung (dass du es für eine hälst, zeigt ziemlich deutlich, dass du dich noch nie mit Projektplanung etc beschäftigt hast).

Nur mal hypothetisch, du als Kunde, ich als Entwickler:
* wie soll ich als Entwickler auf dieser Basis den Aufwand schätzen?
* wie willst du als Kunde prüfen, ob mein Preis gerechtfertigt ist?
* wie soll ich als Entwickler jemals wissen, ob diese Anforderung erfüllt ist?

procedure1:
if(bla_blub)
{
procedure2();
}
else
{
procedure3();
}

procedure2()
{
if(...)
{
procedure4()
}
}


Und wie sieht das hier OO anders aus?
In gleicher Detailiertheit:

Code:
methodenAufruf()
objekt.einMethodenAufruf()
irgendwas.macheDas()

Entweder du willst *konkrete Beispiele, dann bring selber konkrete Beispiele.
Oder du willst über Modellierung reden, dann bring Anforderungen.
Oder du willst Unsinn, dann ist das bisherige schon ganz passend ;)

Und ja ich muss neu kompilieren, aber wenn das mein einziges Problem ist, warum nicht?
Um die Neu-Kompilieren-Problematik mal etwas zu entkräften, was in deiner Argumentation ja doch viel Platz einnimmt.

Du musst immer bestehenden Code ändern (bzw sogar nur eine Methode), um irgendeine Kleinigkeit zu ändern. Schon mal gesehen, wie gut es klappt, wenn mehrere Personen den gleichen Code ändern oder man immer an einer Codestelle arbeitet?
Du kannst das ganze nicht modularisierten, weil du einen großen Blob hast.
(Und zur Kompilezeit: Chromium z.B. braucht mehrere Stunden für ein frisches Kompilieren.)


Doch man bekommt ungefähr eine Idee davon, wo nacher der Code steht. (zu Gleiches bist du üb........)

Diese Idee hab ich aber nicht in der OO-Welt. Von daher frage ich ja, wie du es machen würdest
Ja man sieht viel (Daten)-Struktur, wie auch in den ganzen Diagrammen, die ich oben aufrufen kann.
Aber wo nacher der Code im Konkreten stehen wird, das kann ich da nicht herauslesen.
Ums noch mal zu wiederholen: ERST kommen Anforderungen, dann Modellierung, danach der Code.
Niemanden interessiert vor dem festlegen der Anforderungen, wo nachher ein konkreter Code-Teil steht.

Die Anforderungen habe ich doch bereits in meinem Eingangsbeitrag erklärt.

Ein Raumtyp gibt schonmal ne gewisse Default-Raumkonfiguration vor.
Diese können aber zur Compilezeit als auch zur Laufzeit angepasst werden.

Zu den Befehlen habe ich doch auch bereits umfangreiche Beispiele gegeben, siehe Eingangsbeitrag.
Und vor allem weil es viele Befehle gibt, die Raumübergreifend, Unterraumübergreifend, Global, oder von sonst wo her aufrufbar sind, ist es schwierig, diese an eine bestimmte Stelle zu modellieren.

Das sind wie schon gesagt keine Anforderungen, sondern irgendwelche Ideen, aus denen sich vielleicht irgendwann mal Anforderungen entwicklen.

"irgendwas an eine bestimmte Stelle zu modellieren" ist auch absolut nicht nötig, hat auch niemand gefordert.

Ansonsten, hier ist dein Produkt:

Java:
public class Raum {
    String name = "DefaultName";
    List<Raum> unterräume;
    public void befehlAusführen(Consumer befehl) {
        befehl.consume();
    }

    //GETTER/SETTER
}
* Ein Raumtyp gibt schonmal ne gewisse Default-Raumkonfiguration vor. -> erfüllt, sie haben einen Default-Namen (nämlich "DefaultName")
* Diese können aber zur Compilezeit als auch zur Laufzeit angepasst werden. -> Zur Kompilezeit kannst du neu kompilieren mit anderen Werten, zur Laufzeit kannst du einen Consumer übergeben, der den Raum verändern kann
* Beliebige Befehle sind ausführbar, wobei die nirgends irgendwo eingeschränkt sind
* Jeder Raum kann beliebige Unterräume haben

Siehst du: Perfekt umgesetzt mit OOP.

(Ja, das war Sarkasmus.)

Ein Raumtyp gibt schonmal ne gewisse Default-Raumkonfiguration vor.
Was sind Raumtypen?
Was ist deren Konfiguration?

Einfach nur "Es gibt Raumtypen, die geben Konfigurationen vor" ist keine Anforderung.

Diese können aber zur Compilezeit als auch zur Laufzeit angepasst werden.
Was kann man anpassen?
Ein "Alles" ist keine Anforderung.

Zu den Befehlen habe ich doch auch bereits umfangreiche Beispiele gegeben, siehe Eingangsbeitrag.
Nein, dort gibt es keine sinnvollen Anforderungen an Befehle. Einfach nur abstruse Beispiele Hinklatschen ist keine Anforderung.

Und vor allem weil es viele Befehle gibt, die Raumübergreifend, Unterraumübergreifend, Global, oder von sonst wo her aufrufbar sind, ist es schwierig, diese an eine bestimmte Stelle zu modellieren.

Du sollst auch nichts "an eine bestimmte Stelle modellieren" (was soll das heißen?), sondern einfach nur Anforderungen an Befehle stellen. Eine Anforderung, die du selber nicht aufschreiben kannst, ist keine Anforderung.

Aber gut, du bist ja der OO Spezialist. Ich bin auf deinen Ansatz gespannt^^
Du wirst von niemandem, egal wofür der Spezialist ist, einen Ansatz bekommen, wenn du nicht in der Lage bist, auch nur minimalste Anforderungen vorzugeben. Auch nicht von einem PP-Spezialist.

Zu der Frage ob jeder Raum Unterräume haben darf, kann ich nur sagen:
Was für eine Frage^^
Du weißt doch, ich will alles so flexibel wie möglich haben: also JA^^
Automatisch hat vllt. nicht gleich jeder Raum je nach Raumtyp und Konfiguration direkt die Möglichkeit Unterräume zu spawnen. Aber spätestens wenn ich als Admin einen entsprechenden Befehl ausführe, kann ich jedem Raum einen Unterraum verpassen - Zur Laufzeit!
Das ist immerhin mal ein Ansatz einer konkreten Anforderung ;)

Also: Ein Raum kann Unterräume haben, wenn es für diesen freigegeben ist.

Ob er es darf, hängt von Typ und Konfiguration ab, wobei das von einem Admin überschrieben werden kann (= Konfiguration ändern?).
Was ist ein Befehl in diesem Kontext? Ein Klick in der Oberfläche?
 

Ähnliche Java Themen

Neue Themen


Oben