vertretungsplanme / substitution-schedule-parser

Java library for parsing schools' substitution schedules. Supports multiple different systems mainly used in the German-speaking countries, including Untis, svPlan, and DAVINCI
https://vertretungsplan.app
Mozilla Public License 2.0
39 stars 8 forks source link

Das Projekt benötigt ein massives Refactoring #56

Open Ditscheridou opened 6 years ago

Ditscheridou commented 6 years ago

Hi,

ich hab mich die letzten Tage mal versucht in das Projekt einzuarbeiten und mir sind einige Sachen aufgefallen, die angegangen werden müssen, weil ansonsten die technischen Schulden bald nicht mehr vertretbar sind.

  1. nicht mit JSONObject arbeiten --> es gibt mit GSON und Jackson genug Möglichkeiten der Serialisierung von JSON-Daten.
  2. strikte Trennung von Logik und Datenhaltung. --> das Projekt scheint mir nicht nach dem Domain-Modell aufgesetzt worden sein, deswegen wüsste ich nicht wieso z.B. eine SubstitutionSchedule Anwendungslogik enthalten sollte.
  3. Trennung von Parser und HTTP-Schnittstelle --> es ist nicht die Aufgabe eines Parsers die Daten sich irgendwo herzuholen, das macht ein REST-Controller. Das in einer abstrakten Schicht zu verstecken ist architektonisch falsch
  4. Enums benutzen für API Typen
  5. Entfernen von Apache Commons Lang 3 --> die Bibliothek ist nicht mehr zeitgemäß, grade durch das Fehlen von Generics. Viele Zeilen Code können hier gespart werden indem man auf Spring Web/Spring Boot migriert.
  6. Entwicklung einer einfach erweiterbaren Datenstruktur für den Parser --> Die Daten scheinen momentan einfach nur mit Regex Ausdrücken rausgeparst und in Objekten weggespeichert zu werden. Was ist aber wenn man diese mal erweitern muss? Dann steigt man kaum noch durch was wohin gehört.

Ich würd das Refactoring gerne angehen, hab aber moment noch Probleme zu verstehen wie der Parser funktioniert, da die Unit Tests nicht wirklich aussagekräftig sind. Vielleicht könnt ihr mir da helfen?`

VG Didi

johan12345 commented 6 years ago

Hallo,

entschuldige bitte die späte Antwort. Mir ist auch klar, dass der Parser an einigen Stellen in Unordnung geraten ist - einige Teile, wie z.B. der Untis-Parser, existieren seit über 5 Jahren in ähnlicher Form und wurden immer um kleine Korrekturen erweitert ohne dabei viel aufgeräumt zu werden. Eine Überarbeitung ist auf jeden Fall fällig, momentan bin ich neben der Vertretungsplan-App mit einem Umzug und meiner Masterarbeit aber so ausgelastet, dass ich das jetzt gerade nicht stemmen kann. Gedacht hatte ich außerdem daran, den Parser nach und nach von Java auf Kotlin umzustellen, eine deutlich elegantere, aber vollständig mit Java kompatible Sprache, die die App und der Server bereits zum Teil nutzen.

Gerne kannst du hier Vorschläge machen und Verbesserungen angehen - weil unsere App mit über 600 Schulen und der dazugehörige Server weiter wie bisher funktionieren sollten, muss aber natürlich sichergestellt werden, dass dabei keine Änderungen in der Funktionalität entstehen - die Unit Tests sind zwar vorhanden, aber noch längst nicht vollständig - und dass die nötigen Änderungen am Server, der die Library benutzt, um die Pläne einzulesen, klein bleiben.

Ich kommentiere hier unten mal deine einzelnen Punkte:

nicht mit JSONObject arbeiten --> es gibt mit GSON und Jackson genug Möglichkeiten der Serialisierung von JSON-Daten.

Hier gibt es zwei unterschiedliche Fälle, in denen momentan JSONObject eingesetzt wird:

strikte Trennung von Logik und Datenhaltung. --> das Projekt scheint mir nicht nach dem Domain-Modell aufgesetzt worden sein, deswegen wüsste ich nicht wieso z.B. eine SubstitutionSchedule Anwendungslogik enthalten sollte.

Neben Gettern und Settern, hashCode und toString hat SubstitutionSchedule noch die Filtermethoden und das addDay.

Wenn man die Filter auslagern will, müsste das irgendwie so eine Utilities-Klasse sein, was wiederum nichts mehr mit Objektorientierung zu tun hat und die public API weniger intuitiv macht (SubstitutionScheduleFiltering.filter(schedule, ...) statt schedule.filtered...) . In Kotlin könnte man das als Extension function machen, dann wäre es im Code getrennt, aber trotzdem genauso aufrufbar.

addDay stellt sicher, dass die Tage des Vertretungsplans immer sortiert sind, dass der Stand des Gesamtplans (lastChange) immer dem jüngsten Stand eines einzelnen Tags entspricht, und dass Tage mit gleichem Datum zusammengeführt werden (addOrMergeDay). Eleganter wäre es vielleicht, das über eine Unterklasse von List zu lösen, aber ich sehe nicht, wieso das aus der Datenstruktur herausgenommen werden sollte.

Trennung von Parser und HTTP-Schnittstelle --> es ist nicht die Aufgabe eines Parsers die Daten sich irgendwo herzuholen, das macht ein REST-Controller. Das in einer abstrakten Schicht zu verstecken ist architektonisch falsch

Weil jedes Vertretungsplansystem seine Daten unterschiedlich bereitstellt, ist es nötig, auch die HTTP-Schnittstelle je API-Klasse einzeln zu implementieren. Zum Teil müssen auch erst einzelne Dateien eingelesen werden, um herauszufinden, wo weitere Daten abzurufen sind (z.B. bei Untis Monitor-Plänen mit mehreren Seiten, die über HTML ´´-Tags umgeblättert werden). Somit ist es schwierig, das komplett zu trennen. Was aber wünschenswert wäre, ist die Möglichkeit der Parallelisierung von HTTP-Requests und evtl. sogar auch dem darauffolgenden Parsen in den Fällen, wo die eben unabhängig voneinander möglich sind und eben nicht erst das Ergebnis vom vorherigen abgewartet werden muss, um die URL für den nächsten zu kennen (also wenn zum Beispiel von vornherein eine Liste von URLs für mehrere Seiten des Vertretungsplans übergeben wurde).

Enums benutzen für API Typen

Hier meinst du das api-Feld von SubstitutionScheduleData, richtig? Hier könnte man eventuell sogar direkt eine Referenz auf die Klasse des zu nutzenden Parsers nehmen, statt hier ein neues Enum einzuführen. Da die SubstitutionScheduleData auf dem Server aber auch serialisiert in der DB gespeichert wreden, muss es aber entsprechend eine Jackson-Anbindung geben, die in der JSON-Darstellung daraus wieder Strings macht und umgekehrt.

Entfernen von Apache Commons Lang 3 --> die Bibliothek ist nicht mehr zeitgemäß, grade durch das Fehlen von Generics. Viele Zeilen Code können hier gespart werden indem man auf Spring Web/Spring Boot migriert.

Commons Lang habe ich hier nur für EqualsBuilder und HashCodeBuilder genutzt, und in einem Fall für die Funktion StringEscapeUtils.escapeHtml4. Spring ist ein Web-Framework, von daher verstehe ich gerade nicht, wie das diese Einsatzzwecke ersetzen kann. Davon abgesehen wäre das ein Wechsel von Pest (487 KB JAR für 3 genutzte Methoden) zu Cholera (908 KB für Spring-Boot plus viele Dependencies), besser wäre es wohl, zwei kleine Bibliotheken genau für diese beiden Zwecke zu finden. In bestimmten Fällen (bei "data classes") werden equals und hashCode in Kotlin sowieso hinfällig.

Entwicklung einer einfach erweiterbaren Datenstruktur für den Parser --> Die Daten scheinen momentan einfach nur mit Regex Ausdrücken rausgeparst und in Objekten weggespeichert zu werden. Was ist aber wenn man diese mal erweitern muss? Dann steigt man kaum noch durch was wohin gehört.

In den meisten Fällen basieren die Parser nicht auf Regex, sondern JSoup (für die Vertretungspläne, die aus HTML- oder XML-Dateien eingelesen werden). Der Witz am substitution-schedule-parser ist gerade, dass er die verschiedenen Vertretungspläne in ein gemeinsames Datenformat (also die SubstitutionSchedule-Klasse) übersetzen kann. Natürlich werden nicht bei jedem Plan alle möglichen Felder in Substitution ausgefüllt, das hängt immer davon ab, welche Software die Schule nutzt (also welche API-Klasse dann zum Einlesen nötig ist) und welche Informationen die Schule evtl. ausgeblendet hat (z.B. aus Datenschutzgründen die Lehrernamen auf dem Plan für Schüler). Wenn irgendwann für eine neue API-Klasse ein neues Feld in Substitution benötigt werden sollte, kann das einfach hinzugefügt werden, da sehe ich jetzt kein großes Problem. Die App zeigt, abgesehen von der Detailansicht, allerdings immer nur Klasse (classes) bzw. Lehrer (teachers/previousTeachers), Stunde (lesson), Typ des Eintrags (type) und den über die SubstitutionTextUtils generierten Beschreibungstext an. In den meisten Fällen sollten neue Felder sollten entsprechend bei der Erzeugung des Beschreibungstextes berücksichtigt werden.

Viele Grüße, Johan

Ditscheridou commented 6 years ago

Das größte Probleme sehe ich aktuell in den Unit-Tests, wie du es angesprochen hast. Normalerweise ist das mein Ansatzpunkt, um mir Open Source Projekte anzuschauen, weil es der leichtere Einstieg ist. Dadurch dass die Testabdeckung hier fast gar nicht vorhanden ist, ist das ein wenig schwierig....

Ich glaube bei den anderen Punkten reden wir ein bisschen aneinander vorbei bzw. war mir manches nicht direkt ersichtlich. Wenn du Testdaten bereitstellen kannst für die verschiedenen Parser und entsprechende Grundlagentests implementieren kannst wäre das mehr als hilfreich

VG Jonas

johan12345 commented 6 years ago

Testdaten bereitstellen kannst für die verschiedenen Parser und entsprechende Grundlagentests implementieren kannst

Für fast alle Parser sind Testdaten schon hier. Dort fehlen nur SchoolJoomla (kommt sehr selten vor, kenne momentan keine Instanz die ohne Passwortschutz aufrufbar ist), Turbo-Vertretung (gehört inzwischen der selben Firma wie svPlan, daher lassen sich die Pläne von den aktuellen Versionen von Turbo-Vertretung auch mit SVPlanParser einlesen - die beiden Parser könnten also zusammengelegt werden), und ein paar Varianten von Untis (Untis-Monitor Beispiel, Untis-Substitution Beispiel). Bei den Parsern für die schon Testdaten drin sind sind auch entsprechende Integration-Test (die "...DemoTest") implementiert.

johan12345 commented 6 years ago

Ah, mir fällt gerade auch auf dass in der README für die meisten Parser schon Beispiele verlinkt sind.

DreiDe commented 5 years ago

Wobei dort auch schon vier Links nicht mehr funktionieren/aktuell sind: svPlan, DaVinci, LegionBoard und Indiware.

johan12345 commented 5 years ago

Wobei dort auch schon vier Links nicht mehr funktionieren/aktuell sind: svPlan, DaVinci, LegionBoard und Indiware.

Das sollte jetzt behoben sein.