Warum Pullrequests nicht immer der passende Entwicklungsmodus sind
Eine von mir lange gehaltene Überzeugung, die ich hier endlich aufschreiben werde: Manchmal profitieren Softwareprojekte davon, wenn die Entwicklungsarbeit nicht per Pullrequests eingesendet wird, sondern wenn die Entwickler direkt den Masterbranch ändern. Es erleichtert das Testen und ermächtigt Entwickler.
Erklärungen vorab
Programmierern dürfte der Großteil in diesem Abschnitt klar sein, aber um so viele mitzunehmen wie möglich und Missverständnisse zu vermeiden:
Bei der Arbeit an Software wird inzwischen fast immer Git als Versionsverwaltung eingesetzt. Im üblichen und meiner Erfahrung nach von Firmen ausschließlich genutzten Nutzungsmodus erstellen Entwickler für ihre Aufgaben immer neue Kopien als Grundlage für ihre Änderungen (Branches, dann Featurebranches genannt), die sie dann als Pullrequest an das Projekt schicken. Ein anderer Entwickler prüft die Änderungen in einem mehr oder weniger ausgiebigen Codereview, danach werden sie dem gemeinsamen Entwicklungsbranch hinzugefügt. Wenn zwischendurch andere Codestellen verändert wurden kann oft genug Git die Änderungen zusammenbringen, gibt es aber einen direkten Konflikt müssen die Entwickler diesen manuell auflösen (und entsprechend testen). Diese Nutzungsweise gibt es in mehreren Varianten, eine davon wird Gitflow genannt, nutzt permanent mehrere Branches (mindestens master und dev) und war lange ziemlich populär.
Beim kürzlich wiederaufgekommenen Trunk-based development – vom Namen her ein Verweis auf Entwicklungsweisen vor Git, so war Trunk der Name für den Hauptbranch beim alten Versionsverwaltungssystem CVS – entfallen laut der verlinkten Definition die mehreren Branches, alles liefe schnell im Master zusammen. Aber es gäbe so wie sich das dort liest immer noch die PRs. Laut anderen Definitionen entfallen die ganz, senden Entwickler ihre Änderungen direkt in den gemeinsamen Masterbranch. Und dann sind wir bei meinem Thema.
Vorteile davon, ohne PRs zu arbeiten
Es sind dabei vor allem zwei Punkte, die ich beim direkten Ändern des Masterbranches positiv sehe.
Und zwar primär (und nur tautologisch klingend), dass Änderungen direkt im Masterbranch landen. Das macht sie sichtbarer: Denn anstatt in einem eigenen Branch auf Reviewer und Tester zu warten, wird in manchen Projekten der neue Code im Masterbranch viel eher wahrgenommen. Nämlich immer dann, wenn einzelne Nutzer nicht auf Releases warten, sondern den Code direkt nutzen. Das machen zum Beispiel bei Serendipity ein paar Nutzer mit ihren Blogs. Die akademische Literatur zu FOSS würde sie als Nutzerchampions bezeichnen: Als Nutzer, deren Engagement über das normale Maß hinausgeht und die so die Entwicklung beeinflussen, ohne notwendigerweise selbst Code zu schreiben. Der Masterbranch (aber eben nur der) wird durch sie ausgiebig getestet und Probleme an die Entwickler gemeldet.
Genauso stolpern auch andere Entwickler direkt über den neuen Code, wenn er direkt im Master landet. Es besteht kaum mehr die Gefahr, dass Arbeit gedoppelt wird oder zwei Entwickler in ihren eigenen Branches inkompatible Systeme bauen. Auch Mergekonflikte werden seltener.
Dieser Vorteil wird nochmal wichtiger, wenn PRs lange liegenbleiben können. Wieder Serendipity als Beispiel, das jQuery-Upgrade wäre garantiert nicht fünf Jahre liegengeblieben wenn sein Ansatz schnell im Master gelandet wäre. Wir hätten so bemerkt, dass die gröbsten vermeintlichen Inkompatibilitäten gar nicht an der Javascriptseite hingen (zumindest ist das mein derzeitiger Stand). Das ist übrigens kein Problem nur mit freien Projekten, auch in Firmen mit proprietärer Software habe ich mehrfach erlebt, dass PRs lange liegenbleiben und teils sogar verrotten, also am Ende nicht mehr gebraucht werden konnten weil sich um sie herum zu viel geändert hatte.
Zweitens ist das direkte Ändernkönnen des Masterbranches eine Ermächtigung des Entwicklers. Es bedeutet, dass das Projekt zumindest soweit auch ihm gehört, dass er nicht jede Änderung von anderen absegnen lassen muss. Wenn mal etwas nicht passt, kann das später immer noch geändert werden – bei einer vertrauensvollen Zusammenarbeit eines fähigen Teams ist das auch für niemanden ein Problem. Mit PRs als notwendigen Zwischenschritt ist stattdessen immer die Zustimmung eines Reviewers notwendig (wobei diese Anforderung formell oder informell sein kann), der einzelne Entwickler daher immer sehr viel abhängiger von anderen. Keine gute Voraussetzung, um sich als (Mit-)Entscheider zu fühlen.
Diese Abhängigkeit ist natürlich wieder besonders problematisch, wenn PRs liegenbleiben können. Das muss ja – anders als beim Beispiel mit jQuery oben – gar nicht an konstatierten Problemen liegen, sondern in vielen FOSS-Projekten ist es schlicht Zeitmangel. Und selten ist ein eigener PR den anderen genau so wichtig wie einem selbst. Dann wird die Abhängigkeit von den anderen zu einer frustrierenden Blockade der Entwicklungsarbeit.
Tatsächlich würde (und tue) ich mischen
Pragmatisch ist das Mischen von beiden Ansätzen, und ich fahre damit schon eine Weile meiner Wahrnehmung nach ganz gut. Während das komplizierte Branchmanagement von Gitflow generell abzulehnen ist, haben Pullrequests klare Vorteile. Und die kann man auch dann nutzen, wenn man sonst direkt in den Masterbranch pusht: Immer, wenn Änderungen Abstimmungsbedarf haben, macht man dann eben doch einen PR auf. Zum Beispiel wenn die zu nutzende Version der Programmiersprache sich ändert und dafür große Codeänderungen nötig sind. Sowas landet in einem PR, damit die anderen Entwickler draufschauen und bei der Migration helfen können.
Und wenn mal solch ein PR zu lange braucht und das die eigene Motivation zu ersticken droht? Dann drückt man eben selbst auf den Mergebutton, akzeptiert den eigenen PR auf eigene Kappe. Die anderen hatten dann wenigstens eine Chance gehabt, vorher Rückmeldung zu geben. Sollten in den späteren Tests die Änderungen Probleme machen kann man so oder so immer noch entscheiden, ob an ihnen weitergefeilt werden sollte oder ob sie zurückzunehmen sind.
Klar: Wenn ein Entwickler von außerhalb des Projekts Änderungen einsenden will – wie es bei FOSS-Projekten hoffentlich häufig geschieht – dann wird es völlig unabhängig von allen Überlegungen der internen Arbeit ein PR, der zu reviewen ist. Will man auch in dem Szenario trotzdem PRs soweit möglich vermeiden kann man so jemanden ja schnell ins eigene Team einladen, das mit dem direkten Zugriff auf den Masterbranch entwickelt sich dann meist nach etwas Zögern ganz natürlich.
Das ganze ist ein Balanceakt zwischen Qualität und Entwicklungsgeschwindigkeit, Motivation des Einzelnen und Einflussnahmemöglichkeiten der Gruppe. Man kann damit auf die Nase fallen, man kann aber auch schmerzhafte Blockaden lösen und Projekte besser am Laufen halten. Wichtig ist bei solchen Freiheiten besonders, dass das Entwicklungsteam sich ziemlich vertraut.
Auch ist es wichtig, dass keiner im Team ist, der vor sich hinstümpert und dauernd unlesbaren Code direkt in den Master pusht, am besten noch ohne erkennbare Commitstruktur. Solches Verhalten würde den Ansatz natürlich direkt kaputtmachen und dann braucht es unbedingt die Struktur von PRs und Codereviews, um das Projekt zu schützen. Wobei solche Entwickler dann meiner Erfahrung nach auch mit Codereviews nicht zu bändigen sind, aber das ist ein Thema für sich.
Eure Erfahrungen würden mich interessieren: Habt ihr selbst schon in Teamprojekten ohne PRs gearbeitet, und hat es gut funktioniert?
Acht Kriterien für Programmabhängigkeiten
Ob im Tagesjob oder für eigene Projekte, welche Abhängigkeiten (Module, Gems, libs, …) für Projekte gewählt wurden hat einen massiven Einfluss auf die spätere Entwicklung und besonders die Instandhaltung der Software. Während ich mir zu Beginn dazu kaum Gedanken gemacht habe, ist mittlerweile fast schon ein Regelwerk im Kopf entstanden. Teils aus der Mischung der positiven und den schmerzhaft negativen Erfahrungen, teils aus offiziellen Regeln für Qualitätskriterien auf der Arbeit.
So denke ich derzeit:
0. Ist die Abhängigkeit notwendig?
Die oberste Direktive für Notwendigkeit ist Aufwandsminimierung. Nur wenn sie insgesamt Aufwand minimiert kann eine Abhängigkeit positiv wirken. Also nur wenn sie etwas umsetzt, was mit eigenen Code nicht gegangen wäre oder viel Arbeit gekostet hätte. Entsprechend ist die erste Frage, ob die Abhängigkeit funktionell sinnvoll und wie notwendig sie ist.
Warum diese Aufwandsperspektive? Weil jede Abhängigkeit potentiell Folgearbeit bedeutet. Wann immer eine neue Version herauskommt, die Anpassungen am eigenen Code einfordert, muss nur wegen der Abhängigkeit Arbeit investiert werden. Und das ist dann praktisch immer verlorene Arbeitszeit, die das Projekt an sich nicht weiterbringt. Aber es kann auch andersherum sein: Wenn durch die Einbindung einer Abhängigkeit eine stete Folgearbeit ausgelagert werden kann, wie die Anpassung an eine sich immer wieder ändernde API, spricht viel für die Einbindung. Das Szenario ist aber selten.
Ein Beispiel für diese Überlegungen ist der MF2-Parser, den wir für Webmentions in Serendipity eingebunden haben. Da war ich am Zögern, ob so viel fremder Code notwendig ist, könnte man das HTML doch auch auf anderem Wege parsen. Aber der offizielle MF2-Parser ist im Zweifel exakter und vermeidet dann Arbeit, wenn sonst der eigene Code für die verschiedenen Webmention-Formate immer wieder angepasst werden müsste. Er war auch der empfohlene Weg der Umsetzung, daher schlug ich seine Einbindung vor.
Die Erwartungshaltung der benötigten Folgearbeit variiert hier stark je nach Entwicklungssprache und dann nochmal je nach Nische innerhalb der Sprache, ist sie groß genug. Um ein paar Erfahrungswerte zu teilen: Javascript samt Node ist berüchtigt für die kurze Halbwertszeit und den wahnsinnigen geforderten Aufwand; bei Flutter beziehungsweise Dart gab es da noch keine etablierte Kultur, sondern variierte es wohl je nach Entwicklerhintergrund; Module für Ruby und PHP (hier im Gegensatz zur Sprache selbst) sind normalerweise respektvoll.
1. Welche Abhängigkeiten hat die Abhängigkeit?
Es empfiehlt sich einen Blick auf die Abhängigkeiten der Abhängigkeit zu werfen. Je mehr das sind, desto eher können durch sie neue Probleme entstehen. Ob das Interaktionen mit dem eigenen Code sind oder, wahrscheinlicher, wegbrechende oder sich ändernde Projekte Probleme verursachen, die dann über die Abhängigkeit an das eigene Projekt weitergegeben werden.
2. Wie beliebt ist die Abhängigkeit?
Die Beliebtheit kann durch das jeweilige Empfehlungssystem transportiert werden, wie die Sterne bei Github, oder durch Artikel, die das Modul vorstellen und loben. Beliebtheit ist erstmal positiv, die Weisheit der Masse wird nicht völlig unberechtigt ein Projekt hervorheben. Und viel Aufmerksamkeit kann die Chance erhöhen, dass bei einem Einschlafen der Entwicklung sich ein Nachfolgeprojekt bildet.
Gleichzeitig kann Beliebtheit negative Auswirkungen haben, wenn die Entwickler durch einen Nutzeransturm überfordert werden, die gefühlte Popularität ihnen zu Kopf steigt oder schlicht durch die Masse an Anforderungen der Projektfokus verlorengeht.
Gleichzeitig gibt es immer wieder gute Module, die von der Masse ignoriert werden. Oder das technisch bessere Folgeprojekt, das zwar beliebt ist, aber weniger als das früher etablierte. Die Inertia der Masse ist hier eben auch ein Thema. Daher ist Beliebtheit ein Faktor, aber ein schwacher.
3. Ist die Dokumentation brauchbar?
Die Brauchbarkeit der Dokumentation würde ich sehr viel höher hängen. Brauchbarkeit ist hier bewusst gewählt, statt Umfang. Ich habe mehrfach sehr umfangreiche Dokumentation erlebt, die aber überhaupt nicht hilfreich war, weil sie die aufkommenden Nutzungsfragen einfach nicht beantwortete.
Ein gutes Readme kann völlig ausreichen, solange es den Umgang mit der gebotenen API sauber erklärt. Wobei bei vielen Projekten eine ausführlichere Beschreibung der Optionen in einer dedizierten Dokumentation erstrebenswert ist.
4. Wann war das letzte Update?
Das Datum des letzten Updates bestimmt, ob ein Projekt wirklich aktiv ist oder nicht.
Das darf wieder nicht überbewertet werden. Manche Projekte sind stabil und fertig, sie brauchen nur seltene Anpassungen und das lange vergangene Datum des letzten Updates bedeutet kein brach liegendes Projekt, sondern ein stabiles. Genauso kann ein kürzliches Update und ein hohes Updateinterval auf Instabilität und fehlende Entwicklerdisziplin hindeuten. Aber es gibt hier eine gesunde Balance, die das Ideal wäre.
5. Wie viele offene Bugreports und Pullrequests gibt es?
Hier kommt es wieder stark auf den Kontext an.
Ein übervoller Bug- oder Issuetracker ist erstmal ein Alarmsignal. Blöd sind auch sich aufstauende Pullrequests. Beides zeichnet das Bild eines überforderten Projekts, bei dem Probleme der Nutzer nicht angegangen werden.
Aber man darf diesen Faktor auch nicht überbewerten. Bei einem sehr beliebten Projekt beispielsweise werden fast zwangsläufig sehr viele Nutzerberichte zu Problemen zusammenkommen, die kein Entwicklerteam der Welt alle ordentlich bearbeiten kann. Wenn trotzdem auf wichtige Beiträge geachtet wird, die Entwicklung trotzdem läuft, dann ist der volle Bugtracker fast ein gutes Zeichen: Denn dann wurde kein Bot eingesetzt, der automatisch Bugreports schließt, was immer eine feindselige Haltung zur Nutzerschaft auslöst und wichtige Berichte ungesehen verschwinden lässt.
Genauso können sich bei alten Projekten Berichte ansammeln, die nicht wirklich angegangen werden können, die aber auch nicht unpassend sind und daher lange aufbleiben. Wieder, solange die Software und die Entwicklung trotzdem läuft ist das völlig okay.
6. Wie redet der oder reden die Maintainer mit Nutzern?
Das ist fast das wichtigste: Wichtiger als der Zustand des Issuetrackers in absoluten Zahlen ist das Verhalten der Entwickler darin. Werden Nutzer für ihre Berichte abgekanzelt, oder werden sie ernstgenommen und angehört? Werden Nutzer beleidigt, ihre Bugreports ohne verständlichen Grund geschlossen, fehlt einfach Freundlichkeit und Respekt sollte man sich soweit Fernhalten wie möglich.
Dabei lohnt sich auch ein Blick darauf, wer antwortet. Eine häufige Projektkonfiguration ist der erfahrene und freundliche Originalentwickler, dem aber die Zeit fehlt, und dann gibt es eine Nummer 2, die im Bugtracker viele Anfragen beantwortet. Dem Originalentwickler gegenüber wird diese Person immer nett sein, aber es besteht dann leider die Tendenz, gegenüber Nutzern respektlos zu antworten. Wird das geduldet, und wie weit ist diese Entwicklung vorangeschritten? Das wird viel darüber verraten, ob vom Originalentwickler dem Projekt noch Aufmerksamkeit geschenkt wird.
Auch wenn man selbst in diesen Projektkommunikationsmitteln nicht aktiv teilnehmen will ist das ein wichtiger Faktor, denn es wird darüber bestimmen wie gut und verständlich Antworten auf Nutzerfragen sind, ob das Projekt Nutzungsprobleme angeht und ob bei der Weiterentwicklung der Nutzer bedacht wird. Und ein vernünftiger, freundlicher Maintainer gleicht fast alle anderen möglichen Schwachpunkte eines Projektes wieder aus, denn seine Handlungen werden mittelfristig der Einstellung gleichen.
7. Wie wurde mit inkompatiblen Änderungen umgegangen?
Der Einfluss des Maintainers kumuliert im Umgang mit Breaking Changes. Also Änderungen der Abhängigkeit, die Änderungen am eigenen Code erfordern.
Um die Wichtigkeit des Aufwandsmanagements nochmal klarzumachen hilft vielleicht diese Perspektive: Das eigene Team hat X Mann. Wieviele Leute entwickeln an all den Abhängigkeiten? Wieviele entwickeln an der Grundsprache, wenn z.B. wie bei Flutter Google hunderte oder tausende Entwickler beschäftigt? Jeweils sind es viel mehr, also kann das eigene Team die investierte Arbeit der anderen nicht kontern. Das Projekt kann nur erfolgreich sein, wenn das nicht notwendig ist, wenn eben inkompatible Änderungen vermieden werden.
Daher sind sind sie immer ein großer Negativpunkt. Aber manchmal sind Anpassungen an der Schnittstelle eines Moduls schwer zu vermeiden. Sind sie selten genug, werden die nötigen Änderungen gut erklärt und verständlich gerechtfertigt sind sie natürlich akzeptierbar. Auch hilft es, wenn die Versionsnummer nach Semantic Versioning angehoben wird, zumindest in Ökosystemen, bei denen die Updatetools darauf ausgelegt sind. Dann werden wenigstens Aktualisierungszeitpunkte samt ihren Migrationen etwas besser wählbar.
Aber alleine die Nummer korrekt anzuheben reicht eben nicht. Wenn Änderungen einfach so gemacht werden, ohne Rücksicht, ist es Zeit zu rennen. Wenn beispielsweise vorher eine Funktion auf zwei Arten aufgerufen werden konnte, und jetzt eine davon wegfällt nur weil der Maintainer das schöner findet, hat er die Bodenhaftung verloren. Klar, vereinfacht das wesentlich seinen eigenen Code und Aufwand und erklärt er es entsprechend kann auch sowas mal okay sein. Aber grundsätzlich sollte Abwärtskompatibilität eine große Priorität haben. Einfach, weil sonst Abhängigkeit um Abhängigkeit Änderungen einfordern und die verfügbare Arbeitsleistung des eigentlichen Projekts für diese Pflege aufgefressen wird.
Besonders klares Zeichen sind Sprüche wie "Ich muss mich nicht rechtfertigen", wenn bei kaputtmachenden Änderungen der Grund erfragt wird, oder wenn beim Versuch die Problematik zu erklären mit emotionaler Manipulation dem Nutzer die Schuld für das Problem gegeben wird, ein Ansprechen der Problematik zu Aggressionen führt. Beides habe ich wirklich gesehen, kürzlich erst wieder. Von solchen Projekten kann man gar nicht genug Distanz wahren.
Ein weiteres Beispiel, dass Arbeit als Entwickler auch eine soziale Komponente hat.
Man kann diese Regeln ignorieren und Glück haben, man kann ihnen folgen und trotzdem von unnötigen Änderungen gepiesackt werden. Aber ich glaube, grundsätzlich sind sie kein schlechter Ansatz und recht umfassend.
Gibt es etwas, was ich übersehen habe oder was ihr anders seht?
Kühl - Schattenorganisation
Agiles Management und ungewollte Bürokratisierung - mit diesem Untertitel hatte mich Schattenorganisation gepackt. Denn ich war entsetzt darüber und ziemlich ratlos, wie starr und insgesamt negativ Scrum im neuen Entwicklerteam der Käuferfirma umgesetzt wurde, während meine Firma die Methodik zuvor so positiv genutzt hatte. "Ungewollte Bürokratisierung" schien mir dabei genau das Problem zu beschreiben.
Schattenorganisation aber spricht zu diesem Thema nur indirekt. Statt Scrum, Kanban oder anderen gängigen Praktiken in Entwicklerteams wird über die mir vorab völlig unbekannte Holokratie berichtet. Das ist eine Organisationsform für gesamte Organisationen, bei der softwareunterstützt haarklein aufgeschrieben wird welche Rollen es gibt und wie sie arbeiten, daraufhin Arbeiter gerne auch mehreren Rollen zugeordnet werden und das ganze in Kreisen von Rollen organisiert wird, bei denen es keinen klaren Chef gibt. Bilden sich Abweichungen zu den niedergeschrieben Arbeitswegen, sollen diese direkt formal aufgeschrieben werden, die Rollenbeschreibungen, welche Rollen es gibt und wer sie besetzt ändere sich dadurch ständig. Nie wirklich groß, war es wohl doch in entsprechenden Kreisen eine Mode und ein paar Firmen, die man kennt, hatten versucht sich so zu organisieren, beispielsweise Medium.
Kühl zerlegt dieses Konzept in seine Einzelteile. Durch seine Einordnung in organisationswissenschaftliche Modelle und seine Aufarbeitung, wie diese Methode am Ende wirklich gelebt wird (nicht, die informelle Ebene bildet sich immer wieder), blieb von der Holokratie am Ende zumindest bei mir kein positives Bild über. Kühl selbst positioniert sich nicht klar zu dem Konzept, dass diese Wahrnehmung nicht unbeabsichtigt ist kann ich nur mutmaßen.
Für Scrum etc gab mir Schattenorganisation keine klare Antworten, aber am Ende doch ein paar Ideen. Wenn Holokratie durch eine maßlose, detaillierte Bürokratisierung Agilität herstellen will, ist das bei Scrum mit seinen Ritualen und festen Rollen gar nicht sehr anders. Die vermeintliche agile Softwareentwicklung nach Scrum wäre also – so ausgestaltet, wie ich es erlebte – in Wirklichkeit eine stark formalisierte, eben bürokratische Methode; und eben nicht die Annäherung an die "natürliche" Entwicklungsform, die FOSS-Projekte und entwicklerbestimmte Teams ohne formalisierenden Manager im Rücken wählen würden. Und wie es dazu kommen konnte, wie auch dieser Gedankengang von Agilität durch Bürokratie bzw Formalisierung funktioniert, dafür gibt der Blick auf die Holokratie dann eben doch ein paar Einblicke.
Schattenorganisation zu lesen war also keine totale Zeitverschwendung. Angesichts meiner Fragen hätte ich aber zu einem anderen Buch greifen sollen. Ich finde es schade, dass Kühl nicht über die eine Erwähnung im Nebensatz hinaus im Buch selbst den Bogen geschlagen hat, war Holokratie doch auf dem Klappentext nichtmal erwähnt und sind Scrum sowie Kanban die Arbeitsweisen, an die das Stichwort agil direkt denken lässt. Aber vielleicht stimmt das außerhalb meiner Nische eben nicht, oder war das relevante Stichwort hier eben Management, nicht alleine agil.
Mein Rückblick aufs Studium, Teil 1: Bachelor Informatik TU Darmstadt
Ich habe viel Zeit an der Uni verbracht, darüber aber hier nur selten geschrieben. Mittlerweile arbeite ich seit ein paar Jahren und das Studium wird mehr und mehr eine ferne Erinnerung. Zeit, ein paar Erfahrungen festzuhalten.
Der Start war ein Informatikstudium an der TU Darmstadt, das ich vor fast genau einer Dekade abgeschlossen habe. Es war nur ein Bachelorstudium, den Master machte ich anderswo. Die Rahmenbedingungen waren gut: Die TU galt als gute Uni. Auch wenn es noch Diplomstudenten gab – einer davon wurde unser D&D-Dungeonmaster – war der Umstieg vom Diplomsystem mittlerweile fertig, es gab auch keine Wahl mehr, das schuf Fakten und Klarheit. Unsicherheit kam von den asozialen Studiengebühren, doch die wurden mir im ersten Semester erlassen und im zweiten dank Ypsilanti ganz abgeschafft. Das Studium selbst war von Seiten der Universität ziemlich toll organisiert, aber es war viel Arbeit und die Zeit insgesamt für mich ziemlich chaotisch.
Der Ablauf des Studiums
Da es keine Zulassungsbeschränkung gab saß ich am Anfang des Studiums mit sehr vielen anderen neuen Studenten in völlig überfüllten Hörsälen. Am schlimmsten war da Mathe 1, da passten die Leute nichtmal mehr auf die Stufen neben den Stühlen. Das blieb natürlich nicht so: Es würde mich sehr wundern, wenn mehr als 50% das Studium abgeschlossen haben, zudem sparten sich bald selbst viele die weitermachten die Vorlesungen.
Die Übungen waren sowieso wichtiger: In ihnen kam der Stoff vor, der dann in den Prüfungen getestet wurde. Geleitet wurden diese Übungen meist von Studenten, als Nebenjob. Alle meine Prüfungen waren schriftlich, mündliche Prüfungen gab es in anderen Kursen, aber sie waren selten und galten als schwierig. Bei allen regulären Vorlesungen gab es am Ende stattdessen eine schriftliche Prüfung. Ohne irgendeinen Tests an Scheine zu kommen war mit dem Diplomsystem so ziemlich abgeschafft worden, ging aber noch in Seminaren – da wurde dann eine Präsentation bewertet. Das war natürlich der große Stressfaktor: Dass man durch Prüfungen und nach dreimaligen Scheitern an einer Prüfung sogar durch das ganze Studium durchfallen konnte. Zum Glück blieb mir der Drittversuch erspart, aber nicht alle Prüfungen konnte ich auf Anhieb bestehen. Auch deswegen war es die oben erwähnte chaotische Zeit.
Die Massenvorlesungen am Anfang deckten die Grundthemen ab. Grundlagen der Informatik (GDI), Formale Grundlagen der Informatik (FGDI), Technische Grundlagen der Informatik (TGDI), Mathe. Davon gab es jeweils drei Kurse, nur TGDI blieb bei zweien. Nach den ersten Semestern kamen dann Kanonikfächer hinzu, "Einführung in X" – Computational Engineering, Computer Microsystems, Foundations of Computing, Human Computer Systems, Data and Knowledge Engineering und Net Centric Systems.
Gegen Ende gab es noch Wahlpflichtfächer, wo man sich aus einer Auswahl von Themen die seinen raussuchen konnte, das war die minimale Spezialisierungsmöglichkeit. Bei mir wurde das Kryptographie, Mensch-Computer-Interaktion (HCI) und Künstliche Intelligenz. Man musste ein Bachelorpraktikum abliefern, aber was da die Optionen waren krieg ich nicht mehr zusammen. War das mit der Bachelorarbeit kombiniert? Denn die galt es ganz zuletzt zu schreiben, wobei man dafür noch einen Kurs besuchen konnte und dann mit Kommilitonen den Fortschritt besprach, was sehr hilfreich war.
Studiumsinhalte
Worum ging es jeweils? Ich werde nicht alles durchgehen, will aber einen Eindruck der wichtigsten Inhalte geben.
In GDI ging es ums Bauen von Software. Am Anfang schlicht ums Programmieren: Wir lernten erst Scheme (jetzt Racket, ein LISP), dann Java, programmierten zusammen ein Spiel und lernten über die Komplexität von Algorithmen (O-Notation). Und das war alles GDI 1. An spätere Inhalte erinnere ich mich weniger gut. Ein kurzer Abschnitt wurde in C programmiert. Aber wir müssen auch Stoff über Wasserfallmodelle, Pflichtenhefte, Designpatterns und UML gehört haben – das meiste davon war anders als die erste Vorlesung im Nachhinein irrelevant, aber gut zu wissen dass es das mal gab. Spezielle Algorithmen wie Quicksort kamen später auch vor, das war schon hilfreicher, auch Datenstrukturen wie Bäume waren ein wichtiges und nützliches Thema.
FGDI war Logik. Meine Anleitungen um einen Zustandsautomaten zu mimieren und zum Markierungsalgorithmus stammen daher, ob das nun FGDI 1 oder 2 war ist mir unklar. Anfangs ging es um Grundlagen wie Prädikatenlogik. Ich habe es gehasst, die Artikel landeten im Blog um mir Inhalte ins Hirn zu prügeln und anderen zu helfen. Ich muss aber zugeben, dass es später hilfreich und notwendig war solche formalen Logikausdrücke lesen zu können. FGDI 3 war etwas anders: Da ging es um die Verifikation von Programmen. Die wurden dafür in einer Spezialsprache geschrieben und ihre Richtigkeit musste dann Annahme für Annahme bewiesen werden. Auf der einen Seite absurd komplett nicht praxistauglich, auf der anderen konnte man sich vorstellen, wie das irgendwann doch praxistauglich werden könnte, es hieß sogar Prozessorhersteller würden das bereits machen (und später las ich, sie hätten das größtenteils aufgegeben).
TGDI hasste ich nicht, aber diese technischen Grundlagen hatte ich nie gehabt. Und-, Oder-, XOR-Schaltungen, Verilog und damit irgendwas anfangen – ich schnappte ein paar Grundlagen auf und kam durch die Prüfung. Im zweiten Kurs ging es dann mehr um Prozessorarchitekturen. Ob wir hier oder in GDI in Assembler programmiert haben krieg ich nicht mehr zusammen, aber das war auf jeden Fall spaßig.
Bei Mathe kann ich kaum noch auch nur die Inhalt benennen. Mathe 1 und 2 war ein wilder Mischmasch von irgendwelchen Mathematikkenntnissen – irgendwas mit Reihen, Ableitungen, Beweisen. Mathe 3 war Computation (und Statistik?), also Mathematik vom Computer lösen lassen, was oft andere Ansätze braucht. Das hatte etwas Berechtigung. Aber Mathematik wie es dort zuvor gemacht wurde war ein pures Aussiebefach und vermittelte wenig, was irgendwie hängenblieb oder ich bisher nochmal gebraucht hätte. Teilweise wiederholten sich Inhalte in den spezialisierteren Fächern, die waren dann relevant und gut sie schonmal gehört zu haben, Gruppen beispielsweise. Aber hätte es kein Siebfach gebraucht hätte man Mathe schlicht weglassen und die Inhalte dort lehren können wo sie benutzt wurden.
Bei den "Einführung in X"-Fächern ging es um ganz verschiedene Themen. Bis jetzt lernten wir ja nur allgemeine Grundlagen. Net Centric Systems z.B. konnte dann über Netzwerke und über das Internet reden. Es ging auch darum grob die Forschungsbereiche der Informatik abbilden. Die Wahlpflichtfächer gingen da dann später etwas tiefer, für die, die Interesse an einem bestimmten Thema hatten.
Besondere Inhalte und Lektionen
Besonders prägend war Grundlagen der Informatik. Wegen dem Fach mit seinen Inhalten waren die meisten Studenten in dem Studiengang, mich eingeschlossen. Tatsächlich lernte man dort dann auch viele Konzepte, die für jeden Programmierer völlig relevant sind: Objektorientierte und funktionale Programmierung, Datentypen, Rekursion, Komplexität, aber auch generell das Entwerfen von und Arbeiten mit Algorithmen sowie das Planen von Programmen.
Trotz diesem praktischen Aspekt des Studiums wurde gepredigt, dass ein Informatiker Konzepte lernt und die dann in jeder beliebigen Programmiersprache anwenden kann. Ich merkte später: Das stimmt nur so halb. Um abstrakte Konzepte wirklich anzuwenden muss man die Sprache beherrschen, das braucht Übung mit ihr. Bevor ich Ruby lernte – eigenständig im Master – konnte ich zum Beispiel faktisch kaum Serveranwendungen bauen, völlig egal ob ich die Konzepte drauf hatte. Aber es stimmt, dass sich viel übertragen lässt, und zwischen ähnlichen Sprachen zu wechseln ist kein großes Problem. Es stimmt aber auch der Vorwurf am Hochschulstudium, dass selbst das Bachelorstudium noch zu abstrakt ist um den Studenten wirklich das Programmieren beizubringen. Das müssen sie aus eigenem Antrieb zusätzlich oder später machen, selbst GDI mit seinem Praxisteil schafft nur wenige Grundlagen. Oder zumindest war das damals so.
Was stimmte: Die Predigt vom Dekan ziemlich am Anfang des Studiums, dass er keinen von uns in gewöhnlichen Studentenjobs sehen will, wir seien alle jetzt schon als Programmierer beschäftigbar. Naja, vielleicht galt das nicht für alle, aber tatsächlich waren die Anfangsinhalte die wichtigsten und Firmen hätte mit den motivierteren Studenten definitiv arbeiten können.
Wieviel weiteres nützliches man aus dem Studium ziehen konnte hing aber auch von den Wahlpflichtfächern ab. Meine Themenwahl sehe ich heute noch mit Wohlwollen.
Künstliche Intelligenz war vor dem aktuellen Boom, neuronale Netze ein veraltetes Nischenthema. Trotzdem oder gerade deswegen war die Vorlesung super – denn KI hat im Kern das Thema, das mich an Informatik anfangs so faszinierte, nämlich wie man mit Algorithmen Probleme lösen kann. Der auf einem Infotag präsentierte Dijkstra-Algorithmus, der ein Wegfindeproblem lösen kann, hatte damals erst den Ausschlag gegeben dieses Studium zu wählen. Ich sehe KI als die komprimierte Essenz der Informatik, entsprechend spannend war die Vorlesung. Wobei ich wenig konkrete Inhalte aus ihr bisher anwenden konnte, wohl aber viele Ansätze.
Kryptographie zu belegen war überraschend lohnenswert, weil IT-Sicherheit in jedem Projekt ein Thema ist und die Grundlagen von damals mich immer noch tragen. Zudem war der Professor Johannes Buchmann, der es einfach drauf hatte die Vorlesung interessant und verständlich zu halten. Ich glaube, das war auch die Vorlesung für die ich RSA in Bash implementierte, was ein bisschen zeigt wie locker man das Thema angehen konnte.
Außerdem war HCI ein prägender Kurs zu Usability. Als Informatiker fand ich das besonders toll, weil es einen Weg vorwärts versprach wie man alles andere anwenden konnte. Nicht nur viel wissen, sondern auch herausfinden können was man überhaupt bauen soll und womit Nutzer umgehen können. Damit wollte ich weitermachen, also wurde das Thema mein Masterstudium – was nicht ganz klappte, aber dazu später mal mehr.
Und schließlich, etwas allgemeiner: Diese übliche Prophezeiung, dass im Studium viele gute Leute zusammenkommen und die dann auch oft besser sind als du, in Darmstadt stimmte das für mich. Auch eine Erfahrung.
Leben als Student in Darmstadt
Nach etwas Eingewöhnungszeit wurde das Studieren zum Alltag. Ich besuchte die Vorlesungen fast immer, nahm an den Übungen teil, und füllte die verbliebene Zeit mit anderen Dingen.
Am Anfang hatte ich keine Zeit über, denn anfangs bin ich von meinem Heimatort nach Darmstadt gependelt, was großer Mist war. Dass ich seitdem mit einer kurzen Ausnahme nie wieder gependelt bin ist kein Zufall. Erstens kriegte ich anfangs vom Studentenleben wenig mit, zweitens fühlte ich mich wegen der unsicheren Heimreise in Darmstadt nicht wohl, drittens schadete es der Motivation an den Vorlesungen und Übungen teilzunehmen und damit dem Studium. Gerade kommt mir der Gedanke: Man konnte beides damals nicht von Zuhause machen, vielleicht ginge zumindest die fachliche Seite mittlerweile trotz der Distanz etwas besser?
Später wohnte ich im Studentenheim, das half sehr. Es gab dort aber die absurde Situation, pfeilschnelles Internet zu haben, aber nur 10 oder 20 GB Traffic, danach wurde die Leitung für den Monat abgeschaltet. Ich hoffe, das ist heute Geschichte – ah, sie sind jetzt bei 120GB, das sind immer noch nur 2 AAA-Spiele. Peinlich. Und es half anfangs nicht gerade beim Sicherheitsgefühl in der neuen Heimat.
Studentenjobs an der Uni dagegen waren eine gute Sache für mich. Eine Weile war ich Mentor für Erstsemester, ihr einer Pflichttermin. Diese Tätigkeit hatte abgesehen der üblichen Anliegen der Menties bezüglich mancher Studieninhalte so gar nichts mit Informatik zu tun, das war eine interessante Erfahrung. Später als Tutor in Übungen lernte ich immerhin noch, wie schwer diese Rolle ist und wieviel man von dem Stoff auch wieder vergessen hat.
In meiner Freizeit verbrachte ich als Informatiker viel Zeit vor dem Rechner, klar. Von Darmstadt habe ich nicht viel mitbekommen, um in Cafes rumzusitzen zum Beispiel hätte mir auch schlicht das Geld gefehlt. Aber so ein bisschen Studentenleben nahm ich doch mit: Mit Jugger eine ungewöhnliche Sportart; Serien bekamen durch das Studentennetzwerk eine neue Bedeutung, Filme ebenso durch den Mathefilmabend (mit schlechten Filmen) und dem Studentenkino im Audimax (mit besseren); auf Vorschlag einer Kommilitonin lernte ich mit ihr Salsa, ich war und bin darin untalentiert, aber es war aus Gründen später eine der wichtigsten Sachen die ich hätte lernen können. Die D&D-Gruppe erwähnte ich oben über den Spielemeister, eine Brettspielgruppe fand sich auch, meine erste. Ein paar Klischees erlebte ich zudem: Den sich nie als nützlich erweisenden Sprachkurs, krude Charaktere in WGs, sogar eine für mich gescheiterte WG, ein paar wenige Studentenfeiern.
Man kann sich ja gerade bei Informatik auf den Standpunkt stellen, dass das Studium nicht lohnt. Einfach alles relevante online lernen, oder eine Ausbildung im Unternehmen machen und so früh wie möglich mit dem Arbeiten anfangen, beides bringe am Ende durch die gesparte Zeit mehr Gehalt. Stimmt monetär vielleicht. Aber wenn nicht gerade eine Pandemie alles sowieso blockiert, dann verpasst man so eben auch dieses Leben. Das zwar bei all der Arbeit, dem geringen Einkommen und dem dauernden Prüfungsstress auch nicht immer toll war, aber im Nachhinein echt keine schlechte Zeit.
Fazit
Man wurde im Informatikstudium mit Stoff bombardiert. Das schaffte viele Grundlagen, um Neues zu verstehen und auch anzuwenden. So brauchte ich beispielsweise diesen Schubs, um die praktische Funktion von Datenbanken zu verstehen und in meine Projekte einbauen zu können. Generell wurde alles was irgendwie praxisrelevant war besonders interessant: Beispielsweise auch der Bayes-Algorithmus wegen meinem Bayes-Spamblockplugin. Aber das funktionierte auch andersrum: Praktisch alles, für das ich einen Zugang finden konnte – was mir nicht komplett unergründlich war – hatte in den Folgejahren nochmal irgendwie eine Relevanz. Selbst manche der Mathematikkenntnisse, sogar die Logikformeln aus den formalen Grundlagen erleichterten später mindestens das Lesen mancher Veröffentlichungen.
Gut, manches war zu speziell oder ist mittlerweile veraltet. Das Wasserfallmodell als Entwicklungsmodell mit Lasten und Pflichtenheft zum Beispiel – klar gibt es das noch, aber es ist aus gutem Grund nicht der Standard. UML-Diagramme hätte man auch weniger machen können, damit Programme zu planen wurde sicher nicht die Zukunft der Programmierung. Wobei: No-Code-Programmierung baut ja irgendwo schon auf den gleichen Ideen auf, dafür gibt es derzeit verstärkt Aufmerksamkeit. Bei den Software-Designpatterns weiß ich nicht mehr, ob meine bis heute anhaltende kritische Haltung vom Dozent vermittelt wurde oder ob ich damals schon ihre Probleme wahrnahm. Aber das Thema zeigt die Gefahr, dass man an der Uni auch Dinge lernen kann, die einem Programmierer in der Praxis eher schaden – wie das übertriebene Anwenden von Designpatterns.
Doch insgesamt: Für jemanden, der später Software bauen wollte, war das Informatikstudium in Darmstadt eine gute Wahl. Dass ich mir ein eigenes Bachelorarbeitsthema aussuchen konnte, direkt einen tollen Betreuer fand und daran dann auch noch bei der Telekom arbeiten konnte war dann noch ein guter Endpunkt eines guten Studiums. Rückblickend muss ich auch loben, wie gut die Uni ausgestattet war (schon der PC-Pool im Keller mit sauber funktionierenden Linux-Computern), wie fähig die Professoren Vorlesungen hielten und wie gut das Studium organisiert war, abgesehen nur vom damals fast unschaffbar überfrachteten dritten Semester.
Müsste ich nochmal von vorne anfangen, würde ich direkt wieder dieses Studium wählen und auch wieder nach Darmstadt gehen. Stattdessen ging es mit einem Master weiter, über den es auch einiges zu erzählen gibt.
Video: Cyberpunk 2077 und Softwareentwicklung
Die Argumentation im Video ist nicht unbedingt neu, wenn man etwas im Thema ist. Aber gerade deswegen ist es ein anschauliches Beispiel für Planungsfehler, die möglicherweise dem Spiel geschadet haben.
Vorab: Sie ist auch kritisierbar. Ein Spiel wie Cyberpunk kann man nicht von Anfang an in einem veröffentlichbaren Zustand halten. Auch die verlinkte Präsentation zu Sea of Thieves ist da kein echtes Gegenargument, da wurde dieses Entwicklungsmodell nur teilweise adoptiert (immerhin!), und das Spiel ist viel kleiner und war am Ende wohl auch nicht besonders gut. Das geht mit Werkzeugen, bei denen schon ein kleiner Teil des geplanten nützlich wäre. Vielleicht bei Spielen mit einer sehr simplen Grafik, Mechanik und Frust-Spaß-Schleife (dass die bei Sea of Thieves zu simpel ist war in Tests der größte Kritikpunkt). Aber es geht nicht mit storygetriebenen echten AAA-Spielen, die bei ihrer Vollständigkeit eine hohe Messlatte erreichen müssen um auch nur ein bisschen spaßig zu sein.
Außerdem hatte CD Projekt ja ursprünglich kein Releasedatum genannt, ist also anfangs der im Video vertretenen Theorie gefolgt. Das war sicher eben um flexibel sein zu können. Interessant wäre eher ein Blick auf die Dynamik, die das Entwicklerstudio trotz dieses Starts zum verfrühten und auf alten Konsolen wohl nahezu ungetesten Release verleiteten. Alternativen gab es nicht viele, die angesprochene, nur auf den alten oder neuen Konsolen zu veröffentlichen, war in dem Moment keine Option mehr als einmal etwas anderes angekündigt worden war, auch ohne die Ankündigung wäre es nach dem Release der neuen Konsolen nicht machbar gewesen. Denn: Zu wenige Leute haben die neuen Konsolen bereits, aber die neuen nicht zu beliefern wäre für jedes AAA-Spiel ein PR-Desaster. Tatsächlich war die veröffentlichte Version auch die, die auf die Last-Gen-Konsolen zugeschnitten sein sollte. Was wirklich schiefging werden erst später gute Reportagen wie der zu Andromeda erklären können, bis jetzt gibt es nur Erklärungsansätze.
Die Argumentation des Youtubers ist also völlig daneben. Faszinierenderweise ist sie gleichzeitig völlig richtig. Denn die Grundargumentation im Video passt. Zumindest im letzten Jahr der Entwicklung sind die Entwickler nach allem was bekannt ist genau in die beschriebene Planungsfalle gelaufen. Gleichzeitig einen festen (fast, einmal wurde es ja nochmal verschoben) Termin treffen zu wollen ohne das Spiel nach all den Trailern deutlich verschlanken zu können war keine gewinnbare Situation. Entwicklerteams vergrößern ist, richtig, keine Hilfe. Da ging also etwas wie vom Youtuber beschrieben komplett schief. Das zeigt der Crunch: Scheinbar wurde wider besseren Wissens gehofft, Entwickler länger als 4 bis 5 Tage lange Stunden arbeiten zu lassen verkürze die Entwicklungszeit. Eine Verzweiflungsentscheidung, die offensichtlich verkehrt ist, denn ausgebrannte Entwickler werden höchstens grantig aber keinesfalls schneller. Das weiß eigentlich jeder, es trotzdem zu versuchen ist genau das kritisierte Wunschdenken.
Wenn das anfängliche Scheitern Cyberpunks dazu führt, dass mehr Firmen die Unsinnigkeit von Crunchentwicklung erkennen, hätte das ganze nochmal was gutes.
Immutability und ein praktischer Einsatzzweck
Objekte die immutable sind können in keiner Weise modifiziert werden. Doch was bringt das?
Das Konzept
Viele Sprachen – nicht alle natürlich – unterstützen das Konzept, mal für alle Datentypen, mal nur für spezielle. Aber grundsätzlich könnte es so aussehen:
class Car immutable doors = ['door1', 'door2'] end
Wenn der Entwickler sich dann später ein entsprechendes Objekt erstellt, dann kann er das doors
-Attribut nicht ändern:
myCar = new Car(); myCar.doors.add('door3') #=> Error!
Immutability ist ein Konzept, das man besonders oft bei funktionalen Programmiersprachen finden wird. Wer sich Gedanken über Seiteneffekte macht, der wird auch Immutability bedacht haben. Es kann auch massiv dem Compiler helfen, wenn Objekte entsprechend markiert sind und er so weiß, dass diese Objekte sich nie ändern können. Programme werden so schneller oder sicherer.
Wobei, von wegen funktional, const
geht ja in eine ganz ähnliche Richtung.
Ein Anwendungsfall
Doch hilft es dem Entwickler? Das allerwichtigste ist Entwicklerzeit, -effizienz und -komfort. Immutability schadet dabei oft. Wenn ich als Entwickler ein geschütztes Attribut ändern will, habe ich wahrscheinlich einen guten Grund. Wenn ich jetzt nur des Compilers wegen ein neues angepasste Objekt erstellen und am besten noch einige Attribute kopieren muss, dann stimmen die Prioritäten nicht.
Aber jetzt lief ich in diesen Fall: Ich hatte eine Liste. Und irgendwas veränderte diese Liste. Und zwar wurde der letzte Eintrag in ihr länger. Doch nirgends in meinem Programm gab es eine solche Zuweisung, zumindest keine die eindeutig zu erkennen war.
Was wohl passiert war: Es gab ein Fold.
newList = originalList; targetMap = newList.fold({}, (prev, element) => prev..addAll(element));
Und dazu einige andere solcher Akkumulatoren, die meine Originalliste zusammenfassten (die in Wirklichkeit auch keine einfache Liste war, sondern eine Liste von Listen von Ojekten in einem Elternobjekt). Und irgendeiner davon hatte als Nebeneffekt, dass die Originalliste – denn in dieser Sprache werden meist Referenzen übergeben – ebenfalls verändert wurde.
Die Lösung: Die Liste als immutable markieren.
Die Akkumulatorfunktionen wie das fold
funktionierten immer noch. Aber anstatt die Originalliste zu verändern, erstellten sie sich automatisch ihre eigene Kopie. Die Originalliste blieb unverändert, der Bug war gefixt.
Es ist also keinesfalls so, dass Immutability nur den Code verkompliziert. Im Gegenteil, richtig eingesetzt kann es ein Programm wesentlich einfacher zu verstehen machen, dann dient das Konzept dem Entwickler. Besonders in Sprachen, die Referenzen einsetzen anstatt direkt Werte zu kopieren.
Object-Oriented Programming is Bad
Ich mochte diesen Vortrag. Man muss den Anfang mit seinem "das wichtigste Video ever" ignorieren, aber danach hat es ein paar gute Punkte (die sich nicht alle im zugehörigen Artikel finden). Es wird ziemlich überzeugend erklärt, was die Schwachstellen der objektorierentierten Programmierung sind: Dass dessen Ideal nicht erreicht werden kann und gar kein Ideal ist, warum Objektarchitekturen gerne ausufern und wieso man oft an Fragen gelangt, die nicht entscheidbar sind. Es sind zudem ein paar gute praktische Tricks drin die man in seinen Code einbauen kann, auch wenn die meisten nicht gerade neu sind. Aber sie sind gut gebündelt und im Kontext des Hauptarguments angemessen, lange Funktionen zu rechtfertigen und gekapselte lokale Funktionen ins Spiel zu bringen ist erfrischend.
Vor allem wird betont, dass Abstraktion nicht etwas positives ist, sondern automatisch Komplexität bedeutet, was sehr schlecht ist. Und das ist mindestens so richtig und wichtig wie es oft ignoriert wird. Es kollidiert mit vielem was gelehrt wird, aber ganz besonders mit den Paradigmen aus der Enterprise-Ecke. Man kann diesen Ansatz auch mit vielen gegenteiligen Entwicklungen im Javascriptuniversum vergleichen und wird ihn bestätigt finden.
Allerdings, was es für mich verdächtig komfortabel macht: Ich sehe einige Überschneidungen mit meiner angestrebten Programmarchitektur, die ich vor ein paar Jahren hier im Blog beschrieben habe und die sich seitdem nicht groß geändert hat. Wobei ich meinen Code wesentlich objektorientierter halte als es Brian Will (von dem der Vortrag stammt) wahrscheinlich gut fände. Doch wo der Code letztens Endes lebt ist in der Argumentation nicht so wichtig, solange er nicht unnötig abstrakt und über das Programm verteilt ist, und das war so ziemlich mein Hauptziel. Dieser Aspekt und die funktionalen Anleihen, die ich mittlerweile gerne einbaue, passen gut zu den Ideen des Videos.
Effizienter CSV-Dateien verarbeiten, mit Ruby und generell
Vor kurzem schrieb ich darüber, wie ich mit dem SAX-Parser besser mit XML-Dateien umgehen konnte. Besser bedeutete, mit weniger Speicherbedarf schneller die gesuchten Informationen aus teils relativ großen XML-Dateien zu holen. Es half, aber der Server hatte immer noch spürbare Last durch die anderen Datenquellen: Den CSV-Dateien. Sie benutzen manche der Datenausgeber statt XML, und auch bei ihnen führte das naive Vorgehen zu extremen Speicher- und Prozessorbedarf.
Das naive Vorgehen war grob so:
hardwares = Database.instance.getHardwares hardwares.each do |hardware| csv = cache.getset('csvApi') do csvGz = open("https://url/zur/csv.gz") unzippedCsv = Zlib::GzipReader.new(csvGz).read csv = CSV.parse(unzippedCsv, :headers => true) csv end return csv.detect{|line| line['id'] == hardware.id } end
Es gab also ein Array mit den Bezugsobjekten, zu denen die Zeile mit ihrer ID aus der CSV-Datei gezogen werden soll. Optimiert ist da bereits, dass die CSV-Datei nicht mehrfach heruntergeladen wird. Dafür sorgt der lru-cache.
Wie geht es besser?
1. Speichereffizienter parsen
Der erste Schritt ist das Parsen der CSV-Datei. Der bisherige Code macht das in einem Rutsch und baut – ähnlich wie bei XML-Dateien – ein CSV-Objekt. Wenn wir stattdessen Zeile für Zeile durchgehen entsteht eine Chance, den Speicherbedarf zu reduzieren. Dalibor Nasevic hat dazu Codebeispiele und Benchmarkergebnisse. Der Code ändert sich so:
unzippedCsv = Zlib::GzipReader.new(csvGz) csvFile = CSV.new(unzippedCsv, headers: true) while line = csvFile.shift # do something end
Der GzipReader liest nicht mehr die Datei auf einmal in den Speicher, mit diesem neuen Startpunkt geht der CSV-Parser zeilenweise durch die Datei. Wenn wir jetzt einfach das CSV-Objekt nachbauen bringt das nicht viel, aber es gibt uns die Möglichkeit etwas besseres zu bauen.
2. Mit fastcsv schneller parsen
Doch bleiben wir erstmal beim Parsen selbst. Derzeit benutzt der Code das in Ruby integrierte CSV-Modul. Doch es gibt Alternativen, insbesondere fastcsv. Das Gem kann in vielen Fällen das normale CSV-Modul direkt ersetzen und war in meinen Tests etwa doppelt so schnell.
require 'fastcsv' csvFile = FastCSV.new(unzippedCsv, headers: true)
Nett, aber das Parsen der CSV-Datei war gar nicht das Problem. Das sparte ein paar Sekunden. Das eigentliche Problem war das spätere Durchsuchen des erstellten CSV-Objekts.
3. Mit Hash nicht suchen, sondern nachschlagen
Das ist eine Optimierung, die in jeder Sprache funktionieren wird.
Wenn CSV.parse
ein CSV-Objekt erstellt, ist das im Grunde ein großes Array mit Arrays (headers: false
) oder Hashs (headers: true
) in den Arrayeinträgen. Entsprechend durchsucht der Code von oben dieses Array mit dem üblichen Enumerable.detect. Doch das bedeutet, dass für jedes Suchobjekt die CSV-Struktur durchgegangen werden muss, bis etwas gefunden wurde. Oder bis die Struktur durch ist und eben nichts gefunden wurde. Wenn es nur eine Datenstruktur gäbe, die für eine ID direkt die passende Zeile ausgeben könnte…
Die gibt es natürlich, genau das ist in Ruby der Hash. Da wir jetzt zeilenweise durch die CSV-Datei durchgehen und die Struktur selbst bauen können wir sie nutzen:
csv = cache.getset('csvApi') do … csv = {} while line = csvFile.shift csv[line['id']] = line end csv end return csv[hardware.id]
Das hier ist die große Optimierung. Anstatt mehrfach durch die riesige Datenmenge zu stöbern am Anfang mit etwas Mehraufwand die Hashstruktur zu erstellen spart danach so viel Zeit bei jedem Suchvorgang, dass minuten- bis stundenlange Prozesse in wenigen Sekunden fertig werden.
4. Ungenutzte Datenfelder rausschmeißen
Moment, da gibt es noch eine mögliche Optimierung, wieder völlig unabhängig von Ruby. Eventuell braucht es später gar nicht alle Felder, die in der CSV-Datei gespeichert sind. Vielleicht wird später nur nach price und available geschaut. Wenn dem so ist, dann ist genau hier der Moment die überflüssigen Felder zu entfernen und so den Speicherbedarf zu senken:
while line = csvFile.shift csv[line['id']] = line.to_h.keep_if{|k, _| k == 'price' || k == 'available' } end
Die Kombination dieser vier Schritte ist sehr mächtig. Was vorher viele Minuten rödelte und Prozessorkerne voll auslastete ist in ein paar Sekunden erledigt. Aber es ist ja auch ein Idealfall. Es gab genau eine ID, wir in einer Hashmap als Key nutzen und dann nachschlagen konnten. Was, wenn es mehr als einen Key gibt?
5. SQLite für mehrere IDs
In meinem Anwendungsfall gab es manchmal neben der id noch die sku, also einen zweiten Key. Dann reicht ein Hash nicht, denn es gibt keine mir bekannte Möglichkeit, einen zweiten Key einzusetzen. Klar, wir könnten einen zweiten Hash erstellen. Aber würde das nicht den Speicherbedarf verdoppeln? Nein, es wäre besser einen zweiten Key als Index über die alte Hashmap zu legen. In Ruby wüsste ich nicht wie das geht (wenn du schon: Ein Kommentar wäre klasse!). Aber SQLite macht das mit links und ist in jeder Sprache verfügbar.
Die Idee also ist: Statt einer Hashmap erstellen wir eine SQLite-Datenbank im Arbeitsspeicher. Primary Key
wird die id, aber für die sku baut SQLite einen Index. Das Durchsuchen geht dann mit ein bisschen SQL. YAML serialisiert die CSV-Zeile, die im Zweifel auch wieder wie in Schritt 4 speicheroptimiert werden könnte.
csv = cache.getset('csvApi') do … csvFile = FastCSV.new(result, :headers => true) db = SQLite3::Database.new(':memory:') db.execute "CREATE TABLE csv(id TEXT PRIMARY KEY, sku TEXT, line TEXT)" while line = csv.shift db.execute("INSERT OR IGNORE INTO csv(id, sku, line) VALUES(?, ?, ?)", line['id'], line['sku'], YAML::dump(line)) end db.execute "CREATE INDEX csv_sku ON csv(sku)" db.execute "ANALYZE" db end row = csv.execute("SELECT line FROM csv WHERE id = ?", hardware.id).first unless row row = csv.execute("SELECT line FROM csv WHERE sku = ?", hardware.sku).first end if row return YAML::load(row[0]) end
SQLite ist unheimlich schnell, die CSV-Datei wird in sekundenschnelle durchsucht sein, je nach Größe natürlich.
Fazit
Wenn man es mal richtig macht… Ich fand das ein gutes Beispiel für einen Anwendungsfall von Informatik-Grundkenntnissen. Statt ein Array zu durchsuchen die Datenstruktur zu ändern und eine Hashmap zu nehmen ist Grundlagenstoff des Studiums, Standardbeispiel für O(1) statt O(n). Aber ich brauchte einen Moment um zu erkennen, dass das hier möglich ist, das komfortable CSV.parse
hatte mir das versteckt. SQLite einzubauen und nach einem schnelleren Gem zu schauen ist dann vielleicht etwas mehr aus praktischer Erfahrung gezogen, aber liegt wenn man mal optimiert und und nach dem Datenbankkurs auch nicht mehr fern.
Mir hat dabei auch geholfen, diese Aufgabe als eigenes Projekt zu betrachten. Ursprünglich war das nur eine kleine Ecke im Code des Überprojekts (pc-kombo), schnell mal gebaut, abgewandelt aus Code der eine REST-API nach den Informationen fragt (wo solche Optimierungen nicht möglich sind). Jetzt ist die Ecke ausgelagert in ihr eigenes Git-Repository und der Code ist auf genau diese Aufgabe reduziert. Das macht es einfacher, solche Optimierungsmöglichkeiten zu sehen.
Auf jeden Fall lohnt sich der Aufwand. Zusammen mit der Reduzierung der Last durch XML-Dateien kann ich den großen Server bald wieder abschalten, der nach dem Scaleway-Umzug die temporäre Heimat dieses Mikroservice wurde. Aus dem Mikroservice wurde jetzt tatsächlich auch ein kleines Programm, das auf schmalerer Hardware wird laufen können. Das reduziert die Strom- oder die Hostingkosten dann schnell um ein paar hundert Euro im Jahr.
Ein bisschen anonym geht eben doch: Wie die Übermedien unnötig Panik verbreiten
Weil die Übermedien nicht wissen was ein Angreifermodell ist, verfallen sie in einer Analyse eines Panorama-Interviews in den Panikmodus. Ein Informant sei unzureichend geschützt worden, Panorama sei unfähig. Der Themenkomplex ist in der Nähe meiner Doktorarbeit, daher ein paar Erklärungen hierzu.
Es geht um den Cum-Ex-Skandal, ein wirklich unfassbar dreister Betrug des Kapitals, bei dem der Staat sich über Jahrzehnte willfährig ausnehmen ließ. Der Interviewte ist ein Insider und zukünftiger Kronzeuge. Dementsprechend soll er anonym bleiben: Er fürchtet seine Mittäter und Konsequenzen im Privaten. Panorama hat also sein Gesicht verborgen und seine Stimme verzerrt und ihn so vor der Kamera unkenntlich gemacht.
Die Übermedien meinen das reicht nicht:
Doch dieser Schutz ist unzureichend. Und das wissen Journalisten spätestens seit dem Frühjahr 2014. Auf der Forensiker-Tagung im Mai in Münster wurde nämlich die Methode offenbar, mit der Ermittler durch Analyse der elektrischen Netzfrequenz vermummte und verkleidete Informanten, deren Stimme verzerrt wurde, enttarnen können.
Die Aussage stimmt, und doch ist ihre Folgerung falsch. Warum ist interessant.
Die Netzfrequenzanalyse betrifft erstmal genau solche Interviewszenarios. Die Netzfrequenz ist nicht mehr stabil, ihre Abweichung wird dauernd protokolliert. Sie lässt sich auch aus Aufnahmen wie der Interviewvideoaufnahme ablesen. Wann eine solche Aufnahme gemacht wurde lässt sich damit also bestimmen, man schaut einfach nach wann die Abweichung im Video mit der protokollierten übereinstimmt. Die Uebermedien glauben man würde auch den Ort herauslesen können – ich glaube das stimmt nicht, ich bin mir aber nicht ganz sicher (für diesen Artikel ist es egal).
Wenn man aber den Aufnahmezeitpunkt hat und den Ort bestimmen kann – was ein Geheimdienst ja auch mit anderen Methoden hinkriegt – dann kann ein entsprechend ausgestatteter Akteur den Interviewten identifizieren. Er muss nur Überwachungskameras auswerten und von den paar tausend Leuten, die so wahrscheinlich gefunden werden, über ihre Profile mögliche Träger der Information herauspicken. Einer der so gefundenen ist der Informant. Ein Geheimdienst macht dann was er eben so macht, NSU-Zeugen landen in brennenden Autos, Feinde Putins werden vergiftet, es bleibt sicher immer sehr stilvoll.
Warum also haben die Übermedien nicht recht mit der Anschuldigung, warum hat Panorama hier nicht furchtbar geschlampt? Das liegt schlicht am Angreifermodell und am Schutzziel.
Der Interviewte hier muss nicht umfassend vor Geheimdiensten geschützt werden. Er ist dem System bereits bekannt, er ist ein Kronzeuge, seine Aussage wird bald öffentlich und seine Identität dann bekannt sein. Es geht nur um Schutz vor den Mittätern. Das sind seine Kollegen, seine Nachbarn vielleicht, aber es sind keine Geheimdienste. Wären die hier verwickelt wüssten sie seine Identität über seine Kronzeugenrolle sowieso schon und hätten sich schon eine passende Eliminierungsmethode ausgesucht.
Das ist also praktisch der umgekehrter Fall von "Die US-Regierung wird uns nicht mit Musketen angreifen."
Meine Nachbarn sind nett, intelligent und fähig, aber sie haben nicht die Ressourcen des BND an der Hand. Sie würden in einem solchen Fall keine Überwachungsvideos auswerten und tausende Profile anlegen, sie würden wahrscheinlich die Netzfrequenzanalye nichtmal kennen. Es ist praktisch gegeben, dass dies im Fall dieses Informanten genauso ist. Denn die Mittäter sind ja wohl kaum Geheimdienste, sondern Privatpersonen – eventuell sehr reiche und skrupellose Banker, schlimm genug, aber doch keine KGB-Agenten. Damit fällt die Netzfrequenzanalyse als Angriffsvektor komplett raus. Ohne sie sind Verfremdung von Gesicht und Stimme dann auch ausreichend, um für den kurzen Zeitraum bis zur Kronzeugenaussage die Anonymität des Insiders zu wahren.
Denn interessanterweise stimmt dieser stark klingende Satz eben nicht:
Doch ein bisschen anonym ist problematisch. Das funktioniert genauso wenig wie ein bisschen schwanger.
Ein bisschen schwanger geht nicht, ein bisschen anonym geht durchaus. Das ist nicht ganz intuitiv, aber Stand der Anonymitätsforschung. Denn praktisch immer ist Anonymität eben doch ein Grad und nicht absolut.
Ein Beispiel: Wenn ein geehrter Leser dieses Artikels hierdrunter einen fiesen Kommentar schreibt und einen falschen Namen benutzt ist er wahrscheinlich mir gegenüber anonym. Ich habe kaum Möglichkeiten die civil identity dieses fiktiven Kommentators zu finden, dann zu ihm zu fahren und ihm eins auf die Nase zu hauen. Glücklicherweise ist das auch nie nötig, meine Kommentatoren immer nett.
Jetzt nehmen wir aber mal an der Kommentator schrieb nicht einfach einen fiesen Kommentar, sondern kündigt einen islamistischen Terrorangriff an. Ich habe zwar immer noch keine Methode ihn aufzuspüren. Aber so etwas würde die Polizei und die Geheimdienste alarmieren, und die könnten dann über die Vorratsdatenspeicherung den Kommentatorterroristen ganz schnell identifizieren. Hat er technische Vorkehrungen getroffen geht es vielleicht etwas weniger schnell, aber es gibt im Internet kaum absolute Anonymität.
Das gleiche gilt auch bei anderen Kommunikationswegen. In fast jeder Situation kann man Leute finden, gegen die ein vermummter Kommunikationsteilnehmer anonym ist, aber von dem andere Stellen durchaus die Identität kennen oder kennen könnten.
Das Foto hier taugt dafür als zweites Beispiel. Ich weiß nicht wer das ist. Gegenüber dem Großteil des Internets ist die junge Dame anonym. Aber der Fotograf dürfte wissen wer das ist, ein entsprechend begabter Detektiv kann ihre Identität wohl aufdecken. Und ihre Mutter würde sie wohl auch erkennen.
Deshalb ist bei solch einer Situation wie dem Panorama-Interview der Kontext so wichtig. Er bestimmt das Angreifermodell: Wer wird die Identität aufdecken wollen und welche Fähigkeiten hat er dafür? Dementsprechend sind ganz unterschiedliche Anonymitätsgrade erforderlich. Man kann sich auch überlegen: Wie schlimm wäre es, wenn die Identität bekannt würde? Auch das kann in die Entscheidung reinfließen.
Den Aussagen des Chefredakteus zufolge wurde genau das sorgfältig und richtig gemacht. Die Übermedien hätten seine Aussagen nicht nur zitieren sollen, sondern sie hätten Beachtung verdient gehabt. Denn mit dem richtigen Hintergrundwissen widerlegen sie alleine komplett den dort lancierten Artikel. Aber ich will die Übermedien gar nicht zu stark kritisieren: Anonymität ist ein erstaunlich kompliziertes Thema. Hier fehlte sicher einfach etwas Hintergrundwissen, um falsche Schlussfolgerungen zu vermeiden – obwohl der genannte Autor es besser wissen müsste heißt die Autorenangabe ja nicht, dass alles aus seiner Feder stammt.
Denn ein bisschen anonym geht eben doch. Man ist sogar immer nur ein bisschen anonym, genau wie Sicherheit niemals absolut ist. Und in anderen Kontexten haben gerade das die Übermedien ja durchaus verstanden.
Monitorama PDX 2014 - James Mickens
Monitorama PDX 2014 - James Mickens from Monitorama on Vimeo.
James Mittens erklärt die Verbindung von Batmans Bane mit NoSQL, die Herausforderungen für Sicherheit angesichts des männlichen Faktors, und warum die US-Regierung uns nicht mit Musketen angreifen wird. Und einiges mehr.
The glEnd() of Zelda
Tom Murphy mit einem weiteren absurd-genialen NES-Video.
rsa.sh
Vor ein paar Jahren versuchte man mir zu erklären, wie RSA funktioniert. Als Übung - und mit der üblichen Warnung, unseren Code nie zu benutzen - sollten wir es selbst implementieren, denn nur dann wird es verstanden. Machte ich mit Freude, natürlich in Bash, einfach weil ich es konnte. Und ich noch heute grinsen muss beim Gedanken an die Schimpfworte, die der Tutor bei der Bewertung benutzt haben müsste (wenn ich mich richtig erinnere, bekam ich sogar die volle Punktzahl - was mich dann doch überraschte).
War mir sicher, das hier verbloggt zu haben, konnte es aber (anlässlich noqqes Implementierung) nicht mehr finden. Daher, uneditiert aus der Abgabemail (oder hier als gist):
#!/bin/bash getPrim() { local range="$1" local prim=$(getRandom $range) until [[ $prim -gt 1 ]] && isPrim $prim;do prim=$(getRandom $range) done echo $prim return 0 } #Miller-Rabin-Test isPrim() { local prim=$1 if [[ $(echo "$prim % 2" | bc) -eq 0 ]];then return 1 fi #won't find an "a" for them: if [[ $prim -eq 3 ]] || [[ $prim -eq 5 ]];then return 0 fi prim_range=${#prim} local i=0 while [[ $i -lt 10 ]];do local a=$(getRandom $(($RANDOM % (prim_range + 1) )) ) until [[ $(echo "$a < ($prim - 2) && $a > 2" | bc) -eq 1 ]];do a=$(getRandom $(($RANDOM % (prim_range + 1) )) ) done if isWitness $prim $a;then return 1 fi let i++ done return 0 } function isWitness() { prim=$1 a=$2 prim_minus_one=$(echo "$prim - 1" | bc) test=1 local i=${#prim_minus_one} i=$(($i - 1)) while [[ $i -ge 0 ]];do x=$test test=$(echo "$x^2 % $prim" | bc) if [[ $test -eq 1 ]] && [[ $x -ne 1 ]] && [[ $x -ne $prim_minus_one ]];then return 0 fi let i-- done test=$(powmod $a $prim_minus_one $prim) if [[ $test -ne 1 ]];then return 0 else return 1 fi } setBasics() { local range="$1" local try="$2" if [[ -z "$range" ]];then #the length of sqrt(m) for p and q fits to the necessary length of n range=$(echo "sqrt($m)" | bc) range=${#range} fi if [[ -z "$try" ]];then try=1 fi doBasics $range $try #bash's comparison won't work with big numbers until [[ $(echo "$n > $m" | bc) -eq 1 ]];do let try++ if [[ $try -gt $(($range * 10)) ]] || [[ ${#n} -lt $(( ${#m} -2 )) ]];then let range++ try=1 fi doBasics $range $try done } function doBasics() { local range="$1" local try="$2" #two primenumbers p=$(getPrim $range) q=$(getPrim $range) until [[ p -ne q ]];do p=$(getPrim $range) q=$(getPrim $range) done #RSA-Modul n=$(echo "$p*$q" | bc -l) #eulersche phi=$(echo "($p-1)*($q-1)" | bc -l) } #choose e coprime to phi getPublicKey() { local e=$(($RANDOM)) until [[ $(echo "$e < $phi" | bc) -eq 1 ]];do e=$(($RANDOM)) done local i=2 while [[ $(echo "$i < $e" | bc) -eq 1 ]];do if [[ $(echo "$phi % $i" | bc ) -eq 0 ]] && [[ $(echo "$e % $i" | bc) -eq 0 ]];then getPublicKey exit fi let i++ done echo $e } getPrivateKey() { local result=($(extended_euclid $e $phi)) local d=${result[0]} until [[ $d -gt 0 ]];do d=$(echo "$d+$phi" | bc -l) done echo $d } function extended_euclid() { a=$1 b=$2 local x=0 local lastx=1 local y=1 local lasty=0 while [[ $b -ne 0 ]];do local quotient=$(echo "$a / $b" | bc) local temp=$b b=$(echo "$a % $b" | bc) a=$temp temp=$x x=$(echo "$lastx - ($quotient * $x)" | bc) lastx=$temp temp=$y y=$(echo "$lasty - ($quotient * $y)" | bc) lasty=$temp done local result=($lastx $lasty $a) echo "${result[*]}"; return 0 } #a^b%m: Square & Multiply powmod() { local a=$1 local b=$2 local mod=$3 local i=0 local res=1 #b in binary for binary exponentiation b=$(echo "ibase=10;obase=2; $b" | bc) while [[ $i -lt ${#b} ]];do res=$(echo "$res^2 * $a ^ ${b:$i:1} % $mod" | bc) let i++ done echo $res } getRandom() { local range="$1" if [[ -z "$range" ]];then range=$RANDOM fi local r=$((RANDOM % 10)) while [[ $r -eq 0 ]];do r=$((RANDOM % 10)) done local i=1 while [[ $i -lt $range ]];do local temp=$((RANDOM % 10)) r=${r}${temp} let i++ done echo $r } encrypt() { local m="$1" local c=$(powmod $m $e $n) echo "$c" } decrypt() { local c="$1" local m=$(powmod $c $d $n) echo "$m" } export BC_LINE_LENGTH=0 m=91011121314151617181920212223242526272829 old_m=$m setBasics e=$(getPublicKey) echo "Public Key: ($e, $n)" d=$(getPrivateKey) echo "Private Key: ($d, $n)" echo c=$(encrypt "$m") echo "c: $c" m=$(decrypt "$c") echo "m: $m" if [[ $m -ne $old_m ]];then echo "Error: Wrong message decrypted:" >&2 echo "p: $p" >&2 echo "q: $q" >&2 echo "Public Key: ($e, $n)" >&2 echo "Private Key: ($d, $n)" >&2 echo "c: $c" >&2 echo "m: $m" >&2 fi #Ausgabe #onli@Fallout:~$ uni/ts/rsa.sh #p: 783057321236353042573 #q: 444786834379004491147 #Public Key: (2969, 348293587050020677086428785700425092601231) #Private Key: (137252777651911145904238835165856311899289, 348293587050020677086428785700425092601231) # #c: 134886886292723664083288725067434182648518 #m: 91011121314151617181920212223242526272829
Growing a Language (1998)
My point is that a good programmer in this time does not just write programs, a good programmer builds a working vocabulary - in other word, a good programmer does language design, though not from scratch, but building on the frame of a base language.
Lamport-Diffie Einmal-Signaturverfahren
Willkommen in der Zukunft. Durch Quantencomputer ist Primfaktorzerlegung kein Problem mehr, RSA und andere Verfahren sind nutzlos geworden. Das unheimlich wichtige Dokument D=(1,1,0) soll aber trotzdem signiert werden, also braucht es dafür ein anderes Verfahren mit einer anderen Sicherheitsbedingung. Das Lamport-Diffie Einmail-Signaturverfahren (LD-OTS) eilt zur Rettung.
LD-OTS braucht nur eine funktionierende Hashfunktion, dass eine solche noch existiert ist Mindestvoraussetzung. Und dann ist es ganz einfach, der Signierer bietet schlicht die passenden Urbilder zu den vorher veröffentlichten und durchs Dokument ausgewählten Hashs an.
Also so: Für jedes Bit im Dokument hat der Signierer zwei Werte, die geheim bleiben, den Signaturschlüssel, hier: x = (x01, x11; x02, x12; x03, x13).
Veröffentlicht wird vorher der Verifizierungsschlüssel: y = (y01, y11; y02, y12; y03, y13) = (H(x01), H(x11); H(x02), H(x12); H(x03), H(x13))
Zum Signieren des Dokuments D = (1, 1, 0) veröffentlicht der Signierer dann je nach Bit an der jeweiligen Stelle seinen Teil des Signaturschlüssels und damit die Urbilder der Hashfunktion als Signatur, hier also: (x11, x12, x03). Der Verifizierer braucht dann dann nur zu schauen, ob die veröffentlichten Signaturschlüsselteile mit dem vorher veröffentlichten Verifizierungsschlüssel übereinstimmen, also H(x11) = y11, H(x12)=y12 und H(x03)=y03 ist.
Spamblock-Bayes: Theoretische Grundlagen
Bayes-Formel
Das Bayes-Theorem wurde entdeckt, verworfen und schließlich wiederentdeckt. Ich glaube, die mathematischen Einwände gegen die Formel finden sich manchmal noch in der Kritik an solchen Filtern wieder, deshalb sei ihr Vorhandensein erwähnt.Die allgemeine Formel lautet:

P(A) ist die Wahrscheinlichkeit für A, P(A|B) ist die Wahrscheinlichkeit für A unter der Bedingung B, also wenn B eingetreten ist.
Wie sieht das nun für Spam aus? So:

Zu beachten ist, dass wir diese Spamwahrscheinlichkeit für jedes Wort im Kommentar wissen wollen, daher am Ende mehrere Wahrscheinlichkeiten aufaddieren und normalisieren müssen.
Aufdröselung
Es gibt also genau drei Variabeln zu berechnen: Die Inverse Wahrscheinlichkeit, die Wahrscheinlichkeit von Spam und die Wahrscheinlichkeit für das Auftreten solcher Wörter. Das für jedes Wort. Was genau bedeuten die Formeln jeweils?- P(Wort|Spam)
- Dies ist die Wahrscheinlichkeit, dass in einem Spamkommentar dieses Wort vorkommt: Spamkommentare mit diesem Wort / Spamkommentare
- P(Spam)
- Die Gesamtwahrscheinlichkeit, dass ein Kommentar Spam ist: Spamkommentare / Kommentare.
- P(Wort)
- Die Wahrscheinlichkeit, dass dieses Wort überhaupt in einem Kommentar vorkommt: Kommentare mit diesem Wort / Kommentare
Programmiertechnische Konsequenzen
Es ist völlig klar, dass eine Datenbank gebraucht wird und die Bewertung größtenteils aus dem Heraussuchen der richtigen Daten zu den Worten besteht. Zuerst muss ein sogenannter Tokenizer den Kommentar in seine Einzelwörter zerlegen. Im Wesentlichen ist das ein:tokenize(text) { tokens = split("\W", text ) return unique(tokens) }Das Lernen eines Kommentars als Ham oder Spam ist nun nichts anderes, als diese Tokens in diese Datenbank namens "tokens" zu schreiben:
token (text) | ham (number) | spam (number)Ist das Token schon vorhanden, wird der zugehörige Ham- bzw Spamwert um eins erhöht. Gleichzeitig wird der Gesamtzähler Hamkommentare bzw Spamkommentare um eins erhöht.
Diese Datenbank zusammen mit den Gesamtzählern gibt uns nun alle nötigen Werte:
- P(Wort|Spam)
spam = sql_query("Select spam from tokens where token = Wort") spam / (Spamkommentare)
- P(Spam)
Spamkommentare / (Spamkommentare + Hamkommentare)
- P(Wort)
ham, spam = sql_query("Select ham, spam from tokens where token = Wort)" (ham + spam) / (Spamkommentare + Hamkommentare)
Formelveränderungen
Schaut man sich nun die Bewertungsfunktion im Bayes-Plugin an wird man feststellen, dass der PHP-Code nicht nur wesentlich unschöner als mein Pseudocode aussieht, sondern auch anders funktioniert. Das liegt daran, dass das Plugin auf b8 aufbaute, und b8 nicht simpel das Bayes-Theorem benutzt, die Wahrscheinlichkeiten addiert und dann durch ihre Anzahl teilt. Diese Änderungen basieren auf Tests und anderen theoretischen Annahmen als den hier gezeigten. Insbesondere die folgenden Änderungen sind enthalten oder denkbar:Law of total probability
Wer Vorkenntnisse hat oder nachrecherchierte, dem könnte aufgefallen sein, dass Wikipedias Bayes-Spamfilterformel anders aussieht:
P(Wort) wird hier gemäß dem Law of total probability umgeformt. Ohne das gerade nachgerechnet zu haben vermute ich, dass die Werte gleich sein sollten und diese Formel nur P(Wort) anders betrachtet.
Häufigkeit von Tokens im Kommentar
Wie oft ein Wort im Kommentar auftaucht sollte die Wahrscheinlichkeit beeinflussen. Dieser Artikel hier ist kein Spam, obwohl nun Viagra auftaucht, aber wäre jedes Wort Viagra, wäre er durchaus Spam. Deshalb beachtet b8 die Häufigkeit eines Tokens.Wichtigkeit
Eine beliebte Spammertaktik ist, Kommentare mit ewig langem Fülltext auszustaffieren und den Spammerinhalt so zu verstecken. Der Gedanke dabei ist, dass die vielen harmlosen Worte den ganzen Kommentar harmlos erscheinen lassen. Die auch von b8 genutzte Taktik dagegen ist die Einführung eines Wichtigkeitsfaktors: Beziehe nur die Tokens in die Schlussrechnung ein, die eine Tendenz zu Spam oder Ham haben, also um einen bestimmten Faktor von der Mitte 0,5 abweichen. Der Gedanke dahinter ist, dass die vielen Füllwörter die Bewertung sonst gegen 0,5 tendieren lassen würden.Optimierungsmöglichkeiten
Ich schrieb oben, dass dieser Artikel die theoretischen Grundlagen zwecks einer späteren Optimierung des Filters deutlich machen soll.Diese Optimierungsmöglichkeiten sehe ich bis jetzt:
Ham-Spam-Faktor
Der Anstoßgeber dieses Eintrags ist diese Diskussion. Grischa schlug vor, über irgendeinen Faktor auszugleichen, dass ein typischer Blog sehr viel mehr Spam als Ham bekommt und daher der Filter zu streng werden würde. Meiner Meinung nach ist das nicht wirklich ein Problem, da diese Verteilung elementar für den Filter ist, und nicht automatisch neuer Spam eingelernt wird, wenn man das nicht will.Wikipedia erwähnt, dass P(Spam) generell 0,8 sei. Vielleicht würde es helfen, diesen Wert festzusetzen statt ihn empirisch zu bestimmen?
Häufigkeit von Tokens
Wie oben beschrieben ist die Häufigkeit von Wörtern innerhalb eines Kommentars ein wichtiger Faktor. Zur Zeit fließt das aber nur indirekt in die Bewertung ein, indem es bei der Bewertung selbst ignoriert wird, beim Einlernen aber beachtet wird. Statt in der Tabelle den Ham- bzw Spamwert um eins zu erhöhen, wird er um die Anzahl der Tokens erhöht. Bei der Bewertung aber wird jedes Token nur einmal beachtet, selbst wenn es mehrmals vorkommt. Das könnte man ändern.Es ist auch ein kritischer Punkt, weil man sich hier leicht mit Anzahl der Tokens und Anzahl der Kommentare verheddern kann. Die Formel müsste genau geprüft werden.
Konstanten
Bei der Übernahme von b8 wurde die Klassifizierungsberechnung blind übernommen. In ihr enthalten sind einige Konstanten, die auf den Testergebnissen beruhen. Es geht um diese Zeile:$ratings[$word] = (0.15 + (($stored_tokens[$word]['ham'] + $stored_tokens[$word]['spam']) * $rating)) / (0.3 + $stored_tokens[$word]['ham'] + $stored_tokens[$word]['spam']);0,15 und 0,3 sind die Konstanten, die hier direkt die Bewertung beeinflussen und entfernt bzw verändert werden könnten.