Kapitel 1. Was ist Softwareentwicklung?
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Nichts ist auf Stein gebaut; alles ist auf Sand gebaut, aber wir müssen so bauen, als ob der Sand Stein wäre.
Jorge Luis Borges
Wir sehen drei entscheidende Unterschiede zwischen Programmierung und Softwareentwicklung: Zeit, Umfang und die Kompromisse, die dabei eine Rolle spielen. Bei einem Softwareentwicklungsprojekt müssen sich die Ingenieure mehr Gedanken über den Zeitablauf und eventuelle Änderungen machen. In einer Softwareentwicklungsorganisation müssen wir uns mehr Gedanken über Umfang und Effizienz machen, sowohl für die Software, die wir produzieren, als auch für die Organisation, die sie herstellt. Und schließlich müssen wir als Softwareentwickler/innen komplexere Entscheidungen treffen, bei denen mehr auf dem Spiel steht und die oft auf ungenauen Zeit- und Wachstumsschätzungen beruhen.
Bei Google sagen wir manchmal: "Softwareentwicklung ist zeitlich integrierte Programmierung." Das Programmieren ist sicherlich ein wichtiger Teil der Softwareentwicklung: Schließlich ist das Programmieren die Art und Weise, wie man neue Software überhaupt erst erzeugt. Wenn du diese Unterscheidung akzeptierst, wird auch klar, dass wir zwischen Programmieraufgaben (Entwicklung) und Aufgaben der Softwareentwicklung (Entwicklung, Änderung, Wartung) unterscheiden müssen. Die Hinzufügung der Zeit fügt der Programmierung eine wichtige neue Dimension hinzu. Würfel sind keine Quadrate, Entfernung ist keine Geschwindigkeit. Softwareentwicklung ist nicht Programmieren.
Eine Möglichkeit, die Auswirkungen der Zeit auf ein Programm zu erkennen, ist die Frage: "Wie lang ist die erwartete Lebensdauer1 deines Codes?" Vernünftige Antworten auf diese Frage variieren ungefähr um den Faktor 100.000. Es ist genauso vernünftig, sich einen Code vorzustellen, der nur ein paar Minuten halten muss, wie einen Code, der Jahrzehnte überdauert. Code am kurzen Ende des Spektrums wird im Allgemeinen nicht von der Zeit beeinflusst. Es ist unwahrscheinlich, dass du dich an eine neue Version der zugrunde liegenden Bibliotheken, des Betriebssystems (OS), der Hardware oder der Sprachversion für ein Programm anpassen musst, dessen Nutzen nur eine Stunde lang anhält. Diese kurzlebigen Systeme sind im Grunde "nur" ein Programmierproblem, so wie ein Würfel, der in einer Dimension weit genug zusammengedrückt wurde, ein Quadrat ist. Wenn wir diese Zeitspanne ausdehnen, um längere Lebensspannen zu ermöglichen, wird der Wandel immer wichtiger. Über einen Zeitraum von einem Jahrzehnt oder mehr werden sich die meisten Programmabhängigkeiten, ob implizit oder explizit, wahrscheinlich ändern. Diese Erkenntnis ist die Grundlage für die Unterscheidung zwischen Softwareentwicklung und Programmierung.
Diese Unterscheidung ist der Kern dessen, was wir Nachhaltigkeit für Software nennen. Dein Projekt ist nachhaltig, wenn du während der voraussichtlichen Lebensdauer deiner Software in der Lage bist, auf jede wertvolle Veränderung zu reagieren, sei es aus technischen oder aus geschäftlichen Gründen. Wichtig ist, dass wir nur nach der Fähigkeit suchen - du könntest dich entscheiden, eine bestimmte Aktualisierung nicht durchzuführen, entweder aus Mangel an Wert oder aus anderen Prioritäten.2 Wenn du grundsätzlich nicht in der Lage bist, auf eine Änderung der zugrundeliegenden Technologie oder der Produktausrichtung zu reagieren, gehst du ein hohes Risiko ein, in der Hoffnung, dass eine solche Änderung nie kritisch wird. Für kurzfristige Projekte mag das eine sichere Wette sein. Über mehrere Jahrzehnte hinweg ist es das aber wahrscheinlich nicht.3
Eine weitere Möglichkeit, die Softwareentwicklung zu betrachten, ist die Frage nach dem Umfang. Wie viele Menschen sind daran beteiligt? Welche Rolle spielen sie bei der Entwicklung und Wartung im Laufe der Zeit? Eine Programmieraufgabe ist oft ein individueller Akt, aber eine Aufgabe der Softwareentwicklung ist eine Teamleistung. Ein früher Versuch, Softwareentwicklung zu definieren, brachte eine gute Definition für diesen Gesichtspunkt hervor: "Die Entwicklung von Programmen mit mehreren Personen und mehreren Versionen."4 Das zeigt, dass der Unterschied zwischen Softwareentwicklung und Programmierung sowohl zeitlich als auch personell bedingt ist. Die Zusammenarbeit im Team bringt neue Probleme mit sich, bietet aber auch mehr Möglichkeiten, wertvolle Systeme zu entwickeln, als es ein einzelner Programmierer könnte.
Die Organisation des Teams, die Zusammensetzung des Projekts und die Richtlinien und Praktiken eines Softwareprojekts bestimmen diesen Aspekt der Komplexität der Softwareentwicklung. Diese Probleme hängen mit der Skalierung zusammen: Wird das Unternehmen mit dem Wachstum und der Ausweitung seiner Projekte effizienter bei der Softwareproduktion? Wird unser Entwicklungsworkflow effizienter, wenn wir wachsen, oder kosten uns unsere Richtlinien zur Versionskontrolle und unsere Teststrategien im Verhältnis mehr? Skalierungsfragen im Zusammenhang mit Kommunikation und menschlicher Skalierung werden schon seit den Anfängen der Softwareentwicklung diskutiert, die bis zum Mythical Man Month zurückreichen.5 Solche Skalierungsfragen sind oft eine Frage der Politik und von grundlegender Bedeutung für die Frage der Nachhaltigkeit von Software: Wie viel wird es kosten, die Dinge zu tun, die wir immer wieder tun müssen?
Wir können auch sagen, dass sich die Softwareentwicklung von der Programmierung durch die Komplexität der zu treffenden Entscheidungen und deren Einsatz unterscheidet. In der Softwareentwicklung sind wir regelmäßig gezwungen, Kompromisse zwischen verschiedenen Wegen zu bewerten, manchmal mit hohen Einsätzen und oft mit unvollkommenen Wertmaßstäben. Die Aufgabe eines Softwareentwicklers oder eines Leiters der Softwareentwicklung ist es, die Nachhaltigkeit und das Management der Skalierungskosten für das Unternehmen, das Produkt und den Entwicklungsworkflow anzustreben. Mit diesen Vorgaben im Hinterkopf solltest du deine Kompromisse bewerten und rationale Entscheidungen treffen. Manchmal schieben wir Änderungen in der Wartung auf oder nehmen sogar Maßnahmen an, die sich nicht gut skalieren lassen, weil wir wissen, dass wir diese Entscheidungen überdenken müssen. Bei diesen Entscheidungen sollten die aufgeschobenen Kosten explizit und klar benannt werden.
In der Softwareentwicklung gibt es nur selten eine Einheitslösung, und das gilt auch für dieses Buch. Bei einem Faktor von 100.000 für vernünftige Antworten auf die Frage "Wie lange wird diese Software leben?", einer Spanne von vielleicht einem Faktor 10.000 für "Wie viele Ingenieure gibt es in deinem Unternehmen?" und wer-weiß-wieviel für "Wie viele Rechenressourcen stehen für dein Projekt zur Verfügung?" werden sich die Erfahrungen von Google wahrscheinlich nicht mit deinen decken. In diesem Buch stellen wir vor, was wir bei der Entwicklung und Wartung von Software, die jahrzehntelang halten soll, mit Zehntausenden von Ingenieuren und weltumspannenden Rechenressourcen für gut befunden haben. Die meisten Praktiken, die wir in dieser Größenordnung für notwendig erachten, funktionieren auch bei kleineren Projekten: Betrachte dies als einen Bericht über ein technisches Ökosystem, von dem wir glauben, dass es gut ist, wenn du es vergrößerst. In einigen wenigen Fällen ist eine sehr große Größe mit eigenen Kosten verbunden, und wir wären froh, wenn wir keine zusätzlichen Kosten tragen müssten. Wir weisen darauf hin, um dich zu warnen. Wir hoffen, dass du eine bessere Lösung findest, wenn deine Organisation so groß wird, dass du dir Sorgen um diese Kosten machen musst.
Bevor wir uns mit den Einzelheiten der Teamarbeit, der Kultur, den Richtlinien und den Instrumenten befassen, wollen wir uns zunächst mit den Hauptthemen Zeit, Umfang und Kompromisse befassen.
Zeit und Wandel
Wenn ein Neuling programmieren lernt, wird die Lebensdauer des entstandenen Codes normalerweise in Stunden oder Tagen gemessen. Programmieraufgaben und -übungen werden in der Regel einmalig geschrieben, wenig bis gar nicht überarbeitet und schon gar nicht langfristig gepflegt. Diese Programme werden nach ihrer ersten Erstellung oft nicht mehr neu erstellt oder ausgeführt. In einem pädagogischen Umfeld ist das nicht verwunderlich. Vielleicht gibt es in der Sekundar- oder Hochschulbildung einen Projektkurs oder eine praktische Abschlussarbeit im Team. Wenn das der Fall ist, sind solche Projekte wahrscheinlich die einzige Zeit, in der der Code der Schüler/innen länger als einen Monat oder so lebt. Vielleicht müssen diese Entwickler/innen ihren Code überarbeiten, um auf veränderte Anforderungen zu reagieren, aber es ist unwahrscheinlich, dass sie mit größeren Veränderungen in ihrer Umgebung konfrontiert werden.
Wir finden Entwickler von kurzlebigem Code auch in den üblichen Branchen. Mobile Apps haben oft eine recht kurze Lebensdauer,6 und im Guten wie im Schlechten sind vollständige Überarbeitungen relativ häufig. Ingenieure in einem Startup in der Frühphase könnten sich zu Recht dafür entscheiden, sich auf unmittelbare Ziele statt auf langfristige Investitionen zu konzentrieren: Das Unternehmen lebt vielleicht nicht lange genug, um die Vorteile einer Infrastrukturinvestition zu ernten, die sich nur langsam auszahlt. Ein Serienentwickler in einem Start-up kann durchaus 10 Jahre Entwicklungserfahrung haben und nur wenig oder gar keine Erfahrung in der Wartung von Software, die länger als ein oder zwei Jahre existieren soll.
Am anderen Ende des Spektrums haben einige erfolgreiche Projekte eine praktisch unbegrenzte Lebensdauer: Wir können das Ende der Google-Suche, des Linux-Kernels oder des Apache HTTP Server-Projekts nicht vorhersagen. Bei den meisten Google-Projekten müssen wir davon ausgehen, dass sie auf unbestimmte Zeit leben - wir können nicht vorhersagen, wann wir unsere Abhängigkeiten, Sprachversionen usw. nicht mehr aktualisieren müssen. Mit zunehmender Lebensdauer fühlen sich diese langlebigen Projekte irgendwann anders an als Programmieraufgaben oder die Entwicklung von Start-ups.
Abbildung 1-1 zeigt zwei Softwareprojekte an den entgegengesetzten Enden des Spektrums der "erwarteten Lebensdauer". Welche Art von Wartung ist für einen Programmierer, der an einer Aufgabe mit einer erwarteten Lebensdauer von Stunden arbeitet, angemessen zu erwarten? Das heißt, wenn eine neue Version deines Betriebssystems herauskommt, während du an einem Python-Skript arbeitest, das nur einmal ausgeführt wird, solltest du dann deine Arbeit abbrechen und ein Upgrade durchführen? Natürlich nicht: Das Upgrade ist nicht kritisch. Aber wenn die Google-Suche auf einer Version unseres Betriebssystems aus den 1990er Jahren festsitzt, wäre das ein eindeutiges Problem.
Die niedrigen und hohen Punkte auf dem Spektrum der erwarteten Lebensdauer deuten darauf hin, dass es irgendwo einen Übergang gibt. Irgendwo auf der Strecke zwischen einem einmaligen Programm und einem Projekt, das über Jahrzehnte läuft, findet ein Übergang statt: Ein Projekt muss beginnen, auf sich verändernde externe Faktoren zu reagieren.7 Für jedes Projekt, das nicht von Anfang an für Upgrades geplant wurde, ist dieser Übergang wahrscheinlich aus drei Gründen sehr schmerzhaft, die sich gegenseitig verstärken:
-
Du führst eine Aufgabe aus, die für dieses Projekt noch nicht gemacht wurde; es sind weitere versteckte Annahmen eingebaut worden.
-
Die Ingenieure, die die Aufrüstung durchführen, haben wahrscheinlich keine Erfahrung mit dieser Art von Aufgabe.
-
Der Umfang des Upgrades ist oft größer als üblich, da die Upgrades für mehrere Jahre auf einmal durchgeführt werden, anstatt schrittweise zu erfolgen.
Nachdem man ein solches Upgrade einmal durchgeführt hat (oder auf halbem Weg aufgegeben hat), ist es nur allzu verständlich, dass man die Kosten für ein weiteres Upgrade überschätzt und beschließt: "Nie wieder". Unternehmen, die zu diesem Schluss kommen, verpflichten sich am Ende dazu, alles wegzuwerfen und ihren Code neu zu schreiben oder nie wieder ein Upgrade durchzuführen. Anstatt eine schmerzhafte Aufgabe auf natürliche Weise zu vermeiden, ist es manchmal verantwortungsvoller, in eine weniger schmerzhafte Lösung zu investieren. Es hängt alles von den Kosten deines Upgrades, dem Wert, den es bringt, und der erwarteten Lebensdauer des betreffenden Projekts ab.
Wenn du nicht nur das erste große Upgrade überstehst, sondern den Punkt erreichst, an dem du auch in Zukunft zuverlässig auf dem neuesten Stand bleiben kannst, ist das der Kern der langfristigen Nachhaltigkeit deines Projekts. Nachhaltigkeit erfordert die Planung und das Management der Auswirkungen der erforderlichen Veränderungen. Wir glauben, dass wir bei vielen Projekten bei Google diese Art von Nachhaltigkeit erreicht haben, vor allem durch Versuch und Irrtum.
Wie unterscheidet sich also die kurzfristige Programmierung von der Erstellung von Code mit einer viel längeren Lebensdauer? Mit der Zeit müssen wir uns des Unterschieds zwischen "funktioniert zufällig" und "ist wartbar" viel bewusster werden. Es gibt keine perfekte Lösung, um diese Probleme zu erkennen. Das ist bedauerlich, denn die langfristige Wartbarkeit von Software ist ein ständiger Kampf.
Hyrum's Gesetz
Wenn du ein Projekt pflegst, das von anderen Ingenieurinnen und Ingenieuren genutzt wird, ist die wichtigste Lektion über "es funktioniert" im Gegensatz zu "es ist wartbar" das, was wir als Hyrum's Law bezeichnen:
Bei einer ausreichenden Anzahl von Nutzern einer API spielt es keine Rolle, was du im Vertrag versprichst: Alle beobachtbaren Verhaltensweisen deines Systems werden von jemandem abhängen .
Unserer Erfahrung nach ist dieses Axiom ein dominierender Faktor in jeder Diskussion über die Veränderung von Software im Laufe der Zeit. Es ist konzeptionell mit der Entropie vergleichbar: Bei Diskussionen über Veränderung und Wartung im Laufe der Zeit muss das Hyrum'sche Gesetz berücksichtigt werden8 so wie man bei Diskussionen über Effizienz oder Thermodynamik die Entropie im Auge behalten muss. Nur weil die Entropie niemals abnimmt, heißt das nicht, dass wir nicht versuchen sollten, effizient zu sein. Nur weil das Hyrum'sche Gesetz bei der Wartung von Software zur Anwendung kommt, heißt das nicht, dass wir es nicht einplanen oder versuchen können, es besser zu verstehen. Wir können es abmildern, aber wir wissen, dass wir es nie aus der Welt schaffen können.
Hyrum's Law steht für die praktische Erkenntnis, dass wir - selbst bei den besten Absichten, den besten Ingenieuren und soliden Methoden für die Codeüberprüfung - nicht davon ausgehen können, dass die veröffentlichten Verträge oder bewährten Methoden perfekt eingehalten werden. Als API-Besitzer gewinnst du ein gewisses Maß an Flexibilität und Freiheit, wenn du dich über Schnittstellenversprechen im Klaren bist, aber in der Praxis hängt die Komplexität und Schwierigkeit einer bestimmten Änderung auch davon ab, wie nützlich ein Nutzer ein bestimmtes beobachtbares Verhalten deiner API findet. Wenn sich die Nutzer nicht auf solche Dinge verlassen können, wird deine API leicht zu ändern sein. Mit genügend Zeit und genügend Nutzern wird selbst die harmloseste Änderung etwas kaputt machen;9 Bei der Analyse des Werts dieser Änderung muss die Schwierigkeit berücksichtigt werden, diese Fehler zu untersuchen, zu identifizieren und zu beheben.
Beispiel: Hash-Bestellung
Betrachte das Beispiel der Hash-Iterationsreihenfolge. Wenn wir fünf Elemente in eine hashbasierte Menge einfügen, in welcher Reihenfolge bekommen wir sie wieder heraus?
>>>
for
i
in
{
"apple"
,
"banana"
,
"carrot"
,
"durian"
,
"eggplant"
}:
(
i
)
...
durian
carrot
apple
eggplant
banana
Die meisten Programmierer wissen, dass Hash-Tabellen nicht offensichtlich geordnet sind. Nur wenige wissen, ob die Hash-Tabelle, die sie verwenden , diese besondere Ordnung für immer bietet. Das mag unscheinbar erscheinen, aber in den letzten zehn Jahren haben sich die Erfahrungen der Computerindustrie mit solchen Typen weiterentwickelt:
-
Hash Flooding10 Angriffe bieten einen erhöhten Anreiz für nicht-deterministische Hash-Iterationen.
-
Potenzielle Effizienzgewinne durch die Forschung an verbesserten Hash-Algorithmen oder Hash-Containern erfordern Änderungen an der Reihenfolge der Hash-Iterationen.
-
Gemäß dem Hyrum'schen Gesetz werden Programmierer/innen Programme schreiben, die von der Reihenfolge abhängen, in der eine Hashtabelle durchlaufen wird, wenn sie die Möglichkeit dazu haben.
Wenn du also einen Experten fragst: "Kann ich eine bestimmte Ausgabereihenfolge für meinen Hash-Container annehmen?", wird er vermutlich "Nein" sagen. Das ist im Großen und Ganzen richtig, aber vielleicht zu einfach. Eine differenziertere Antwort lautet: "Wenn dein Code kurzlebig ist und sich weder die Hardware noch die Laufzeit der Sprache oder die Wahl der Datenstruktur ändert, ist eine solche Annahme in Ordnung. Wenn du nicht weißt, wie lange dein Code leben wird, oder du nicht versprechen kannst, dass sich nichts, von dem du abhängig bist, jemals ändern wird, ist eine solche Annahme falsch." Und selbst wenn deine eigene Implementierung nicht von der Reihenfolge der Hash-Container abhängt, kann sie von anderem Code verwendet werden, der implizit eine solche Abhängigkeit schafft. Wenn deine Bibliothek zum Beispiel Werte in einer Remote Procedure Call (RPC)-Antwort serialisiert, kann es sein, dass der RPC-Aufrufer von der Reihenfolge dieser Werte abhängig ist .
Dies ist ein sehr einfaches Beispiel für den Unterschied zwischen "es funktioniert" und "es ist richtig". Bei einem kurzlebigen Programm wird es keine technischen Probleme geben, wenn du dich auf die Iterationsreihenfolge deiner Container verlässt. Bei einem Softwareentwicklungsprojekt hingegen ist die Abhängigkeit von einer bestimmten Reihenfolge ein Risiko - mit der Zeit wird es sich lohnen, die Iterationsreihenfolge zu ändern. Dieser Wert kann auf verschiedene Weise zum Ausdruck kommen, sei es aus Gründen der Effizienz, der Sicherheit oder einfach nur, um die Datenstruktur zukunftssicher zu machen und zukünftige Änderungen zu ermöglichen. Wenn dieser Wert deutlich wird, musst du die Kompromisse zwischen diesem Wert und dem Schmerz, den deine Entwickler oder Kunden erleiden, abwägen.
In einigen Sprachen wird die Hash-Reihenfolge zwischen Bibliotheksversionen oder sogar zwischen verschiedenen Ausführungen desselben Programms zufällig angeordnet, um Abhängigkeiten zu vermeiden. Aber selbst das kann noch zu Überraschungen führen: Es gibt Code, der die Hash-Iterationsreihenfolge als ineffizienten Zufallszahlengenerator nutzt. Diese Zufälligkeit jetzt zu entfernen, würde die Nutzerinnen und Nutzer kaputt machen. So wie die Entropie in jedem thermodynamischen System zunimmt, gilt das Hyrum'sche Gesetz für jedes beobachtbare Verhalten.
Wenn wir über die Unterschiede zwischen Code nachdenken, der mit einer "funktioniert jetzt"- und einer "funktioniert auf unbestimmte Zeit"-Mentalität geschrieben wurde, können wir einige klare Zusammenhänge erkennen. Wenn wir Code als ein Artefakt mit einer (sehr) variablen Lebenszeitanforderung betrachten, können wir anfangen, Programmierstile zu kategorisieren: Code, der von spröden und unveröffentlichten Merkmalen seiner Abhängigkeiten abhängt, wird wahrscheinlich als "hacky" oder "clever" beschrieben, während Code , der bewährten Methoden folgt und für die Zukunft geplant wurde, eher als "sauber" und "wartbar" beschrieben wird. Beide haben ihre Berechtigung, aber welche du wählst, hängt entscheidend von der erwarteten Lebensdauer des betreffenden Codes ab. Wir haben uns angewöhnt zu sagen: "Es ist Programmierung, wenn 'clever' ein Kompliment ist, aber es ist Softwareentwicklung, wenn 'clever' eine Anschuldigung ist."
Warum nicht einfach "Nichts ändert sich" anstreben?
In all diesen Diskussionen über Zeit und die Notwendigkeit, auf Veränderungen zu reagieren, ist die Annahme enthalten, dass Veränderungen notwendig sein könnten. Ist sie das?
Wie bei fast allem in diesem Buch kommt es darauf an. Wir bekennen uns bereitwillig zu der Aussage: "Bei den meisten Projekten muss über einen ausreichend langen Zeitraum hinweg alles, was darunter liegt, geändert werden." Wenn du ein Projekt hast, das in reinem C geschrieben ist und keine externen Abhängigkeiten hat (oder nur externe Abhängigkeiten, die große Langzeitstabilität versprechen, wie POSIX), kannst du vielleicht jede Form von Refactoring oder schwierigem Upgrade vermeiden. C ist sehr stabil - in vielerlei Hinsicht ist das sein Hauptzweck.
Die meisten Projekte sind viel stärker von Veränderungen der zugrunde liegenden Technologie betroffen. Die meisten Programmiersprachen und Laufzeiten ändern sich viel stärker als C. Sogar Bibliotheken, die in reinem C implementiert sind, können sich ändern, um neue Funktionen zu unterstützen, was sich auf nachgeschaltete Benutzer auswirken kann. Sicherheitsprobleme werden in allen möglichen Technologien aufgedeckt, von Prozessoren über Netzwerkbibliotheken bis hin zum Anwendungscode. Jede Technologie, von der dein Projekt abhängt, birgt ein (hoffentlich geringes) Risiko, dass sie kritische Fehler und Sicherheitslücken enthält, die erst ans Licht kommen, wenn du dich schon auf sie verlassen hast. Wenn du nicht in der Lage bist, einen Patch für Heartbleed zu installieren oder Probleme mit spekulativer Ausführung wie Meltdown und Spectre zu entschärfen, weil du davon ausgegangen bist (oder versprochen hast), dass sich nie etwas ändern wird, ist das ein großes Wagnis.
Effizienzsteigerungen machen das Bild noch komplizierter. Wir wollen unsere Rechenzentren mit kosteneffizienter Ausrüstung ausstatten und vor allem die CPU-Effizienz steigern. Algorithmen und Datenstrukturen aus der Anfangszeit von Google sind jedoch auf modernen Geräten einfach weniger effizient: Eine verknüpfte Liste oder ein binärer Suchbaum funktionieren zwar immer noch gut, aber die immer größer werdende Kluft zwischen CPU-Zyklen und Speicherlatenz hat Auswirkungen darauf, wie "effizienter" Code aussieht. Mit der Zeit kann der Wert eines Upgrades auf neuere Hardware sinken, ohne dass die Software entsprechend angepasst wird. Die Abwärtskompatibilität stellt sicher, dass ältere Systeme noch funktionieren, aber das ist keine Garantie dafür, dass alte Optimierungen noch hilfreich sind. Wenn du nicht willens oder in der Lage bist, solche Chancen zu nutzen, riskierst du hohe Kosten. Solche Effizienzprobleme sind besonders subtil: Der ursprüngliche Entwurf mag vollkommen logisch gewesen sein und bewährten Methoden gefolgt sein. Erst nach einer Reihe von rückwärtskompatiblen Änderungen wird eine neue, effizientere Option wichtig. Es wurden zwar keine Fehler gemacht, aber der Lauf der Zeit macht Änderungen trotzdem wertvoll.
Bedenken wie die eben genannten sind der Grund, warum langfristige Projekte, die nicht in Nachhaltigkeit investiert haben, große Risiken bergen. Wir müssen in der Lage sein, auf diese Art von Problemen zu reagieren und diese Chancen zu nutzen, unabhängig davon, ob sie uns direkt betreffen oder sich nur in der vorübergehenden Schließung der Technologie manifestieren, auf die wir bauen. Veränderung ist nicht per se gut. Wir sollten uns nicht nur um des Wandels willen verändern. Aber wir müssen fähig sein, uns zu verändern. Wenn wir diese Notwendigkeit in Betracht ziehen, sollten wir auch darüber nachdenken, ob wir in diese Fähigkeit investieren sollten, um sie billig zu machen. Wie jeder Systemadministrator weiß, ist es eine Sache, theoretisch zu wissen, dass man von einem Band wiederherstellen kann, aber eine andere, in der Praxis genau zu wissen, wie man es macht und wie viel es kostet, wenn es notwendig wird. Praxis und Fachwissen sind die wichtigsten Faktoren für Effizienz und Zuverlässigkeit.
Maßstab und Effizienz
Wie im Buch Site Reliability Engineering (SRE) beschrieben,11 Das gesamte Produktionssystem von Google gehört zu den komplexesten Maschinen, die von der Menschheit geschaffen wurden. Die Komplexität, die mit dem Aufbau einer solchen Maschine und ihrem reibungslosen Betrieb verbunden ist, hat unzählige Stunden des Nachdenkens, der Diskussion und der Umgestaltung durch Experten in unserem Unternehmen und auf der ganzen Welt erfordert. Deshalb haben wir bereits ein Buch darüber geschrieben, wie komplex es ist, diese Maschine in diesem Umfang am Laufen zu halten.
Ein Großteil dieses Buches befasst sich mit der Komplexität der Organisation, die eine solche Maschine produziert, und mit den Prozessen, die wir nutzen, um diese Maschine über die Zeit am Laufen zu halten. Betrachte noch einmal das Konzept der Nachhaltigkeit der Codebasis: "Die Codebasis deines Unternehmens ist dann nachhaltig, wenn du in der Lage bist, alle Dinge, die du ändern solltest, sicher zu ändern, und zwar über die gesamte Lebensdauer deiner Codebasis. In der Diskussion über die Fähigkeit ist auch eine Diskussion über die Kosten versteckt: Wenn eine Änderung mit zu hohen Kosten verbunden ist, wird sie wahrscheinlich aufgeschoben. Wenn die Kosten im Laufe der Zeit überproportional ansteigen, ist der Vorgang eindeutig nicht skalierbar.12 Irgendwann wird die Zeit kommen und etwas Unerwartetes wird eintreten, das du unbedingt ändern musst. Wenn sich der Umfang deines Projekts verdoppelt und du diese Aufgabe erneut durchführen musst, wird sie dann doppelt so arbeitsintensiv sein? Wirst du beim nächsten Mal überhaupt die nötigen personellen Ressourcen haben, um das Problem zu lösen?
Die Personalkosten sind nicht die einzige endliche Ressource, die skaliert werden muss. Genauso wie die Software selbst mit den traditionellen Ressourcen wie Rechenleistung, Speicher, Speicherung und Bandbreite skalieren muss, muss auch die Entwicklung dieser Software skalieren, sowohl in Bezug auf den Zeitaufwand der Mitarbeiter als auch auf die Rechenressourcen, die deinen Entwicklungsworkflow antreiben. Wenn die Rechenressourcen für deinen Testcluster überproportional ansteigen und jedes Quartal mehr Rechenressourcen pro Person verbraucht werden, bist du auf einem unhaltbaren Weg und musst bald etwas ändern.
Schließlich muss auch das wertvollste Gut eines Softwareunternehmens - die Codebasis selbst - skalierbar sein. Wenn dein Build- oder Versionskontrollsystem im Laufe der Zeit superlinear skaliert, z. B. aufgrund des Wachstums und der zunehmenden Änderungshistorie, kann ein Punkt kommen, an dem du einfach nicht mehr weitermachen kannst. Viele Fragen wie "Wie lange dauert es, einen vollständigen Build zu erstellen?", "Wie lange dauert es, eine neue Kopie des Repositorys zu erstellen?" oder "Wie viel kostet es, auf eine neue Sprachversion zu aktualisieren?" werden nicht aktiv überwacht und ändern sich nur langsam. Es ist viel zu einfach, dass sich die Probleme langsam verschlimmern und sich nicht in einem einzigen Krisenmoment manifestieren. Nur mit einem unternehmensweiten Bewusstsein und der Verpflichtung zur Skalierung kannst du diese Probleme in den Griff bekommen.
Alles, worauf sich dein Unternehmen bei der Erstellung und Pflege von Code verlässt, sollte in Bezug auf die Gesamtkosten und den Ressourcenverbrauch skalierbar sein. Vor allem sollte alles, was dein Unternehmen wiederholt tun muss, in Bezug auf den menschlichen Aufwand skalierbar sein. Viele gängige Richtlinien scheinen in diesem Sinne nicht skalierbar zu sein.
Politiken, die nicht skalierbar sind
Mit ein wenig Übung wird es einfacher, Richtlinien mit schlechten Skalierungseigenschaften zu erkennen. Meistens kann man diese erkennen, indem man die Arbeit betrachtet, die einem einzelnen Ingenieur auferlegt wird, und sich vorstellt, dass die Organisation um das 10- oder 100-fache wächst. Wenn wir 10-mal so groß sind, werden wir dann 10-mal so viel Arbeit haben, mit der unser Beispielingenieur mithalten muss? Wächst die Menge der Arbeit, die unser Ingenieur erledigen muss, mit der Größe des Unternehmens? Nimmt die Arbeit mit der Größe der Codebasis zu? Wenn beides zutrifft, verfügen wir dann über Mechanismen, um diese Arbeit zu automatisieren oder zu optimieren? Wenn nicht, haben wir ein Skalierungsproblem.
Betrachte einen traditionellen Ansatz für die Verwerfung. Wir werden in Kapitel 15 viel mehr über die Verwerfung diskutieren, aber der übliche Ansatz zur Verwerfung dient als gutes Beispiel für Skalierungsprobleme. Es wurde ein neues Widget entwickelt. Es wird beschlossen, dass alle das neue Widget verwenden sollen und das alte nicht mehr. Um dies zu motivieren, sagen die Projektleiter: "Wir werden das alte Widget am 15. August löschen; stellt sicher, dass ihr auf das neue Widget umgestellt habt."
Diese Art von Ansatz mag in einer kleinen Softwareumgebung funktionieren, schlägt aber schnell fehl, wenn sowohl die Tiefe als auch die Breite des Abhängigkeitsgraphen zunimmt. Teams sind von einer immer größeren Anzahl von Widgets abhängig, und ein einziger Build-Break kann einen wachsenden Prozentsatz des Unternehmens betreffen. Um diese Probleme auf skalierbare Weise zu lösen, müssen wir die Art und Weise, wie wir die Abkündigung durchführen, ändern: Anstatt die Migrationsarbeit an die Kunden weiterzugeben, können die Teams sie selbst internalisieren, mit allen damit verbundenen Skaleneffekten.
2012 haben wir versucht, dem ein Ende zu setzen, indem wir Regeln aufgestellt haben, die die Abwanderung eindämmen: Infrastrukturteams müssen die Arbeit, ihre internen Nutzer auf neue Versionen umzustellen, selbst erledigen oder die Aktualisierung an Ort und Stelle vornehmen, und zwar auf rückwärtskompatible Weise. Diese Regel, die wir "Churn Rule" genannt haben, lässt sich besser skalieren: Abhängige Projekte müssen nicht mehr immer mehr Aufwand betreiben, nur um mitzuhalten. Wir haben auch gelernt, dass es besser ist, eine Gruppe von Experten mit der Durchführung der Änderungen zu beauftragen, als von jedem Nutzer mehr Wartungsaufwand zu verlangen: Die Experten verbringen einige Zeit damit, das gesamte Problem gründlich kennenzulernen, und wenden dieses Wissen dann auf jedes Teilproblem an. Wenn du die Nutzer/innen zwingst, auf die Abwanderung zu reagieren, bedeutet das, dass jedes betroffene Team eine schlechtere Leistung erbringt, sein unmittelbares Problem löst und dann das jetzt nutzlose Wissen wegwirft. Fachwissen skaliert besser.
Die traditionelle Verwendung von Entwicklungszweigen ist ein weiteres Beispiel für eine Strategie, die Probleme mit der Skalierung mit sich bringt. Ein Unternehmen könnte feststellen, dass das Zusammenführen großer Funktionen in den Stamm das Produkt destabilisiert hat, und zu dem Schluss kommen: "Wir brauchen eine strengere Kontrolle darüber, wann Dinge zusammengeführt werden. Wir sollten weniger häufig fusionieren." Das führt schnell dazu, dass jedes Team oder jedes Feature einen eigenen Entwicklungszweig hat. Sobald ein Zweig als "vollständig" eingestuft wird, wird er getestet und mit dem Stamm zusammengeführt, was für andere Ingenieure, die noch an ihrem Entwicklungszweig arbeiten, potenziell teure Arbeit in Form von Neusynchronisierung und Tests nach sich zieht. Eine solche Zweigverwaltung kann in einem kleinen Unternehmen mit 5 bis 10 solcher Zweige funktionieren. Wenn die Größe eines Unternehmens (und die Anzahl der Zweige) zunimmt, wird schnell klar, dass wir für dieselbe Aufgabe einen immer größeren Aufwand betreiben müssen. Wenn wir größer werden, brauchen wir einen anderen Ansatz, den wir in Kapitel 16 besprechen.
Strategien, die gut skalieren
Welche Art von Maßnahmen führt zu besseren Kosten, wenn die Organisation wächst? Oder noch besser: Welche Maßnahmen können wir einführen, die einen superlinearen Wert schaffen, wenn die Organisation wächst?
Eine unserer beliebtesten internen Richtlinien unterstützt Infrastrukturteams und schützt ihre Fähigkeit, Infrastrukturänderungen sicher durchzuführen. "Wenn ein Produkt aufgrund von Infrastrukturänderungen ausfällt oder andere Probleme auftauchen, das Problem aber nicht durch Tests in unserem Continuous Integration (CI) System aufgedeckt wurde, ist das nicht die Schuld der Infrastrukturänderung." Umgangssprachlich wird dies als "Wenn es dir gefallen hat, hättest du einen CI-Test durchführen sollen" bezeichnet, was wir die "Beyoncé-Regel" nennen.13 Aus Sicht der Skalierung bedeutet die Beyoncé-Regel, dass komplizierte, einmalige, maßgeschneiderte Tests, die nicht von unserem gemeinsamen CI-System ausgelöst werden, nicht zählen. Ohne diese Regel wäre es denkbar, dass ein Ingenieur des Infrastrukturteams jedes Team mit betroffenem Code ausfindig machen und sie fragen müsste, wie sie ihre Tests durchführen sollen. Das konnten wir tun, als wir noch hundert Ingenieure hatten. Das können wir uns definitiv nicht mehr leisten.
Wir haben festgestellt, dass Fachwissen und gemeinsame Kommunikationsforen von großem Wert sind, wenn eine Organisation wächst. Wenn Ingenieure in gemeinsamen Foren diskutieren und Fragen beantworten, verbreitet sich das Wissen in der Regel. Neue Experten wachsen heran. Wenn du hundert Ingenieure hast, die Java schreiben, wird ein einziger freundlicher und hilfreicher Java-Experte, der bereit ist, Fragen zu beantworten, bald hundert Ingenieure hervorbringen, die besseren Java-Code schreiben. Wissen verbreitet sich viral, Experten sind Überträger, und es spricht viel dafür, dass du die häufigsten Stolpersteine für deine Ingenieure aus dem Weg räumst. Darauf gehen wir in Kapitel 3 genauer ein.
Beispiel: Compiler Upgrade
Stell dir die schwierige Aufgabe vor, deinen Compiler zu aktualisieren. Theoretisch sollte ein Compiler-Upgrade billig sein, wenn man bedenkt, wie viel Aufwand Sprachen betreiben, um abwärtskompatibel zu sein, aber wie billig ist es in der Praxis? Wenn du noch nie ein solches Upgrade durchgeführt hast, wie würdest du dann beurteilen, ob deine Codebasis mit dieser Änderung kompatibel ist?
Unserer Erfahrung nach sind Sprach- und Compiler-Upgrades subtile und schwierige Aufgaben, selbst wenn sie im Großen und Ganzen abwärtskompatibel sein sollen. Ein Compiler-Upgrade führt fast immer zu geringfügigen Änderungen im Verhalten: Fehlkompilierungen werden behoben, Optimierungen werden optimiert oder die Ergebnisse von zuvor undefinierten Funktionen werden geändert. Wie würdest du die Korrektheit deiner gesamten Codebasis im Hinblick auf all diese möglichen Ergebnisse bewerten?
Das größte Compiler-Upgrade in der Geschichte von Google fand im Jahr 2006 statt. Zu diesem Zeitpunkt waren wir bereits seit ein paar Jahren aktiv und hatten mehrere Tausend Ingenieure im Einsatz. Wir hatten die Compiler seit etwa fünf Jahren nicht mehr aktualisiert. Die meisten unserer Ingenieure hatten keine Erfahrung mit einem Compilerwechsel. Der Großteil unseres Codes war nur einer einzigen Compiler-Version ausgesetzt gewesen. Es war eine schwierige und mühsame Aufgabe für ein Team von (meist) Freiwilligen, die schließlich nach Abkürzungen und Vereinfachungen suchten, um Compiler- und Sprachänderungen zu umgehen, von denen wir nicht wussten, wie wir sie übernehmen sollten.14 Am Ende war das Compiler-Upgrade 2006 äußerst schmerzhaft. Viele große und kleine Probleme mit dem Hyrum'schen Gesetz hatten sich in die Codebasis eingeschlichen und unsere Abhängigkeit von einer bestimmten Compiler-Version verstärkt. Diese impliziten Abhängigkeiten zu durchbrechen, war schmerzhaft. Die betreffenden Ingenieure gingen ein Risiko ein: Wir hatten weder die Beyoncé-Regel noch ein flächendeckendes CI-System, so dass es schwierig war, die Auswirkungen der Änderung im Voraus zu erkennen oder sicher zu sein, dass sie nicht für Regressionen verantwortlich gemacht werden würden.
Diese Geschichte ist gar nicht so ungewöhnlich. Ingenieure in vielen Unternehmen können eine ähnliche Geschichte über ein schmerzhaftes Upgrade erzählen. Ungewöhnlich ist, dass wir im Nachhinein erkannten, dass die Aufgabe schmerzhaft war, und uns auf technologische und organisatorische Veränderungen konzentrierten, um die Skalierungsprobleme zu überwinden und die Skalierung zu unserem Vorteil zu nutzen: Automatisierung (damit ein einziger Mensch mehr tun kann), Konsolidierung/Konsistenz (damit Änderungen auf niedriger Ebene einen begrenzten Problemumfang haben) und Fachwissen (damit ein paar Menschen mehr tun können).
Je häufiger du deine Infrastruktur änderst, desto einfacher ist es, dies zu tun. Wir haben festgestellt, dass Code, der z. B. im Rahmen eines Compiler-Upgrades aktualisiert wird, in den meisten Fällen weniger spröde ist und sich in Zukunft leichter aktualisieren lässt. In einem Ökosystem, in dem der meiste Code bereits mehrere Upgrades durchlaufen hat, hängt er nicht mehr von den Feinheiten der zugrundeliegenden Implementierung ab, sondern von der aktuellen Abstraktion, die durch die Sprache oder das Betriebssystem garantiert wird. Unabhängig davon, was genau du aktualisierst, musst du damit rechnen, dass das erste Upgrade für eine Codebasis deutlich teurer ist als spätere Upgrades, selbst wenn du andere Faktoren berücksichtigst.
Durch diese und andere Erfahrungen haben wir viele Faktoren entdeckt, die die Flexibilität einer Codebasis beeinflussen:
- Expertise
- Wir wissen, wie man das macht; für einige Sprachen haben wir inzwischen Hunderte von Compiler-Upgrades auf vielen Plattformen durchgeführt.
- Stabilität
- Zwischen den einzelnen Versionen gibt es weniger Änderungen, weil wir regelmäßig neue Versionen herausbringen; für einige Sprachen stellen wir jetzt alle ein bis zwei Wochen Compiler-Upgrades bereit.
- Konformität
- Es gibt weniger Code, der noch kein Upgrade durchlaufen hat, weil wir regelmäßig aktualisieren.
- Vertrautheit
- Da wir dies regelmäßig tun, können wir Redundanzen bei der Durchführung eines Upgrades erkennen und versuchen, diese zu automatisieren. Dies überschneidet sich erheblich mit den SRE-Ansichten über die Arbeit.15
- Politik
- Wir haben Prozesse und Richtlinien wie die Beyoncé-Regel. Der Nettoeffekt dieser Prozesse ist, dass Upgrades machbar bleiben, weil sich die Infrastrukturteams nicht um jede unbekannte Nutzung kümmern müssen, sondern nur um die, die in unseren CI-Systemen sichtbar ist .
Die eigentliche Lektion betrifft nicht die Häufigkeit oder Schwierigkeit von Compiler-Upgrades, sondern die Tatsache, dass wir, sobald uns bewusst wurde, dass Compiler-Upgrades notwendig waren, Wege gefunden haben, diese Aufgaben mit einer konstanten Anzahl von Ingenieuren durchzuführen, auch wenn die Codebasis wuchs.16 Hätten wir stattdessen entschieden, dass diese Aufgabe zu teuer ist und in Zukunft vermieden werden sollte, würden wir vielleicht immer noch eine zehn Jahre alte Compiler-Version verwenden. Aufgrund der verpassten Optimierungsmöglichkeiten würden wir vielleicht 25 % mehr für Rechenressourcen bezahlen. Unsere zentrale Infrastruktur könnte erheblichen Sicherheitsrisiken ausgesetzt sein, da ein Compiler aus dem Jahr 2006 sicherlich nicht dazu beiträgt, Schwachstellen bei der spekulativen Ausführung zu entschärfen. Stagnation ist eine Option, aber oft keine kluge.
Linksverschiebung
Es hat sich gezeigt, dass die Idee, Probleme früher im Entwicklungsworkflow zu finden, in der Regel die Kosten senkt. Betrachte den Entwicklungsworkflow für eine Funktion von links nach rechts, beginnend mit der Konzeption und dem Entwurf, über die Implementierung, die Prüfung, das Testen, die Freigabe, den Kanarienvogel und schließlich den Produktionseinsatz. Wenn ein Problem auf dieser Zeitachse früher erkannt wird, ist es billiger, es zu beheben, als länger zu warten, wie in Abbildung 1-2 dargestellt.
Dieser Begriff scheint aus dem Argument entstanden zu sein, dass Sicherheit nicht bis zum Ende des Entwicklungsprozesses aufgeschoben werden darf, mit der entsprechenden Aufforderung, "Sicherheit nach links zu verschieben". Das Argument ist in diesem Fall relativ einfach: Wenn ein Sicherheitsproblem erst entdeckt wird, nachdem dein Produkt in Produktion gegangen ist, hast du ein sehr teures Problem. Wenn das Problem vor dem Einsatz in der Produktion entdeckt wird, kann es zwar immer noch viel Arbeit kosten, das Problem zu identifizieren und zu beheben, aber es ist billiger. Wenn du das Problem entdeckst, bevor der ursprüngliche Entwickler den Fehler in die Versionskontrolle eingibt, ist es sogar noch billiger: Er kennt die Funktion bereits; eine Überarbeitung unter Berücksichtigung der neuen Sicherheitsbedingungen ist billiger, als das Problem einzugeben und jemand anderen zu zwingen, es zu bewerten und zu beheben.
Das gleiche Grundmuster taucht in diesem Buch immer wieder auf. Fehler, die durch statische Analyse und Codeüberprüfung abgefangen werden, bevor sie übertragen werden, sind viel billiger als Fehler, die es bis zur Produktion schaffen. Die Bereitstellung von Werkzeugen und Praktiken, die Qualität, Zuverlässigkeit und Sicherheit schon früh im Entwicklungsprozess hervorheben, ist ein gemeinsames Ziel für viele unserer Infrastrukturteams. Kein einzelner Prozess und kein einzelnes Werkzeug muss perfekt sein, daher können wir einen Defense-in-Depth-Ansatz verfolgen, der hoffentlich so viele Fehler wie möglich auf der linken Seite des Diagramms abfängt.
Kompromisse und Kosten
Wenn wir wissen, wie man programmiert, die Lebensdauer der Software verstehen, die wir pflegen, und wissen, wie wir sie pflegen, wenn wir mit immer mehr Ingenieuren neue Funktionen entwickeln und pflegen, müssen wir nur noch gute Entscheidungen treffen. Das scheint offensichtlich: In der Softwareentwicklung, wie auch im Leben, führen gute Entscheidungen zu guten Ergebnissen. Die Auswirkungen dieser Beobachtung werden jedoch leicht übersehen. Bei Google gibt es eine starke Abneigung gegen "weil ich es gesagt habe". Es ist wichtig, dass es für jedes Thema einen Entscheider gibt und klare Eskalationswege, wenn Entscheidungen falsch zu sein scheinen, aber das Ziel ist ein Konsens, keine Einstimmigkeit. Es ist in Ordnung und wird erwartet, dass es Fälle gibt, in denen gesagt wird: "Ich bin mit deinen Maßstäben/Bewertungen nicht einverstanden, aber ich verstehe, wie du zu diesem Schluss kommst." In all dem steckt die Vorstellung, dass es für alles einen Grund geben muss; "nur weil", "weil ich es sage" oder "weil alle anderen es so machen" sind Orte, an denen schlechte Entscheidungen lauern. Wann immer es effizient ist, sollten wir in der Lage sein, unsere Arbeit zu begründen, wenn wir uns zwischen den allgemeinen Kosten für zwei technische Optionen entscheiden.
Was verstehen wir unter Kosten? Wir sprechen hier nicht nur über Geld. "Kosten" bedeutet in etwa "Aufwand" und kann einen oder alle dieser Faktoren umfassen:
-
Finanzielle Kosten (z. B. Geld)
-
Ressourcenkosten (z. B. CPU-Zeit)
-
Personalkosten (z. B. technischer Aufwand)
-
Transaktionskosten (z. B. was kostet es, etwas zu unternehmen?)
-
Opportunitätskosten (z.B. was kostet es, nicht zu handeln?)
-
Gesellschaftliche Kosten (z.B.: Welche Auswirkungen hat diese Entscheidung auf die Gesellschaft insgesamt?)
In der Vergangenheit war es besonders einfach, die Frage nach den gesellschaftlichen Kosten zu ignorieren. Doch Google und andere große Tech-Unternehmen können jetzt glaubhaft Produkte mit Milliarden von Nutzern anbieten. In vielen Fällen sind diese Produkte ein klarer Nettonutzen, aber wenn wir in einer solchen Größenordnung operieren, werden selbst kleine Unterschiede in der Benutzerfreundlichkeit, Zugänglichkeit, Fairness oder dem Missbrauchspotenzial vergrößert, oft zum Nachteil von Gruppen, die ohnehin schon marginalisiert sind. Software durchdringt so viele Aspekte der Gesellschaft und der Kultur; deshalb ist es klug, dass wir uns bei unseren Produkt- und Technikentscheidungen sowohl der guten als auch der schlechten Seiten bewusst sind. Darauf gehen wir in Kapitel 4 näher ein.
Zusätzlich zu den oben genannten Kosten (bzw. unserer Schätzung dieser Kosten) gibt es noch andere Faktoren: Status Quo, Verlustaversion und andere. Wenn wir die Kosten bewerten, müssen wir alle zuvor genannten Kosten im Auge behalten: Die Gesundheit einer Organisation hängt nicht nur davon ab, ob Geld auf der Bank ist, sondern auch davon, ob sich ihre Mitglieder wertgeschätzt und produktiv fühlen. In hochkreativen und lukrativen Bereichen wie der Softwareentwicklung sind die finanziellen Kosten in der Regel nicht der begrenzende Faktor - die Personalkosten sind es in der Regel. Die Effizienzgewinne, die dadurch erzielt werden, dass die Ingenieure zufrieden, konzentriert und engagiert sind, können andere Faktoren leicht überwiegen, weil Konzentration und Produktivität so unterschiedlich sind und ein Unterschied von 10 bis 20 % leicht vorstellbar ist.
Beispiel: Markierungen
In vielen Unternehmen werden Whiteboardmarker wie kostbare Güter behandelt. Sie werden streng kontrolliert und sind immer Mangelware. Unweigerlich ist die Hälfte der Marker an einem Whiteboard trocken und unbrauchbar. Wie oft warst du schon in einer Besprechung, die durch das Fehlen eines funktionierenden Markers unterbrochen wurde? Wie oft hast du deinen Gedankengang unterbrochen, weil dir ein Marker ausgegangen ist? Wie oft sind alle Marker einfach verschwunden, vermutlich weil einem anderen Team die Marker ausgegangen sind und es sich deiner bemächtigen musste? Und das alles für ein Produkt, das weniger als einen Dollar kostet.
Bei Google gibt es in den meisten Arbeitsbereichen unverschlossene Schränke voller Büromaterial, darunter auch Whiteboard-Marker. Im Handumdrehen sind Dutzende von Markern in verschiedenen Farben zur Hand. Irgendwann sind wir einen Kompromiss eingegangen: Es ist viel wichtiger, das Brainstorming ohne Hindernisse zu optimieren, als uns davor zu schützen, dass jemand mit einem Haufen Marker abhaut.
Wir wollen bei allem, was wir tun, die Augen offen halten und die Kosten-Nutzen-Kompromisse genau abwägen - vom Büromaterial und den Vergünstigungen für die Mitarbeiter über die täglichen Erfahrungen der Entwickler bis hin zur Bereitstellung und dem Betrieb von Diensten im globalen Maßstab. Wir sagen oft: "Google ist eine datengesteuerte Kultur". Tatsächlich ist das eine Vereinfachung: Auch wenn es keine Daten gibt, kann es immer noch Beweise, Präzedenzfälle und Argumente geben. Bei guten technischen Entscheidungen geht es darum, alle verfügbaren Informationen abzuwägen und fundierte Entscheidungen über Kompromisse zu treffen. Manchmal beruhen diese Entscheidungen auf Instinkt oder bewährten Methoden, aber erst nachdem wir alle Ansätze ausgeschöpft haben, die versuchen, die wahren Kosten zu messen oder zu schätzen.
Letztendlich sollten sich die Entscheidungen in einer Ingenieurgruppe auf wenige Dinge beschränken:
-
Wir tun dies, weil wir es müssen (gesetzliche Vorschriften, Kundenanforderungen).
-
Wir tun dies, weil es die beste Option ist, die wir zum jetzigen Zeitpunkt auf der Grundlage der aktuellen Faktenlage sehen können (wie von einem geeigneten Entscheidungsträger festgelegt).
Entscheidungen sollten nicht nach dem Motto "Wir machen das, weil ich es sage" getroffen werden.17
Inputs für die Entscheidungsfindung
Wenn wir Daten abwägen, finden wir zwei häufige Szenarien:
-
Alle beteiligten Größen sind messbar oder können zumindest geschätzt werden. Das bedeutet in der Regel, dass wir Kompromisse zwischen CPU und Netzwerk oder Dollar und Arbeitsspeicher abwägen oder überlegen, ob wir zwei Wochen Ingenieurzeit aufwenden sollen, um N CPUs in unseren Rechenzentren einzusparen.
-
Einige der Größen sind subtil, oder wir wissen nicht, wie wir sie messen können. Manchmal heißt es: "Wir wissen nicht, wie viel Entwicklungszeit wir dafür brauchen. Manchmal ist es sogar noch nebulöser: Wie misst man die technischen Kosten einer schlecht entwickelten API? Oder die gesellschaftlichen Auswirkungen einer Produktwahl?
Es gibt wenig Grund, bei der ersten Art von Entscheidung unzureichend zu sein. Jede Organisation, die sich mit Softwareentwicklung beschäftigt, kann und sollte die aktuellen Kosten für Rechenressourcen, Ingenieurstunden und andere Größen, mit denen du regelmäßig zu tun hast, verfolgen. Auch wenn du die genauen Dollarbeträge nicht veröffentlichen willst, kannst du eine Umrechnungstabelle erstellen: So viele CPUs kosten genauso viel wie so viel RAM oder so viel Netzwerkbandbreite.
Mit einer vereinbarten Umrechnungstabelle in der Hand kann jeder Ingenieur seine eigene Analyse durchführen. "Wenn ich zwei Wochen damit verbringe, diese verknüpfte Liste in eine leistungsfähigere Struktur umzuwandeln, verbrauche ich fünf Gibibytes mehr Arbeitsspeicher, spare aber zweitausend CPUs. Soll ich das tun?" Diese Frage hängt nicht nur von den relativen Kosten für RAM und CPUs ab, sondern auch von den Personalkosten (zwei Wochen Support für einen Softwareentwickler) und den Opportunitätskosten (was könnte dieser Ingenieur in den zwei Wochen noch produzieren?).
Auf die zweite Art von Entscheidungen gibt es keine einfache Antwort. Wir verlassen uns auf Erfahrung, Führung und Präzedenzfälle, um diese Fragen zu klären. Wir investieren in die Forschung, um die schwer zu quantifizierenden Faktoren zu ermitteln (siehe Kapitel 7). Der beste allgemeine Vorschlag, den wir haben, ist, sich bewusst zu machen, dass nicht alles messbar oder vorhersehbar ist, und zu versuchen, solche Entscheidungen mit der gleichen Priorität und größerer Sorgfalt zu behandeln. Sie sind oft genauso wichtig, aber schwieriger zu handhaben.
Beispiel: Verteilte Builds
Denk an deinen Build. Völlig unwissenschaftlichen Twitter-Umfragen zufolge bauen etwa 60 bis 70 % der Entwickler lokal, selbst bei den großen, komplizierten Builds von heute. Das führt direkt zu Nicht-Witzen, wie dieser "Compiling"-Comiczeigt - wieviel produktive Zeit geht in deinem Unternehmen durch das Warten auf einen Build verloren? Vergleiche das mal mit den Kosten, die entstehen, wenn du etwas wie distcc
für eine kleine Gruppe laufen lässt. Oder wie viel kostet es, eine kleine Build-Farm für eine große Gruppe zu betreiben? Wie viele Wochen/Monate dauert es, bis sich diese Kosten bezahlt machen?
Mitte der 2000er Jahre verließ sich Google ausschließlich auf ein lokales Build-System: Du hast Code ausgecheckt und ihn lokal kompiliert. In einigen Fällen hatten wir riesige lokale Maschinen (du konntest Maps auf deinem Desktop bauen!), aber die Kompilierungszeiten wurden immer länger, je größer die Codebasis wurde. Es überrascht nicht, dass die Personalkosten aufgrund der verlorenen Zeit sowie die Ressourcenkosten für größere und leistungsstärkere lokale Rechner usw. stiegen. Diese Ressourcenkosten waren besonders lästig: Natürlich wollen wir, dass die Leute so schnell wie möglich bauen können, aber die meiste Zeit steht eine leistungsstarke Desktop-Entwicklungsmaschine still. Das ist nicht der richtige Weg, um diese Ressourcen zu investieren.
Schließlich entwickelte Google sein eigenes verteiltes Build-System. Die Entwicklung dieses Systems war natürlich mit Kosten verbunden: Die Ingenieure brauchten Zeit, um es zu entwickeln, sie brauchten noch mehr Zeit, um die Gewohnheiten und Arbeitsabläufe aller zu ändern und das neue System zu lernen, und natürlich kostete es zusätzliche Rechenressourcen. Aber die Einsparungen waren es wert: Die Builds wurden schneller, die Zeit der Ingenieure wurde zurückgewonnen und die Hardware-Investitionen konnten auf eine gemeinsam verwaltete Infrastruktur (in Wirklichkeit ein Teil unserer Produktionsflotte) statt auf immer leistungsstärkere Desktop-Rechner konzentriert werden. In Kapitel 18 gehen wir näher auf unseren Ansatz für verteilte Builds und die damit verbundenen Kompromisse ein.
Also haben wir ein neues System gebaut, es in die Produktion eingeführt und die Produktion für alle beschleunigt. Ist das das Happy End der Geschichte? Nicht ganz: Die Bereitstellung eines verteilten Build-Systems hat die Produktivität der Ingenieure massiv verbessert, aber im Laufe der Zeit wurden die verteilten Builds selbst immer umfangreicher. Was früher von den einzelnen Ingenieuren eingeschränkt wurde (weil sie ein Interesse daran hatten, dass ihre lokalen Builds so schnell wie möglich waren), wurde in einem verteilten Build-System nicht eingeschränkt. Aufgeblähte oder unnötige Abhängigkeiten im Build-Graphen wurden nur allzu häufig. Wenn jeder den Schmerz eines nicht optimalen Builds direkt zu spüren bekam und einen Anreiz hatte, wachsam zu sein, wurden die Anreize besser aufeinander abgestimmt. Indem wir diese Anreize beseitigten und aufgeblähte Abhängigkeiten in einem parallelen, verteilten Build versteckten, schufen wir eine Situation, in der der Konsum ausufern konnte und fast niemand einen Anreiz hatte, die Aufblähung des Builds im Auge zu behalten. Das erinnert an das Jevons-Paradoxon: Der Verbrauch einer Ressource kann als Reaktion auf eine effizientere Nutzung steigen.
Insgesamt überwogen die eingesparten Kosten, die mit dem Einbau einer dezentralen Anlage verbunden waren, bei weitem die negativen Kosten, die mit ihrem Bau und ihrer Wartung verbunden waren. Aber wie wir beim erhöhten Verbrauch gesehen haben, haben wir nicht alle diese Kosten vorhergesehen. Nach unserem Vorstoß befanden wir uns in einer Situation, in der wir die Ziele und Einschränkungen des Systems und unserer Nutzung neu konzipieren, bewährte Methoden (kleine Abhängigkeiten, maschinelle Verwaltung von Abhängigkeiten) ermitteln und die Werkzeuge und die Wartung für das neue Ökosystem finanzieren mussten. Selbst ein relativ einfacher Kompromiss in der Form "Wir geben $$$ für Rechenressourcen aus, um die Zeit der Ingenieure wieder hereinzuholen" hatte unvorhergesehene Auswirkungen auf das System.
Beispiel: Die Entscheidung zwischen Zeit und Maßstab
Die meiste Zeit überschneiden sich unsere Hauptthemen Zeit und Umfang und wirken zusammen. Eine Richtlinie wie die Beyoncé-Regel lässt sich gut skalieren und hilft uns, Dinge im Laufe der Zeit zu erhalten. Eine Änderung an der Benutzeroberfläche eines Betriebssystems erfordert vielleicht viele kleine Refactorings, aber die meisten dieser Änderungen lassen sich gut skalieren, weil sie eine ähnliche Form haben: Die Änderung des Betriebssystems macht sich nicht bei jedem Anrufer und jedem Projekt anders bemerkbar.
Gelegentlich geraten Zeit und Umfang in Konflikt, und nirgendwo so deutlich wie bei der grundlegenden Frage: Sollen wir eine Abhängigkeit hinzufügen oder sie aufspalten/neu implementieren, um sie besser an unsere lokalen Bedürfnisse anzupassen?
Diese Frage kann sich auf vielen Ebenen des Software-Stacks stellen, denn es ist regelmäßig der Fall, dass eine maßgeschneiderte Lösung, die auf deinen engen Problembereich zugeschnitten ist, die allgemeine Utility-Lösung, die alle Möglichkeiten abdecken muss, übertrifft. Indem du Utility-Code forkst oder neu implementierst und ihn an deinen engen Bereich anpasst, kannst du leichter neue Funktionen hinzufügen oder mit größerer Sicherheit optimieren, ganz gleich, ob es sich um einen Microservice, einen In-Memory-Cache, eine Kompressionsroutine oder etwas anderes in unserem Software-Ökosystem handelt. Was vielleicht noch wichtiger ist: Die Kontrolle, die du durch einen solchen Fork gewinnst, isoliert dich von Änderungen an deinen zugrunde liegenden Abhängigkeiten: Diese Änderungen werden nicht von einem anderen Team oder einem Drittanbieter diktiert. Du hast die Kontrolle darüber, wie und wann du auf den Lauf der Zeit und die Notwendigkeit von Veränderungen reagierst.
Wenn andererseits jeder Entwickler alles, was in seinem Softwareprojekt verwendet wird, forkt, anstatt das Bestehende wiederzuverwenden , leidet neben der Nachhaltigkeit auch die Skalierbarkeit. Um auf ein Sicherheitsproblem in einer zugrunde liegenden Bibliothek zu reagieren, reicht es nicht mehr aus, eine einzelne Abhängigkeit und deren Nutzer zu aktualisieren: Es geht jetzt darum, jeden verwundbaren Fork dieser Abhängigkeit und die Nutzer dieser Forks zu identifizieren.
Wie bei den meisten Entscheidungen in der Softwareentwicklung gibt es auch in diesem Fall keine pauschale Antwort. Wenn die Lebensdauer deines Projekts kurz ist, sind Forks weniger riskant. Wenn der fragliche Fork nachweislich nur einen begrenzten Umfang hat, ist das ebenfalls hilfreich. Vermeide außerdem Forks für Schnittstellen, die über Zeit- oder Projektgrenzen hinweg funktionieren könnten (Datenstrukturen, Serialisierungsformate, Netzwerkprotokolle). Konsistenz ist von großem Wert, aber Allgemeingültigkeit hat ihren Preis, und du kannst oft gewinnen, wenn du dein eigenes Ding machst - wenn du es sorgfältig tust .
Entscheidungen revidieren, Fehler machen
Einer der unbesungenen Vorteile einer datengesteuerten Kultur ist die Fähigkeit und die Notwendigkeit, Fehler einzugestehen. Irgendwann wird eine Entscheidung auf der Grundlage der verfügbaren Daten getroffen - hoffentlich auf der Grundlage guter Daten und nur weniger Annahmen, aber implizit auf der Grundlage der aktuell verfügbaren Daten. Wenn neue Daten hinzukommen, sich der Kontext ändert oder Annahmen widerlegt werden, kann sich herausstellen, dass eine Entscheidung fehlerhaft war oder dass sie zu dem Zeitpunkt sinnvoll war, aber jetzt nicht mehr gilt. Das ist besonders kritisch für eine langlebige Organisation: Mit der Zeit ändern sich nicht nur die technischen Abhängigkeiten und Softwaresysteme, sondern auch die Daten, die als Entscheidungsgrundlage dienen.
Wir glauben fest daran, dass Daten die Grundlage für Entscheidungen sind, aber wir wissen auch, dass sich die Daten im Laufe der Zeit ändern und neue Daten auftauchen können. Das bedeutet, dass Entscheidungen während der Lebensdauer des betreffenden Systems von Zeit zu Zeit überdacht werden müssen. Bei Projekten mit langer Lebensdauer ist es oft entscheidend, dass man die Möglichkeit hat, die Richtung zu ändern, nachdem man eine erste Entscheidung getroffen hat. Und das bedeutet vor allem, dass die Entscheidungsträger das Recht haben müssen, Fehler zuzugeben. Entgegen dem Instinkt mancher Menschen werden Führungskräfte, die Fehler zugeben, nicht weniger, sondern mehr respektiert.
Orientiere dich an Fakten, aber sei dir auch bewusst, dass Dinge, die nicht gemessen werden können, trotzdem von Wert sein können. Wenn du eine Führungskraft bist, ist es das, was du tun sollst: Urteilsvermögen zeigen und behaupten, dass Dinge wichtig sind. In den Kapiteln 5 und 6 werden wir mehr über Führung sprechen.
Softwareentwicklung vs. Programmierung
Wenn du mit unsere Unterscheidung zwischen Softwareentwicklung und Programmierung vorstellst, könntest du dich fragen, ob hier ein Werturteil im Spiel ist. Ist Programmieren irgendwie schlechter als Softwareentwicklung? Ist ein Projekt, das ein Jahrzehnt lang mit einem Team von Hunderten von Mitarbeitern durchgeführt werden soll, von Natur aus wertvoller als ein Projekt, das nur einen Monat lang nützlich ist und von zwei Personen erstellt wird?
Nein, natürlich nicht. Es geht nicht darum, dass die Softwareentwicklung besser ist, sondern lediglich darum, dass es sich um zwei verschiedene Problembereiche mit unterschiedlichen Einschränkungen, Werten und bewährten Methoden handelt. Der Wert des Hinweises auf diesen Unterschied liegt vielmehr in der Erkenntnis, dass einige Werkzeuge in dem einen Bereich großartig sind, in dem anderen aber nicht. Für ein Projekt, das nur ein paar Tage dauert, brauchst du wahrscheinlich keine Integrationstests (siehe Kapitel 14) und Continuous Deployment (CD) Methoden (siehe Kapitel 24). Auch all unsere langfristigen Überlegungen zur semantischen Versionierung (SemVer) und zum Abhängigkeitsmanagement in Softwareentwicklungsprojekten (siehe Kapitel 21) gelten nicht wirklich für kurzfristige Programmierprojekte: Verwende alles, was zur Lösung der anstehenden Aufgabe zur Verfügung steht.
Wir glauben, dass es wichtig ist, zwischen den verwandten, aber unterschiedlichen Begriffen "Programmierung" und "Softwareentwicklung" zu unterscheiden. Ein großer Teil dieses Unterschieds ergibt sich aus der Verwaltung von Code im Laufe der Zeit, den Auswirkungen der Zeit auf den Umfang und der Entscheidungsfindung angesichts dieser Ideen. Programmieren ist der unmittelbare Akt der Codeerstellung. Softwareentwicklung ist die Gesamtheit der Richtlinien, Praktiken und Werkzeuge, die notwendig sind, um den Code so lange zu nutzen, wie er gebraucht wird, und um die Zusammenarbeit im Team zu ermöglichen.
Fazit
In diesem Buch werden all diese Themen behandelt: Richtlinien für ein Unternehmen und für einen einzelnen Programmierer, wie man seine bewährten Methoden bewertet und verfeinert, und die Werkzeuge und Technologien, die zu wartbarer Software gehören. Google hat hart daran gearbeitet, eine nachhaltige Codebasis und Kultur zu schaffen. Wir sind nicht unbedingt der Meinung, dass unser Ansatz der einzig wahre ist, aber er zeigt beispielhaft, dass es machbar ist. Wir hoffen, dass er einen nützlichen Rahmen bietet, um über das allgemeine Problem nachzudenken: Wie kannst du deinen Code so lange pflegen, wie er funktionieren muss?
TL;DRs
-
Die "Softwareentwicklung" unterscheidet sich von der "Programmierung" in mehreren Dimensionen: Bei der Programmierung geht es um die Erstellung von Code. Bei der Softwareentwicklung geht es auch um die Pflege des Codes während seiner Lebensdauer.
-
Die Lebensdauer von kurzlebigem und langlebigem Code unterscheidet sich mindestens um den Faktor 100.000. Es ist töricht anzunehmen, dass an beiden Enden des Spektrums die gleichen bewährten Methoden gelten.
-
Software ist dann nachhaltig, wenn wir während der erwarteten Lebensdauer des Codes in der Lage sind, auf Änderungen der Abhängigkeiten, der Technologie oder der Produktanforderungen zu reagieren. Wir können uns dafür entscheiden, Dinge nicht zu ändern, aber wir müssen dazu in der Lage sein.
-
Hyrum's Law: Bei einer ausreichenden Anzahl von Nutzern einer API spielt es keine Rolle, was du im Vertrag versprichst: Alle beobachtbaren Verhaltensweisen deines Systems werden von irgendjemandem in Anspruch genommen.
-
Jede Aufgabe, die dein Unternehmen wiederholt erledigen muss, sollte in Bezug auf den menschlichen Einsatz skalierbar sein (linear oder besser). Richtlinien sind ein wunderbares Instrument, um Prozesse skalierbar zu machen.
-
Ineffiziente Prozesse und andere Aufgaben in der Software-Entwicklung lassen sich nur langsam steigern. Sei vorsichtig bei Problemen mit gekochten Fröschen.
-
Fachwissen zahlt sich besonders aus, wenn es mit Skaleneffekten kombiniert wird.
-
"Weil ich es sage" ist ein schrecklicher Grund, etwas zu tun.
-
Sich an Daten zu orientieren ist ein guter Anfang, aber in Wirklichkeit basieren die meisten Entscheidungen auf einer Mischung aus Daten, Annahmen, Präzedenzfällen und Argumenten. Am besten ist es, wenn objektive Daten den größten Teil dieser Inputs ausmachen, aber es können selten alle sein.
-
Datenorientierung bedeutet, dass du die Richtung ändern musst, wenn sich die Daten ändern (oder wenn Annahmen widerlegt werden). Fehler oder überarbeitete Pläne sind unvermeidlich.
1 Wir meinen nicht die "Ausführungsdauer", sondern die "Wartungsdauer" - wie lange wird der Code noch erstellt, ausgeführt und gewartet werden? Wie lange wird diese Software einen Wert haben?
2 Das ist vielleicht eine vernünftige Definition von technischer Schuld: Dinge, die getan werden "sollten", aber noch nicht getan werden - das Delta zwischen unserem Code und dem, was wir uns wünschen, dass es so wäre.
3 Berücksichtige auch die Frage, ob wir im Voraus wissen, dass ein Projekt langlebig sein wird.
4 Es herrscht Unklarheit über die ursprüngliche Zuordnung dieses Zitats. Der Konsens scheint zu sein, dass es ursprünglich von Brian Randell oder Margaret Hamilton formuliert wurde, aber es könnte auch komplett von Dave Parnas erfunden worden sein. Das übliche Zitat lautet "Softwareentwicklungstechniken": Report of a conference sponsored by the NATO Science Committee," Rome, Italy, 27-31 Oct. 1969, Brussels, Scientific Affairs Division, NATO.
5 Frederick P. Brooks Jr. The Mythical Man-Month: Essays on Softwareentwicklung (Boston: Addison-Wesley, 1995).
6 Appcelerator, "Nothing is Certain Except Death, Taxes and a Short Mobile App Lifespan", Axway Developer blog, 6. Dezember 2012.
7 Deine eigenen Prioritäten und Vorlieben bestimmen, wo genau der Übergang stattfindet. Wir haben festgestellt, dass die meisten Projekte innerhalb von fünf Jahren aufrüsten wollen. Ein Zeitraum zwischen 5 und 10 Jahren scheint eine konservative Schätzung für den Übergang im Allgemeinen zu sein.
8 Hyrum hat sich redlich bemüht, dies bescheiden "The Law of Implicit Dependencies" (Das Gesetz der impliziten Abhängigkeiten) zu nennen, aber "Hyrum's Law" ist die Kurzform, auf die sich die meisten Leute bei Google geeinigt haben.
9 Siehe "Workflow", ein xkcd-Comic.
10 Eine Art von Denial-of-Service (DoS)-Angriff, bei dem ein nicht vertrauenswürdiger Benutzer die Struktur einer Hash-Tabelle und die Hash-Funktion kennt und Daten so bereitstellt, dass die algorithmische Leistung von Operationen auf der Tabelle beeinträchtigt wird.
11 Beyer, B. et al. Site Reliability Engineering: How Google Runs Production Systems. (Boston: O'Reilly Media, 2016).
12 Wenn wir in diesem Kapitel "skalierbar" in einem informellen Kontext verwenden, meinen wir "sublineare Skalierung in Bezug auf menschliche Interaktionen".
13 Das ist eine Anspielung auf den beliebten Song "Single Ladies", der den Refrain "If you liked it then you shoulda put a ring on it" enthält.
14 Insbesondere mussten Schnittstellen aus der C++-Standardbibliothek im Namensraum std referenziert werden, und eine Optimierungsänderung für std::string
erwies sich für unsere Verwendung als erhebliche Pessimierung, so dass einige zusätzliche Workarounds erforderlich waren.
15 Beyer et al. Site Reliability Engineering: How Google Runs Production Systems, Kapitel 5, "Eliminating Toil".
16 Nach unserer Erfahrung produziert ein durchschnittlicher Softwareentwickler (SWE) eine ziemlich konstante Anzahl von Codezeilen pro Zeiteinheit. Bei einer festen SWE-Population wächst die Codebasis im Laufe der Zeit linear-proportional zur Anzahl der SWE-Monate. Wenn deine Aufgaben einen Aufwand erfordern, der mit der Anzahl der Codezeilen wächst, ist das besorgniserregend.
17 Das soll nicht heißen, dass Entscheidungen einstimmig oder sogar mit breiter Zustimmung getroffen werden müssen; am Ende muss jemand die Entscheidung treffen. Dies ist in erster Linie eine Aussage darüber, wie der Entscheidungsprozess für denjenigen ablaufen sollte, der tatsächlich für die Entscheidung verantwortlich ist.
Get Softwareentwicklung bei Google now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.