jCLS - Vorstellung und Bitte um Codereview

Guten Abend allerseits

Jetzt ist es endlich soweit, ich habe eine erste Version meines Projekts auf SF hochgeladen und da dies mein erstes "ernsthaftes" Programm ist und ich kein Programmierer bin, bitte ich euch um eure Meinung zur Umsetzung. :)
Gleich vorweg: Erwartet von dem Programm noch nichts. Es ist so ziemlich die kleinstmögliche Version, um eine Vorstellung davon zu vermitteln, was es später mal machen soll. Es ist noch lange nicht fertig, es ist noch nichtmal in irgendeiner Art und Weise praktisch einsetzbar. Ich finde aber, es ist ein guter Zeitpunkt, um Kritik über die Architektur einzuholen, daher habe ich es jetzt schon veröffentlicht.

Zum Zweck des Programms:
Ich komme ja aus dem Hardwarebau, und da gibt es eine für mich äußerst lästige Arbeit: das Pflegen von Bauteilbibliotheken in EDA-Programmen. (Wer es nicht kennt: Electronic Design Automatisation, damit erstellt man Schaltpläne, Platinenlayouts und mehr oder weniger alles was man braucht um diese dann fertigen lassen zu können, im Prinzip das was CAD für Maschinenbauer ist.)

Programme wie Altium Designer können einem unheimlich vieles an Arbeit abnehmen wenn z.B. Dinge wie die Herstellernummer des Bauteils hinterlegt ist. Bisher habe ich dafür meist Exelscripte geschrieben oder viel Handarbeit angelegt, andere Entwicker sind da pragmatischer und verzichten auf die ganzen tollen Extras und nehmen die Umständlichkeiten in Kauf. Für mich sind beide Optionen inakzeptabel da ich einerseits penibel und akkurat bin, andererseits bin ich ein stockfauler Hund. Und Altium Designer ist ein nicht ganz billiges Werkzeug, das will gefälligst genutzt werden.
Also muß ein Werkzeug her, auf das der Rechner nun auch für die Bibliothekspflege geknechtet werden kann.

Ich erkläre am Besten an einem Beispiel: Dies ist das Datenblatt für eine Widerstandsserie von Yageo:

Dort steht gleich auf der ersten Seite, wie sich die Herstellernummer zusammensetzt.
1. Serienkürzel
2. Gehäuse (0402/0603/0805/1206)
3. Toleranz (0,1%; 0,25%; 0,5%; 1%)
4. Verpackung
5. Widerstandswert (variiert je nach Gehäusegröße)

Ziel ist es nun, eine Tabelle zu erhalten, in der ein paar Informationen über ein Bauteil enthalten sind. Der Benutzer soll frei wählen können welche Informationen er für relevant hält und was drinstehen soll. Gehäuse, Toleranz und Bauteilwert wären schonmal nützliche Informationen, die Herstellernummer wollen wir auch. Zusätzlich braucht es noch ein paar Parameter um Altium Designer mitzuteilen, wo es Schaltplansymbol und Footprint findet, und wir wollen auch das Datenblatt verlinken.

So in etwa soll es am Ende aussehen:
Widerstandswert
Toleranz
Gehäuse
Schaltplansymbol
Footprint
1kΩ1%0603C:/lib/Symboldatei.libC:/lib/Footprints.lib
1kΩ1%0805C:/lib/Symboldatei.libC:/lib/Footprints.lib
1kΩ1%1206C:/lib/Symboldatei.libC:/lib/Footprints.lib

So in etwa...in Kurzform. Eine einzelne Widerstandsserie erzeugt gerne mal >100k Zeilen, und ich hab in meinen Bibliotheken ich glaub über 20 Parameter für jedes Bauteil drin.


Um euch zu zeigen wie das Programm arbeitet, hier ein kleines Minimalbeispiel:
1.:
Startet das Programm. Falls es mit Gradle nicht funktioniert (was leider gut sein kann), gibt es eine fertiges Kompilat. Die Ordner und Dateien wichtig, die .jar kann alleine nicht. Aktuell besteht das Programm im Wesentlichen aus zwei Tabs. Geht in den ersten Tab. Das Programm kann leider momentan nur über das Kontextmenü bedient werden.

2.:
Zuerst erstellt ihr ein neues Datenset. Gebt ihm irgendeinen Namen.

3.:
Noch ein Rechtsklick, diesmal fügt ihr eine neue Bauteileigenschaft hinzu. Laßt diese persistent, aber gebt ihr den Namen "Liste". Erstellt eine zweite persistente Eigenschaft und nennt sie "Konstante".

4.:
Wenn alles gut ging, könnt ihr dem Datenset jetzt einen Datenstempel zuweisen. Rechtsklick so, daß ihr Datenset und die Eigenschaft "Liste" trefft, und fügt einen Listenstempel hinzu. Danach öffnet sich ein Stempeleditor. In diesem tragt ihr ein:
1
2
3
und klickt auf OK.
Jetzt fügt ihr auf die gleiche Weise einen Konstantenstempel ein, als Werte gebt ihr jetzt aber
A
B
ein und klickt wieder auf OK.

6.:
Wenn bis hierher alles gut lief, müßtet ihr jetzt (in zweifellos verbesserungsbedürftiger Weise) ein Datenset mit zwei Bauteilstempeln und deren Parametrisierung angezeigt bekommen. Es ist Zeit, daraus eine Tabelle zu machen. Rechtsklick -> Bauteilwerte generieren. Danach seht ihr im zweiten Tab (der war vorher leer) eine Tabelle wie:
ListeKonstante
1A
2A
3A
1B
2B
3B

Es ist zwar nur ein kleines Beispiel, aber ich denke, die meisten von euch bekommen jetzt eine Idee davon, wie ich damit zu großen Bauteiltabellen kommen will. Es gibt nochmehr Stempelarten, jede erzeugt Werte auf eine andere Weise. Es können übrigens auch noch mehr Datensets erzeugt werden. Mehr als diese Tabelle im zweiten Tab ist aktuell noch nicht zu sehen, es gibt noch keine Exportfunktion...wie gesagt, kleinstmögliche Demonstratorversion. Aber ich denke, als Begutachtungsgegenstand ist es ausreichend.

Hier ist das Projekt:

Zunächst einmal...
...würde ich mich sehr freuen, wenn ihr euch meine Programmstruktur anschaut. Wie gesagt, es ist mein erstes Programm, ich hab vorher in Java nur Kleinkram oder Übungen gemacht. Ich habe mich fast immer aus bestimmten Gründen bestimmte Wege eingeschlagen, aber ohne Erfahrung sind solche Abschätzungen nur von wenig Wert. Daher würde mich eure Kritik und Denkanstöße interessieren, warum ich was wie gemacht habe würde ich im Verlauf des Threads näher ausführen. Der Post ist lang genug, und ich kriege das alles heute Abend sowieso nicht mehr zusammen.

Was noch werden soll
Extras und Funktionen, die ich früher oder später noch hinzufügen will (länsgt nicht vollständig):
-Bibliotheksbrowser (ist aktuell in Arbeit):
Das, was jetzt in den beiden Tabs zusammengefaßt ist, wird zu einer Bibliotheksseite gebündelt. Viele solcher Bibliotheksseiten sollen als Baumstruktur angeordnet werden können.
-Weitgehend vollständige Bedienung über die Tastatur
-Toolbar
-...
 
Mein einziges Feedback wäre: Bitte nicht Sourceforge... :) Das ist nur noch ein Archiv von vor 2008 gestarteten Projekten und keine neuen Projekte werden mehr darauf gehostet.
Bitte nehme einen ernstzunehmenden Repository-Hoster, deren Seite nicht so sehr mit Werbebannern zugeknallt ist, dass man Links zu dem eigentlichen Repository/Projekt nicht mehr von Werbung unterscheiden kann, und der einem erlaubt, eine README.md als zentrale Information auf der Landing-Page des Repositories anzuzeigen.
Alternativen:
- GitHub
- BitBucket
- GitLab
 
Ich bin noch bei weitem nicht durch den Code durch - derzeit konnte ich mir nur ein paar Library Klassen ansehen.

Dabei sind mir ein paar Dinge aufgefallen - aber gleich vorweg: Ich sage nur, dass ICH etwas evtl. anders machen würde. Das heißt aber nicht automatisch, dass DEINE Lösung schlecht ist. Da Ich die genaue Nutzung noch nicht kenne, ist evtl. der eine oder andere Hinweis nicht oder nicht direkt umsetzbar. Das einfach als kurzen "Disclaimer" :)

jCLSLibraries/src/main/java/de/oliverlenz/engineeringutils/SINumerics.java
Hier fällt mir auf:
a) Die Methoden scheinen alle keine Instanz zu nutzen. Daher ist dies so wie sie derzeit existiert, so eine typische Utils Klasse, wie man sie bei einigen Libraries findet (z.B. StringUtils Klasse in diversen Libraries. Oft werden diese Klassen entsprechend benannt (Java Framework macht es aber interessanter Weise nicht. Arrays wäre da aus meiner Sicht so eine Utils Klasse.)
Wichtig ist aber: In diesen Klassen sind die Methoden alle statisch. Es macht einfach keinen Sinn, da erst eine Instanz zu erzeugen, denn diese Instanz wird ja so eigentlich nicht verwendet.
b) Du hast da unter dem Strich zusammengesetzte Werte aber behandelst diese nicht zusammen. Wenn es zusammengesetzte Werte wie 7.5k gibt, dann erwarte ich eigentlich eine Klasse, die dies abbildet. Also dann hättest Du sowas wie eine Klasse SIValue oder so mit entsprechend benötigten Möglichkeiten.

Ansonsten sind es mehr Kleinigkeiten: Bei den Sprach Klassen hast du teilweise keine JavaDoc Kommentare, die Du aber sonst überall hast. Der Kommentar am Kopf ist mir bei mindestens einer Datei aufgefallen, dass es noch der default Kopf war ...

Ansonsten muss ich mal schauen, dass ich den Code herunter geladen bekomme um ihn in der IDE zu betrachten. Hatte da jetzt nur paar Minuten im Webbrowser den Code angesehen.
 
a) Die Methoden scheinen alle keine Instanz zu nutzen. Daher ist dies so wie sie derzeit existiert, so eine typische Utils Klasse, wie man sie bei einigen Libraries findet (z.B. StringUtils Klasse in diversen Libraries. Oft werden diese Klassen entsprechend benannt (Java Framework macht es aber interessanter Weise nicht. Arrays wäre da aus meiner Sicht so eine Utils Klasse.)
Das 's' am Ende ist doch Namenskonvention? Zumindest ist's die im JDK benutzte, Arrays hast du ja selbst genannt, Objects, Collections, ...
 
Das 's' am Ende ist doch Namenskonvention? Zumindest ist's die im JDK benutzte, Arrays hast du ja selbst genannt, Objects, Collections, ...
Also bei dem Thema möchte ich nicht einmal eine feste Position beziehen. Jeder soll es so handhaben, wie er es gerne möchte und ich habe lediglich Optionen aufgezeigt, die ich kenne ohne eine Wertung vornehmen zu wollen.

Was die Namenskonvention angeht: Mir ist da keine offizielle Reglung bekannt, die drauf eingeht. Was mir diesbezüglich derzeit bekannt ist und was relativ "offiziell" ist durch Quelle oder Anzahl der Bezüge, dich ich kenne:
- Von Oracle kenne ich:
a) https://www.oracle.com/technetwork/java/codeconventions-150003.pdf
b) https://www.oracle.com/technetwork/java/codeconvtoc-136057.html
c) https://www.oracle.com/technetwork/java/codeconventions-135099.html
- Von Google kenne ich: https://google.github.io/styleguide/javaguide.html

Diese gehen da nicht im Detail drauf ein (So ich nichts übersehen habe, will ich nicht ausschließen). Bei einer Bewertung würde ich nur mit in betracht ziehen:
- Das mit dem "s" wäre im Java Framework auch nicht konsequent durchgezogen. Siehe z.B. java.lang.Math.
- Oracle schreibt, dass keine Abkürzungen verwendet werden sollen, außer sie sind wirklich üblich. Beispiele sind da sowas wie HTML, URL und so. Da ist jetzt die Frage, ob "Util" üblich ist oder nicht. Aber Oracle hat ja auch einen Namespace java.util - somit scheint es durchaus üblich zu sein.

Aber ganz wichtig ist mir: Der Punkt a ist nur eine kleine Denkanregung. Mir geht es vor allem um Punkt b)! Diesbezüglich sollte das Design evtl. überdacht werden! (Aber auch das nur als Denkanregung. Das darf jeder so machen, wie er es möchte. Das ist hoffentlich in meinem "Disclaimer" auch deutlich geworden!) Bezüglich wieso weshalb warum wird man per google genug fündig - z.B. https://www.vojtechruzicka.com/avoid-utility-classes/
 
Also bei dem Thema möchte ich nicht einmal eine feste Position beziehen. Jeder soll es so handhaben, wie er es gerne möchte und ich habe lediglich Optionen aufgezeigt, die ich kenne ohne eine Wertung vornehmen zu wollen.
[...]
Aber ganz wichtig ist mir: Der Punkt a ist nur eine kleine Denkanregung. Mir geht es vor allem um Punkt b)! Diesbezüglich sollte das Design evtl. überdacht werden! (Aber auch das nur als Denkanregung. Das darf jeder so machen, wie er es möchte. Das ist hoffentlich in meinem "Disclaimer" auch deutlich geworden!) Bezüglich wieso weshalb warum wird man per google genug fündig - z.B. https://www.vojtechruzicka.com/avoid-utility-classes/
Sollte auch nur ne kleine Anmerkung am Rande sein, grundsätzlich stimm ich den sonst auch allem vorbehaltlos zu :)

Was die Namenskonvention angeht: Mir ist da keine offizielle Reglung bekannt, die drauf eingeht. Was mir diesbezüglich derzeit bekannt ist und was relativ "offiziell" ist durch Quelle oder Anzahl der Bezüge, dich ich kenne:
- Von Oracle kenne ich:
a) https://www.oracle.com/technetwork/java/codeconventions-150003.pdf
b) https://www.oracle.com/technetwork/java/codeconvtoc-136057.html
c) https://www.oracle.com/technetwork/java/codeconventions-135099.html
- Von Google kenne ich: https://google.github.io/styleguide/javaguide.html

Diese gehen da nicht im Detail drauf ein (So ich nichts übersehen habe, will ich nicht ausschließen). Bei einer Bewertung würde ich nur mit in betracht ziehen:
- Das mit dem "s" wäre im Java Framework auch nicht konsequent durchgezogen. Siehe z.B. java.lang.Math.
- Oracle schreibt, dass keine Abkürzungen verwendet werden sollen, außer sie sind wirklich üblich. Beispiele sind da sowas wie HTML, URL und so. Da ist jetzt die Frage, ob "Util" üblich ist oder nicht. Aber Oracle hat ja auch einen Namespace java.util - somit scheint es durchaus üblich zu sein.
Bekannt ist mir da auch keine Konvention, nur halt wie implizit im JDK gemacht wird. Google macht’s zumindest in guava auch so, wundert mich, dass die das nicht in ihren StyleGuide mit aufgenommen haben.

Und persönlich gefällt mir die „Plural statt Singular“-Variante auch deutlich besser als Helper oder Util am Ende, wobei das natürlich nur persönliche Präferenz ist.
Finde ich in den meisten Fällen aber auch nur schön, wenns dazu eine passende normale Klasse gibt, wie zB bei Objekts oder Collections.

Math und Arrays würd ich da sogar als die berühmten Ausnahme von der Regel sehen. Maths klingt einfach doof und das Math-Pendant würde fehlen, bei Arrays fehlt zwar das Array-Pendant (bzw, macht was völlig anderes), Array ohne 's' wäre aber auch kein passender Name.

Unschön gelöst finde ich das bei StreamSupport, da hätte ich einfach nur Streams deutlich schöner gefunden.
 
Erstmal danke soweit.

@JustNobody
Zu a)
Du hast Recht, die Methode(n) sollten statisch sein.

Zu b)
Du meinst, so wie es eine Klasse Double und Integer gibt, soll ich eine Klasse SINumeric bauen und benutzen?
Hm...die Methode hab ich eigentlich nur geschrieben, um Benutzerein- und -ausgaben handlicher zu machen. Eine einfache Konvertierung von String nach Double und umgedreht.
Das Ganze in eine Klasse gießen...ja, könnte man machen. Aber momentan sehe ich keinen Vorteil, ich denke, bisher könnte es sogar geringfügig umständlicher werden. Aber das könnte später mal anders werden.
 
Mal über ein paar Klassen drübergeschaut und zumindest einen Fehler gefunden :) (Ja ich mache gerne Code-Reviews, auch auf der Arbeit)

CSVExporter.java:
Java:
        //2. remove all double entries
        i = 0;
        while (i < allPropertienames.size()) {
            j = i + 1;
            while (j < allPropertienames.size()) {
                if (allPropertienames.get(i).equals(allPropertienames.get(j))) {
                    allPropertienames.remove(j);
                    continue;
                }
                j++;
            }
            i++;
        }
Der Code klappt nicht. Probier es mal mit einer Liste wo 3x das gleiche drin ist aus, dann sollte am Ende eine Liste mit 2x dem gleichen übrigen bleiben:

Problem ist, sobald du ein Element entfernst, verschieben sich die restlichen nach links. Das heißt, es gilt für mein Beispiel

size=3
i=0
j=1
=> Duplikat gefunden, Element am Index 1 wird gelöscht.
Dann wird j hochgezählt, geht also auf 2. size der Liste hat sich nun aber auf 2 geändert und die innere Schleife bricht ab.

Kann man unter anderem lösen, in dem man vor das continue ein j--; schreibt.

Aber einfacher ist:
Java:
allPropertienames = allPropertienames.stream().distinct().collect(Collectors.toList());
 
Trotzdem vielen Dank für den Hinweis, bzw. daß du da so genau drüberschaust. :)

Allerdings hab ich den CSV-Export noch gar nicht implementiert, fällt mir da gerade so ein. Da gibt es aktuell noch keine Befehle für, und keine Bedienelemente.
 
Zu jCLSLibarys:

  • Die Package-Ordner sind immer klein ,-Ordner aber in CamelCase geschrieben, zb EngineeringUtils vs engineeringutils. Da sollten beide klein geschrieben sein :)
  • SIPrefixes enthält die übersetzen Namen direkt im enum. Bei nur drei mag das noch klappen, besser wäre aber da einfach nur den Englischen Begriff (oder sogar nur die Kurzform) zu hinterlegen, und den Rest aus entsprechenden Sprach-Dateien zu laden.
  • Lang und MultiLangSupport würde ich wenn möglich durch Dinge aus dem JDK ersetzten, zB Locale und ResourceBundle
  • Das Format dort ist auch ... kreativ, warum hast du nicht eines der üblichen genommen? z.B. normale Properties mit de_DE.properties-Benennung, dürfte den meisten bekannt sein, ist mMn deutlich besser lesbar ([500] als key ist unpraktischer help) und hat bessere Tooling-Unterstützung
  • Der Test dazu besteht ja zum großen Teil aus dem erzeugen der statischen Datei, die könntest du einfach direkt als Datei erstellen (in src/test/resources), dann fällt der ganze Bloat weg
  • Wenn du trotzdem Dateien erzeugen musst, nimm File.createTempFile dafür, dann wird das aktuelle Verzeichnis nicht zugemüllt
  • Und zum Inhalt der Tests: Ich würde die Tests deutlich kleiner halten. Aktuell hast du sieben Assertions im ersten Test, schlägt davon die erste Fehl, werden alle anderen gar nicht erst getestet (alternativ könnte man da auch mit zb SoftAssertions aus AssertJ arbeiten, das löst zumindest das Problem). Soweit ich das sehe bieten sich da auch Paremtrized Tests Parameterized Tests an, dann reduziert sich der Test auf wenige Zeilen.
Aber einfacher ist:
Java:
allPropertienames = allPropertienames.stream().distinct().collect(Collectors.toList());
Spricht dort irgendwas gegen ein Set und die 17 Zeilen dann einfach ersetze durch sowas?
Java:
        for (Component component : components) {
            allPropertienames.addAll(component.getPropertyNames());
        }
 
Bzw, das ganze Bauen des Headers ersetzen durch:

Code:
Set<String> allPropertienames = new LinkedHashSet<>();
        
for (Component component : components) {
    allPropertienames.addAll(component.getPropertyNames());
}

String tmp = String.join(columnSeparator, allPropertienames);
 
  • Die Package-Ordner sind immer klein ,-Ordner aber in CamelCase geschrieben, zb EngineeringUtils vs engineeringutils. Da sollten beide klein geschrieben sein :)
  • SIPrefixes enthält die übersetzen Namen direkt im enum. Bei nur drei mag das noch klappen, besser wäre aber da einfach nur den Englischen Begriff (oder sogar nur die Kurzform) zu hinterlegen, und den Rest aus entsprechenden Sprach-Dateien zu laden.
Ja, da hast du Recht. Das kommt gleich mal auf meine Liste.

  • Lang und MultiLangSupport würde ich wenn möglich durch Dinge aus dem JDK ersetzten, zB Locale und ResourceBundle
  • Das Format dort ist auch ... kreativ, warum hast du nicht eines der üblichen genommen? z.B. normale Properties mit de_DE.properties-Benennung, dürfte den meisten bekannt sein, ist mMn deutlich besser lesbar ([500] als key ist unpraktischer help) und hat bessere Tooling-Unterstützung
Wenn ich mich recht erinnere war das genau der Grund, weshalb ich mich hier angemeldet habe. Und ja, auf die .properties bin ich da auch schon hingewiesen worden.
Ich habe mich dann allerdings doch dafür entschieden, einen anderen Weg zu gehen. Einerseits möchte ich, daß sich jeder möglichst einfach eine eigene Sprachdatei schreiben und das Programm für sich übersetzen kann. Auch ohne Java-Kenntnisse oder das Programm kompilieren zu müssen, außerdem sollte die Sprache im Programm auch änderbar sein.
Und ich habe nicht gesehen, wie das mit den .properties-Dateien funktionieren soll, und habe daher selber etwas selbst gestrickt.

Sofern ich den Weg mit den .properties-Dateien nicht falsch eingeschätzt habe: Was sollte ich ändern, daß das Format "üblicher" erscheint?

Vielen Dank für den Tipp mit File.createTempFile, das kannte ich bisher noch nicht.
 
Und ich habe nicht gesehen, wie das mit den .properties-Dateien funktionieren soll, und habe daher selber etwas selbst gestrickt.
Die properties-Files sind ganz einfache Key-Value-Paare. Du kannst z. B. in bundle.properties schreiben:
Code:
SIPrefix.Y=yotta
SIPrefix.Z=zetta
...
In bundle_de.properties kommen dann die Übersetzungen ins Deutsche rein:
Code:
SIPrefix.Y=Yotta
SIPrefix.Z=Zetta
...
und in bundle_ru.properties:
Code:
SIPrefix.Y=иотта
SIPrefix.Z=зетта
...
Verwendung:
Java:
ResourceBundle labels = ResourceBundle.getBundle("das.paket.mit.dateien.bundle", locale);
...
String text = labels.getString("SIPrefix.Y");
Liefert die Übersetzung SIPrefix.Y in der Sprache entsprechend der locale.
 
Wenn ich mich recht erinnere war das genau der Grund, weshalb ich mich hier angemeldet habe. Und ja, auf die .properties bin ich da auch schon hingewiesen worden.
Ich habe mich dann allerdings doch dafür entschieden, einen anderen Weg zu gehen. Einerseits möchte ich, daß sich jeder möglichst einfach eine eigene Sprachdatei schreiben und das Programm für sich übersetzen kann. Auch ohne Java-Kenntnisse oder das Programm kompilieren zu müssen, außerdem sollte die Sprache im Programm auch änderbar sein.
Und ich habe nicht gesehen, wie das mit den .properties-Dateien funktionieren soll, und habe daher selber etwas selbst gestrickt.

Sofern ich den Weg mit den .properties-Dateien nicht falsch eingeschätzt habe: Was sollte ich ändern, daß das Format "üblicher" erscheint?
Grundsätzlich funktioniert das nicht anders als dein jetziger Weg.
Statt deinem eigenem Format sind die Dateien im properties-Format, im einfachsten Fall sieht das so aus:
Code:
help=Hilfe
menu=Menü
Und statt deiner bisherigen Benennung wäre das im Format en.properties, de_DE.properties, de_CH.properties, etc, entsprechend ISO 639 und 3166. (Je nach Library mit leichten Unterscheiden bzgl Präfix und Default-Language, wie in @mihe7's Kommentar :) )
 
Das wechseln der Sprache, während das Programm läuft, musst du schon irgendwie implementieren, aber das musst du bei der aktuellen Variante ja auch.

Üblicherweise Locale neu setzen, ResourceBundle laden und das ganze an alles weitergeben, was irgendwas anzeigt. Kann man natürlich beliebig abstrahieren.
 
Passende Stellenanzeigen aus deiner Region:

Neue Themen

Oben