INHALT

Teil IV
Software Engineering: Wie erstellt man Programme?

Rückblick zur Auffrischung

Erinnern wir uns an dieser Stelle noch einmal, was wir bis jetzt gelernt haben. Wir wollen Eingaben in Ausgaben umwandeln. Diese Ein- und Ausgaben sind Daten: Zahlen, Sensordaten, E-Mails, Suchanfragen, Tastatureingaben, Kamerabilder, Sprache und so weiter. Deswegen gibt es im Deutschen das hinreißend altmodische Wort der elektronischen Datenverarbeitung (EDV) oder auch, unsauberer, Informationsverarbeitung. Der Zusammenhang von Ein- und Ausgabe ist eine Funktion im mathematischen Sinn, denn aus der Schule erinnern wir uns, dass eine Funktion ja immer Ein- auf Ausgaben abbildet. Wie genau der Wert einer Funktion berechnet wird, das müssen sich Programmiererinnen und Programmierer überlegen. Die Berechnung kann durch Modelle erfolgen, die beispielbasiert per Maschinenlernen automatisch erstellt wurden. Oder sie kann durch Angabe eines Algorithmus erfolgen, der das Problem im Wesentlichen dadurch löst, dass es in einzelne Schritte zerlegt wird. Den Algorithmus formulieren die Programmiererinnen und Programmierer dann unter Hinzufügen von Details als Programm in einer dem Problem angemessenen Programmiersprache, das auf einem Prozessor unter Verwendung von Speicher, Netzwerkanschluss und weiterer Hardware ausgeführt wird. Schließlich haben wir ein zentrales Prinzip der Konstruktion von Software kennengelernt: Große Probleme werden in kleine Probleme zerlegt, die unabhängig voneinander gelöst werden, um die Lösungen dann zusammenzusetzen: Anweisungen in einer Programmiersprache wie die Wiederholungsansweisung lassen sich aus Anweisungen zusammensetzen, die ein Prozessor verstehen kann. In einem Programm kann man andere Funktionalitäten verwenden, die man selbst programmiert hat, wie etwa die Summenfunktion. Oder man setzt große Programme aus vielen kleinen Programmen zusammen, die in Bibliotheken zur Verfügung stehen.

Es ist nicht so, dass für ein gegebenes Problem nur ein einziger Algorithmus existiert, der dann auch nur auf eine einzige Art und Weise in ein Programm umgesetzt werden kann. Ganz im Gegenteil. Für das beispielhafte Problem „Sortieren einer Zahlenfolge“, das für Informatikerinnen und Informatiker immer noch ganz wesentlich ist, gibt es erstens Dutzende von Algorithmen mit unterschiedlichen Eigenschaften bezüglich des notwendigen Speichers für Zwischenergebnisse und der für die Berechnung erforderlichen Zeit und Energie. Zweitens kann man für jeden Algorithmus nicht nur wegen der freien Wahl einer Programmiersprache ganz unterschiedliche Programme schreiben, die denselben Algorithmus implementieren. Und wir haben oben gesehen, dass mit Maschinenlernen noch eine ganz andere Art unterschiedlicher Lösungen für dasselbe Problem gefunden werden kann.

Wann ist ein Programm gut?

Wenn es unterschiedliche Programme gibt, die denselben Algorithmus implementieren, und wenn es unterschiedliche Algorithmen gibt, die dasselbe Problem lösen, dann stellt sich die Frage, worin diese Programme und gegebenenfalls Algorithmen sich eigentlich unterscheiden. Algorithmen sind ja auf einer etwas höheren Ebene formulierte Anweisungen, wie ein Problem gelöst werden kann. Auf der Ebene von Algorithmen interessieren sich Informatikerinnen und Informatiker zunächst dafür, ob diese Algorithmen das gegebene Problem wirklich in allen Fällen lösen. Das war ja für unsere Berechnung von Quadrat- und Summenfunktion nicht der Fall, wenn wir uns an den Fall negativer Eingabewerte erinnern. Wenn das Problem immer richtig gelöst wird, nennen wir den Algorithmus, und dann auch das Programm, korrekt. Korrektheit ist so wichtig und gerade im Fall von Maschinenlernen so inhärent schwierig, dass wir weiter unten noch einmal darauf zurückkommen werden. Unser erstes Gütekriterium ist also Korrektheit, die man durch null fehlerhafte Berechnungen oder null fehlerhafte Programmschritte charakterisieren kann.

Beim Maschinenlernen begegnet uns die Korrektheit in etwas anderer Form: Wie gut ist die Erkennung oder die Klassifizierung? Beim Erkennen von Hunden und Katzen beispielsweise sollen einerseits alle Hunde als Hunde erkannt werden, andererseits aber keine Katzen fälschlich als Hunde. Das ist nicht dasselbe. Es gibt gute Ansätze, diese doppelte Qualität zu messen. Wie das Maschinenlernen selbst setzen sie allerdings oft eine große Datenmenge voraus – und die hat man eben nicht immer zur Verfügung. Im Fall der Gruppierung (dem Clustering) ist es sogar oft so, dass man vorab gar nicht weiß, ob die gefundenen Gruppen sinnvoll sind – denn wüsste man das schon, würde man wiederum oft gar kein Maschinenlernen benötigen.

Informatikerinnen und Informatiker interessieren sich dann insbesondere dafür, wie viel Speicher Algorithmen beziehungsweise die sie implementierenden Programme benötigen, wie viel Zeit die Ausführung in Anspruch nimmt und auch dafür, wie viel Energie sie verbrauchen. Speicher ist vergleichsweise teuer, deswegen ist weniger mehr. Intuitiv ist klar, dass eine schnellere Problemlösung im Normalfall einer langsamen vorzuziehen ist. Und dass weniger Energieverbrauch besser ist als höherer, leuchtet auch unmittelbar ein: Mit 200 Suchanfragen bei Google benötigen Sie die gleiche Menge Strom wie für das Bügeln eines Hemdes [Quelle; siehe zum Gesamtstromverbrauch von Google auch diesen Link]. Bei geschätzt 63.000 Suchanfragen pro Sekunde im Jahr 2020 [Quelle] ist es unbedingt erforderlich, hier so energiesparsam wie möglich zu programmieren, was Informatikerinnen und Informatiker natürlich auch tun. Speicher-, Zeit- und Energiebedarf sind also ein zweiter Satz wichtiger Gütekriterien.

Schließlich gibt es noch eine ganze Reihe anderer Kriterien für die Güte von Programmen. Einerseits sind das Eigenschaften, die die Nutzerinnen und Nutzer eines Programms erleben: Ist ein Programm sicher in dem Sinn, dass ein Angreifer das Programm oder seine Eingabe- oder Ausgabedaten oder Zwischenergebnisse nicht sehen oder verändern kann? Ist es sicher in dem Sinn, dass es etwa im Fall von Robotern der Umwelt keinen Schaden zufügt? Gewährt es Privatheit? Ist es einfach zu verwenden? Macht die Verwendung Spaß? Und dann gibt es Kriterien, die aus Sicht der Ingenieurinnen und Ingenieure relevant sind: Angesichts der Tatsache, dass Programme oft jahrzehntelang „leben“, ist es entscheidend, ob das Programm gut wartbar ist. Sind Änderungen einfach durchzuführen? Ist es leicht verständlich? Ist es einfach, von einer Computerhardware auf eine andere zu übertragen, was leider gar nicht selbstverständlich ist?

Software trifft zunehmend Entscheidungen, die uns alle betreffen – im autonomen Fahrzeug, bei der Gewährung von Krediten, in Ampelschaltungen, in der Medizin oder in der Polizeiarbeit. Dass das Wort „Entscheidung“ hier in die Irre führen kann, weil es Verantwortung suggeriert, haben wir schon kurz angedeutet. Am bidt kümmern wir uns deswegen auch um einen weiteren Aspekt von Güte, der sich auf ethisch wünschenswerte Überlegungen stützt und deswegen noch viel schwieriger zu bewerten ist als die anderen Gütekriterien. Leider führt auch das hier zu weit, und wir laden Sie ein, sich über unser Projekt zu Ethik in der Softwareentwicklung (und nicht nur dem Maschinenlernen!) am bidt zu informieren.

Software wird weiterhin in der Regel von Unternehmen entwickelt, die ein Interesse daran haben müssen, möglichst schnell zu entwickeln. Sonst ist die Konkurrenz schon da und hat viele Nutzerinnen und Nutzer an sich gebunden; an anderer Stelle werden wir diesen „winner takes it all“-Sachverhalt betrachten. Natürlich wollen Unternehmen auch die Kosten möglichst gering halten. Es stellt sich jetzt leider schnell heraus, dass die oben genannten Kriterien, unter anderem Korrektheit, Ressourcenverbrauch, Sicherheit und Privatheit, Nutzbarkeit und Wartbarkeit sowie Kosten und Entwicklungszeit häufig miteinander in Konflikt stehen. Gute Nutzbarkeit kollidiert oft mit hoher Sicherheit, hohe Sicherheit kollidiert bisweilen mit schneller Programmausführung; gute Wartbarkeit kann mit schneller Entwicklungszeit kollidieren und so weiter. Zielkonflikte sind ganz normal und bestimmen unser Leben: Denken Sie nur an die verschiedenen Faktoren, die eine Coronastrategie beeinflussen müssen.

Die Güte eines Programms ist also eine Kombination aus den genannten Faktoren. Dabei gibt es nicht die eine goldene Kombination, die für alle Programme optimal wäre. Software ist sehr stark abhängig von und verwoben mit dem Entwicklungs- und Einsatzkontext: Medizintechnische Produkte, Kläranlagensteuerungen, Pizza-Lieferdienst-Apps, autonome Fahrzeuge, Gartenbewässerungsanlagen und so weiter weisen ganz offensichtlich sehr unterschiedliche Anforderungen an die Güte der entsprechenden Software auf. Wir kommen später darauf zurück, wie man die unterschiedlichen Anforderungen an Güte erfüllen kann: Das ist genau eine der zentralen Aufgaben des Software Engineering, derjenigen Disziplin, die sich um das Erstellen und Warten im mehrfachen Sinn „guter“ Software kümmert.

Was soll ein Programm tun: Anforderungen

Bevor wir zum Abschluss erklären, was Software Engineering ist und warum Software letztlich doch mehr als Programme und sehr viel mehr als Maschinenlernen ist, möchte ich auf das oben angesprochene Problem der Korrektheit zurückkommen. Erinnern wir uns: Korrektheit eines Programms ist dann gegeben, wenn es das tut, was es tun soll, wenn es also ein gegebenes Problem löst. Damit ergibt sich sofort, dass Korrektheit kein absolutes Konzept ist: Korrektheit kann immer nur mit Bezug zu dem gedacht werden, was eigentlich erwünscht ist. Informatikerinnen und Informatiker unterscheiden hier zwischen dem Soll- und dem Istverhalten eines Systems: Ersteres ist erwünscht. Zweiteres ist das, was das Programm wirklich tut, wenn es ausgeführt wird. Idealerweise sind Soll- und Istverhalten identisch.

Wir müssen vor Formulierung des Sollverhaltens genau verstehen, welches Bedürfnis wir adressieren und welches Problem wir eigentlich lösen wollen. Oft passieren in der Systementwicklung hier schon die ersten Fehler. Vielleicht kann das folgende Beispiel das illustrieren: Vor einiger Zeit habe ich mit meiner Familie in den USA gelebt. Wir hatten kein Auto, mussten also das Problem lösen, zum Supermarkt zu kommen. Sie werden das als eher unerfreuliches Problem wiedererkennen, wenn Sie selbst schon einmal mit Rucksack und schreienden Kleinkindern zu Fuß circa drei Kilometer zum Supermarkt gegangen und vollbeladen, schweißgebadet und am Rande des Nervenzusammenbruchs zurückgewankt sind. Wir haben überlegt, ein Fahrrad anzuschaffen, sich gerade entwickelnde Carsharing-Angebote wahrzunehmen oder schlicht in den sauren Apfel zu beißen und ein Taxi zu nehmen. Irgendwie war das alles nichts – und dann haben wir eines Tages den Lastwagen eines Lieferdienstes für Lebensmittel gesehen. In dem Moment fiel es uns wie Schuppen von den Augen: Das Problem war nicht, wie wir zum Supermarkt kommen. Das wirkliche Problem war, wie die Lebensmittel in unsere Wohnung kommen würden.

Im Nachhinein ist das offensichtlich. Aber vielleicht haben Sie selbst auch schon einmal festgestellt, dass Sie ein falsches Problem lösen wollten. Als Sie das dann verstanden haben, war auf einmal alles viel einfacher. Das richtige zu lösende Problem zu erkennen und zu verstehen wird in der Softwareindustrie als Requirements Engineering bezeichnet und stellt immer die erste große Hürde für ein Softwareprojekt dar. Requirements Engineering ist dabei eine Menge von Aktivitäten, die sich um das Erheben, Verstehen, Aufschreiben und Überprüfen von Bedürfnissen und Anforderungen kümmert; und wenn Sie schon einmal etwas von Design Thinking gehört haben, werden Sie sich erinnern, dass das Verständnis des richtigen zu lösenden Problems dort eine ganz große Rolle spielt. In der sogenannten agilen Softwareentwicklung spricht man deswegen auch während der Systementwicklung ununterbrochen mit der Auftraggeberin oder dem Auftraggeber beziehungsweise den zukünftigen Nutzerinnen und Nutzern eines Systems, um sicherzustellen, dass die richtige Lösung gebaut wird. Das ist auch deswegen eine gute Idee, weil die Anforderungen der Auftraggeberin oder des Auftraggebers nicht ein für alle Mal festgezurrt sind, sondern sich während der Entwicklungs- und Lebenszeit eines Softwareprodukts kontinuierlich und deutlich verändern. Es gibt Tausende Beispiele für Softwareentwicklungsprojekte, die deswegen gescheitert sind, weil die Bedürfnisse und Anforderungen nicht richtig verstanden, nicht richtig aufgeschrieben und während der unterschiedlichen Aktivitäten der Entwicklung nicht richtig kommuniziert wurden. Jede Informatikerin, jeder Informatiker kennt die Analogie der missglückten Entwicklung einer Schaukel, die bildhaft sehr schön dargestellt werden kann.

Wenn das Bedürfnis und das richtige zu lösende Problem identifiziert sind, kann man in einem zweiten Schritt darüber nachdenken, die entsprechenden Anforderungen aufzuschreiben. Warum? Weil man dann den Entwicklungsprozess strukturieren, die Arbeit in Teams aufteilen und vor allem am Ende die Korrektheit überprüfen kann. Wenn es keine klaren Anforderungen gibt, die das Sollverhalten beschreiben, dann kann es eigentlich keine Korrektheit geben. Natürlich können die Nutzerinnen und Nutzer eines Programms dieses dann benutzen und sich ein Urteil erlauben, ob es das tut, was es tun soll – aber das ist hochgradig unsystematisch, und eigentlich möchte man sie nicht mit Programmen belästigen, bei denen man weiß, dass sie noch sehr unausgegoren sind.

Fehler in Programmen finden: Testen

Um schon etwas früher überprüfen zu können, ob das System das tut, was es tun soll, geht man in der Praxis etwas anders vor: Man geht davon aus, dass die richtigen Anforderungen richtig notiert sind, und darauf aufbauend testen schon die Entwicklerinnen und Entwickler das Programm. Testen bedeutet, dass man sich für einige wenige repräsentative Eingaben vorab überlegt, was die erwünschte Ausgabe des Programms sein soll, also das Sollverhalten für diese Eingabe. Die erwünschte Ausgabe lässt sich aus den Anforderungen ableiten. Dann führt man das Programm mit genau dieser Eingabe aus und vergleicht die Ausgabe des Programms mit der Ausgabe, die man sich gewünscht hätte.

Klingt einfach, ist aber in der Praxis sehr schwierig. Denn „repräsentative“ Eingaben und damit „gute“ Testfälle zu finden, wie wir das verlangt haben, ist aus verschiedenen Gründen außerordentlich herausfordernd. Einer der Gründe ist die unglaubliche Menge möglicher Eingaben: Wenn Sie nur eine ganze Zahl als Eingabe haben, wie in unserem Quadratfunktionsbeispiel oben, sind das schon 264 Möglichkeiten, das ist eine unvorstellbar große Zahl. Für die Summe aus zwei Summanden sind es schon 2128 Möglichkeiten. Im Universum gibt es geschätzt 2350 Atome, eine Zahl, die wir als Kombination von nur vier Zahlen als Eingaben schon überschreiten.

An dieser Stelle ist es interessant, noch einmal über das Erdnussbutter-Marmeladen-Sandwich und vor allem die Fußgängererkennung und das Maschinenlernen nachzudenken. Wir haben oben erläutert, dass Maschinenlernen unter anderem dann eingesetzt wird, wenn man den Lösungsweg nicht genau kennt. Gleichzeitig haben wir im Zusammenhang der Erkennung von Fußgängerinnen und Fußgängern gesehen, dass es fast unmöglich ist, das Konzept „Fußgänger“ ganz präzise zu fassen. Genau deswegen, haben wir argumentiert, wird in solchen Fällen Maschinenlernen eingesetzt. Wir benutzen Maschinenlernen also nicht nur dann, wenn das Wie schwierig zu fassen ist, sondern auch, wenn wir das Problem, also das Was, nicht genau beschreiben können. Hier passiert etwas wirklich Verrücktes: Für viele maschinengelernte Funktionen gibt es gar keine präzise Beschreibung des Sollverhaltens – denn wenn es die gäbe, hätten wir vielleicht nicht Maschinenlernen eingesetzt, sondern eine präzise Beschreibung des Sollverhaltens von Hand in die einzelnen Schritte eines Programms umgesetzt!

Wenn es aber nun keine präzise Beschreibung des Sollverhaltens gibt, wie können wir es dann systematisch testen? Die kurze Antwort ist: Das können wir zumindest im Allgemeinen nicht, und wir können es aus den genannten Gründen auch gar nicht können. Ingenieurinnen und Ingenieure verwenden verschiedene Tricks, um dieser Tatsache zu begegnen, aber hier sehen wir einen ganz frappanten Unterschied zwischen traditioneller Softwareentwicklung und Software, die auf Maschinenlernen basiert. Das erklärt auch, warum sich heute viele kluge Köpfe um das Problem der sogenannten Absicherung von KI kümmern, von der Sie im Zusammenhang mit autonomen Fahrzeugen vielleicht schon gelesen haben.

Neben dem Testen gibt es noch andere sehr nützliche Verfahren zum Finden von Fehlern, etwa das Lesen von Programmen, ohne sie auszuführen. In der Praxis zeigt sich, dass diese Verfahren sehr gut funktionieren. Nur der Vollständigkeit halber wollen wir festhalten, dass auch das leider für maschinengelernte Funktionen nicht funktionieren kann, weil diese Funktionen ja gerade keine einzelnen Schritte beinhalten, die ein Mensch nachvollziehen könnte.

Strukturieren von Systemen: Softwaredesign

Bis jetzt haben wir drei Aktivitäten des Software Engineering kennengelernt: das sogenannte Requirements Engineering, das sich mit den durch ein Programm zu erfüllenden Bedürfnissen und Anforderungen beschäftigt; mit der Programmierung; und mit der Überprüfung von Programmen, dem Testen. Informatikerinnen und Informatiker unterscheiden bei der Konstruktion von Softwaresystemen manchmal zwischen dem sogenannten Programmieren im Kleinen und dem Programmieren im Großen. Die Art von Programmen, die wir bisher kennengelernt haben, implementieren im Kleinen einen Algorithmus oder basieren auf Maschinenlernen. Wir haben angenommen, dass wir für ein gegebenes Problem immer direkt ein Programm schreiben können. Wenn die Probleme jetzt aber sehr groß werden, werden auch die Programme sehr groß. Um dieser Komplexität Herr zu werden, müssen Probleme in Teilprobleme zerlegt werden, die ihrerseits so lange zerlegt werden müssen, bis handhabbare Teile entstehen. Das wird Programmieren im Großen genannt. Für die identifizierten Teile können dann individuell Lösungen in Form von Programmen implementiert werden, und die Teile werden dann hinterher zusammengesetzt (und das Zusammengesetzte muss seinerseits wieder getestet werden). Das haben wir auf einer eher feingranularen Ebene schon diskutiert, als wir oben die summe-Funktion in der quadrat2-Funktion verwendet haben. Hier geht es mir um eine gröbergranulare Ebene, etwa um das Auto, in dem heute ungefähr 100 Computer ihren Dienst versehen – auf denen jeweils mehrere Programme mit insgesamt Hunderten Millionen von Codezeilen ablaufen, deren Integration dann all die Funktionalitäten ergibt, die moderne Fahrzeuge anbieten. Wie man diese Funktionalität strukturiert, ist Aufgabe von Softwaredesignerinnen und -designern.

Der Punkt, auf den ich hinauswill, ist der folgende: Das Zerlegen eines Problems in Teilprobleme und das Zerlegen eines großen Systems in handhabbare Teilsysteme sind als solche kreative Akte, genau so, wie es das Programmieren und übrigens auch die Anforderungserhebung und das Testen sind. Neben Anforderungserhebung und Test beinhaltet das Erstellen von Software auch das Festlegen einer Struktur, der sogenannten Architektur eines Systems. Die ist nicht nur aus Gründen der Organisation der Aktivitäten der Systemstellung wichtig, sondern beeinflusst direkt fast alle Gütekriterien, über die wir oben gesprochen haben. Es lohnt sich zu wiederholen, dass das Programmieren und auch das Maschinenlernen nur einen sehr kleinen Teil der Aktivitäten ausmachen, die für das Bauen, Überprüfen und Warten großer Softwaresysteme notwendig sind.

Software Engineering

Software Engineering bezeichnet die Summe dieser Aktivitäten: Aufnehmen, Aufschreiben, Priorisieren und Überprüfen von Anforderungen; Zerlegen des Gesamtproblems in Teilprobleme und Design einer Architektur; Implementierung der Lösungen für Teilprobleme durch Programmieren oder Maschinenlernen; Test dieser Lösung gegen die Anforderungen. Und Software Engineering kümmert sich darum, wie diese Aktivitäten organisiert werden – sicher haben Sie im Zusammenhang mit Software schon einmal von „agiler Entwicklung“ oder „Scrum“ gehört. Dazu kommen noch weitere Aktivitäten, die wir schon angedeutet haben: Die Wartung solcher Systeme, die die Fehlerbehebung und die Weiterentwicklung beinhaltet; das sehr anspruchsvolle Management unterschiedlicher Versionen von Software; das Sicherstellen von Sicherheit und Privatheit sowie das Verstehen und Beheben von Sicherheits- und Privatheitsproblemen und so weiter. Schließlich beinhaltet Software Engineering auch das Strukturieren, Speichern und Verwalten von Daten. Da deren Beschreibung einen eigenen Text ungefähr der gleichen Länge wie des vorliegenden ergeben würde, verschieben wir sie auf einen zukünftigen Beitrag.