- Eine Entwicklungskultur, in der das Installieren von Paketen zum Standard geworden ist, vergrößert den dependency churn, der durch Updates, Patches, Audits und das Management transitiver Abhängigkeiten entsteht, und schafft so versteckte Produktivitätskosten
- Besonders groß ist der Effekt in gut ausgebauten Packaging-Ökosystemen wie JavaScript und Rust; Beispiele sind 28 Crates in einem neuen Tokio-Projekt, 172 Crates in einem Rocket-Projekt und 142 Abhängigkeiten in der MiniJinja-CLI
- Selbst für seit Langem stabile Funktionen wie
terminal_size entsteht paradoxerweise zusätzlicher Aufwand durch Änderungen an Bibliotheken zur Plattformabstraktion, was weitere Crates und wiederholte Releases nach sich zieht
- Für kleine Funktionen kann es mit ChatGPT oder Cursor schneller sein, eine Implementierung ohne Abhängigkeiten zu erstellen, als Abhängigkeiten zu recherchieren und laufend zu aktualisieren; außerdem sinkt die Last, externen Code zu kompilieren
- Bibliotheken für schwierige Bereiche wie HTTP, QUIC, Grafik oder tokio sind zwar nötig, aber die Entscheidung, wegen einer einzigen Funktion einen großen Abhängigkeitsgraphen zu akzeptieren, sollte stärker hinterfragt werden
Das Wartungs-Treadmill durch wachsende Abhängigkeiten
- Entwickler installieren Pakete leichtfertig aus Produktivitätsgründen, landen dadurch aber in einem endlosen dependency churn aus Updates, Patches, Audits und dem Management transitiver Abhängigkeiten
- Je besser die Packaging-Lösungen eines Ökosystems ausgebaut sind, desto deutlicher tritt das Problem hervor; JavaScript und Rust sind besonders stark betroffen
- Beispiele aus Rust:
- Ein neues Tokio-Projekt bringt 28 Crates mit
- Ein neues Rocket-Projekt wächst auf 172 Crates an
- MiniJinja selbst kann mit einer einzigen Abhängigkeit existieren, aber die CLI-Variante bringt 142 Abhängigkeiten mit
- Das Kernproblem ist, dass für eine kleine Funktion viel mehr externer Code gebaut und verwaltet werden muss, als für die eigentliche Funktion tatsächlich nötig wäre
Das Paradox stabilen Codes am Beispiel terminal_size
terminal_size ist, wie der Name sagt, ein Crate zum Ermitteln der Terminalgröße
- Die zugrunde liegenden APIs für diese Funktion sind seit den Anfängen von Computing-Terminals faktisch stabil, dennoch werden je nach OS 3 bis 4 zusätzliche Crates mit hereingezogen
- So entsteht die Situation, dass Tausende andere Funktionen kompiliert werden, nur um festzustellen, ob ein Terminal 80x25 oder 120x40 groß ist
- Das Crate wurde 26-mal released, während eine eigene Implementierung, die vor 10 Jahren in ein Projekt eingebaut wurde, für dieselbe Funktion ohne Updates noch immer funktioniert
- Die vielen Releases sind nicht darauf zurückzuführen, dass sich die Funktion selbst geändert hätte, sondern darauf, dass sich die zugrunde liegenden Bibliotheken zur Plattformabstraktion ständig ändern
- Unter UNIX gibt es die Ausnahme, dass eine
libc-Abhängigkeit nötig ist
- Rust stellt die
libc-Konstanten der Plattform nicht direkt bereit, und diese Konstanten sind auch nicht standardisiert
- Allerdings ist
libc eine übliche und leichte Abhängigkeit, die sich kaum vermeiden lässt
Sicherheit und eine Kultur der Code-Wiederverwendung verstärken Abhängigkeiten
- Ein Ansatz im Stil einer „big supply chain“ sieht das Kopieren einzelner Funktionen oder den direkten Einsatz von
unsafe kritisch und drängt dazu, solche Aufgaben an Plattformabstraktionsschichten zu delegieren
- Es gibt auch Unternehmen, die Werkzeuge zum Umgang mit Abhängigkeitsproblemen anbieten und im Namen der Sicherheit dazu anhalten, Abhängigkeiten zu pflegen und aktuell zu halten
- Viele Abhängigkeiten selbst können jedoch eine zentrale Quelle von Sicherheitsproblemen sein
- Code sollte mit dem Ziel geschrieben werden, irgendwann einen stabilen Zustand zu erreichen, in dem keine Updates mehr nötig sind
- Im Rust-Ökosystem können selbst stabil funktionierende Abhängigkeiten in RUSTSEC schlecht bewertet werden, wenn ihr Bugtracker nur begrenzt aktiv ist
- Auch die unternehmensgeprägte Code-Review-Kultur wirkt auf Open Source zurück
- Ein Engineer, der eine neue und glänzende Bibliothek einführt, wird eher belohnt als dafür kritisiert
- Dadurch sind Werkzeuge wie Dependabot entstanden, und Projekte erhalten fortlaufend PRs für Abhängigkeits-Updates
- Innerhalb von Unternehmen können allein Vendoring, interne Audits und unternehmensweite Upgrades Engineering-Teams dauerhaft beschäftigen
Kleine Funktionen kann man selbst bauen
- Der einfachere Weg kann sein, den benötigten Code selbst zu schreiben
- Der Anfangsaufwand mag höher sein, aber nach der Fertigstellung braucht der Code kein neues Crate, und man muss nicht warten, bis ein Upstream-Autor Edge Cases behebt
- Wenn der Code am eigenen Einsatzort kaputtgeht, kann man ihn direkt selbst reparieren; funktionierender Code muss nicht zwangsläufig auf das Wartungs-Treadmill aufspringen
- Stand 2025 können ChatGPT oder Cursor schnell Implementierungen ohne Abhängigkeiten für gängige kleine Funktionen erzeugen
- Viele kleine Funktionen haben geringen Wartungsaufwand und können weniger belastend sein als laufende Upgrades von Abhängigkeiten
- Wenn es nur um ein paar Zeilen Code geht, muss man nicht Tausende Zeilen fremden Codes für eine einzige Funktion kompilieren
Geringe Abhängigkeiten höher bewerten
- Nicht jede Abhängigkeit ist schlecht
- Grafikbibliotheken, die komplexe Treiber abstrahieren
- Protokollimplementierungen wie HTTP und QUIC
- Wichtige Bibliotheken wie tokio, die man weder entfernen kann noch entfernen will
- Wenn jedoch für nur eine Funktion Hunderte anderer Funktionen kompiliert werden, sollte das als Warnsignal gelten
- Die Entscheidung, kleine Funktionen selbst zu schreiben und so den transitiven Abhängigkeitsgraphen zu vermeiden, sollte stärker anerkannt werden
- Großen Crate-Graphen sollte man mit mehr Skepsis begegnen, und einfacher, stabiler Code, den man über Jahre nicht anfassen muss, sollte positiver bewertet werden
sha1-smol wurde ursprünglich unter dem Namen sha1 zum Standard-Crate für die Berechnung von SHA1-Hashes, gab den Namen später aber an rust-crypto ab und geriet unter Druck, sich an ein größeres Krypto-Ökosystem anzupassen
- Nutzt man das neue
sha1-Crate, kommen 10 Abhängigkeiten mit
- Wegen der Bedeutung des Registry-Namens und Anforderungen an Trait-Kompatibilität ließ sich das kaum vermeiden
- MiniJinja betont in seiner README die geringe Zahl an Abhängigkeiten
$ cargo tree
minimal v0.1.0 (examples/minimal)
└── minijinja v2.6.0 (minijinja)
└── serde v1.0.144
- Für MiniJinja gibt es einen PR, um die letzte Abhängigkeit zu entfernen
- Wo es sinnvoll ist, sollte man selbst bauen feiern und Autoren von Open-Source-Bibliotheken mit wenigen oder keinen Abhängigkeiten mehr Anerkennung geben
1 Kommentare
Meinungen auf Hacker News
Die Sprache Rust selbst finde ich gut, aber das Abhängigkeits-Ökosystem von Rust mag ich nicht. Bei C++ wird oft beklagt, dass es schwierig ist, Abhängigkeiten hinzuzufügen, aber genau das fühlt sich für mich eher wie ein Feature an. Denn es zwingt einen, darüber nachzudenken, ob man sie wirklich braucht.
In C++ kontrolliere ich meine Abhängigkeiten selbst, in Rust ist man schnell bei über 100 und gibt irgendwann auf. Aus Sicherheitssicht weiß ich ehrlich gesagt nicht, was ich eigentlich ausliefere.
Außerdem hat Rust keine ABI-Kompatibilität und keine Kultur rund um Shared Libraries. Dadurch fühlt es sich so an, als würde es das Paketverteilungsmodell von Betriebssystemen kaputtmachen. Wenn ich eine Linux-Distribution auswähle, vertraue ich den Leuten, die diese Distribution bauen; Rust wirkt eher wie Python, wo jeder etwas auf PyPI hochladen kann.
Wie sieht es bei Docker/Python/Rust aus? Ich kenne die Leute, die mein Docker-Image, mein PyPI-Paket oder meine Rust-Crate erstellt haben, überhaupt nicht.
Im Grunde sind wir wieder in der Zeit gelandet, in der man EXE- und DLL-Dateien per ZIP austauschte. Nur nennen wir das jetzt Container und führen es stolz mit Root-Rechten aus.
Wie der Autor sagt, ist es manchmal am besten, den Quellcode einer Abhängigkeit einfach zusammenzuführen. Früher nannte man das Vendoring, und bei Rails/Ruby war es ziemlich wichtig. Es hat den großen Vorteil, dass man später nicht von einer böswilligen Paketübernahme betroffen ist, und wenn man möchte, kann man Upstream-Sicherheits-Patches selbst mergen.
Mir fällt kein Projekt ein, bei dem das NIH-Syndrom einen positiven Nettoeffekt hatte. Selbst nicht zwingend notwendige Abhängigkeiten sparen enorm viel Zeit.
Wenn man Teile optionaler Abhängigkeiten in jedem Projekt neu baut, frage ich mich, woher die Zeit kommen soll, zusätzliche Bugs und Randfälle, mehrere Versionen von Unterabhängigkeiten und die Unterstützung verschiedener Plattformen zu beheben.
Kombiniert man das mit Repositories wie PyPI, npm oder Cargo, in die jeder hochladen kann und die nur schwach kuratiert sind, sowie mit einer kleinen Standardbibliothek, muss man mit Schmerzen rechnen.
Entwickler entscheiden, welche Abhängigkeiten sie einbinden. Abhängigkeiten, die niemand nutzt, werden verschwinden, und der Grund, warum die meisten Bibliotheken viele Abhängigkeiten haben, ist, dass viele Entwickler lieber auf Abhängigkeiten aufbauen.
Wenn es um das Build-System geht: Cargo erzwingt nicht die Nutzung von Crates.io. Es macht sie nur einfach. Man kann auch pfadbasierte Abhängigkeiten wie bei CMake/Vcpkg oder Conan verwenden oder Bibliotheken selbst erstellen.
Selbst wenn man Crates.io nutzt, kann man Versionen pinnen, wenn man keine Änderungen möchte. Es macht nur einfach, die neuesten Versionen zu bekommen.
In Rust ist es einfach, Software auf bestehender Software aufzubauen. Wenn einem bestehende Software oder deren Änderungstempo nicht gefällt, sollte man nicht Cargo die Schuld geben, sondern es so machen, wie man es möchte.
Distributionen bauen Rust-Pakete weiterhin aus dem Quellcode und vendorn Crate-Abhängigkeiten in ihre Repositories. Das ist schmerzhafter, weil es mehr Abhängigkeiten gibt und sie häufiger aktualisiert werden, aber das ist unabhängig von Shared Libraries.
Die Aussage, dass die API zum Ermitteln der Terminalgröße seit 50 Jahren stabil sei, ist nicht korrekt. TIOCGWINSZ ioctl wurde meines Wissens nie standardisiert, und je nach Unix und BSD gibt es mehrere Namen dafür.
Die Funktion
tcgetwinsize()kam erst 2024 in POSIX, und dieses ganze Thema hat eine ziemlich traurige Geschichte https://news.ycombinator.com/item?id=42039401. Und das gilt schon, bevor man überhaupt zur Windows-Seite kommt.Manchmal ist das in Ordnung, aber für Leute mit anderen Use Cases oder für Menschen, die die Software auf Systemen ausführen, die sie selbst nicht verstehen und nicht nutzen, kann die Software dadurch schlechter werden.
Der eigentliche Wert einer Bibliothek liegt darin, dass sie damit umgeht, dass selbst scheinbar einfache Probleme in Wahrheit viel Komplexität enthalten. Ich finde auch nicht, dass 3–4 Abhängigkeiten für eine Bibliothek, die sowohl unter Windows als auch unter Linux läuft, besonders viel sind. Im guten Fall hat die Person, die diese Bibliothek schreibt, in diesem Bereich mehr Erfahrung und kann auch unbekannte Bereiche abdecken, von denen ich nichts weiß.
Bei ioctl gibt es sicherlich Probleme, aber die Änderungen, die in
terminal-sizeoder seinen Abhängigkeiten mehrere Releases verursacht haben, haben nichts mit den Konstantenioctl/TIOCGWINSZoder der Strukturwinsizezu tun. Dieser Code hat sich nicht geändert.terminal-sizeeinfach Rustix’tcgetwinsizeauf, und Rustix ruft wiederum libc'stcgetwinsizeauf. Wenn man dasselbe also direkt macht, kann man ziemlich viele Abhängigkeiten reduzieren; der Preis ist ungefähr die Windows-Unterstützung.Ob diese API nun 50 oder 25 Jahre stabil war, ist ein Detail. Diese Abhängigkeit tut nicht einmal so, als würde sie diese Komplexität behandeln, und es ist auch unwahrscheinlich, dass sich diese Funktion in naher Zukunft ändert oder entfernt wird.
Das meiste schreibt man direkt dorthin, aber Funktionen wie Terminalgröße oder Raw Mode sind ein Durcheinander und brauchen Dinge wie
ioctl. Ehrlich gesagt ist das nicht besonders schön.Aber Bibliotheken sind noch schlimmer. Wenn man nicht gegen
ncurseslinken will, braucht man Gottes Beistand.Kürzlich habe ich meine erste Startup-Webapp aus dem Jahr 2006 wiederbelebt. Es war eine Social-Media-Site mit Schwerpunkt auf dem Teilen von Medien und für damalige Verhältnisse ein ziemlich einfacher LAMP-Stack. PHP 5, MySQL 3.2, aber die Social-Media-Funktionen von damals waren im Großen und Ganzen vorhanden.
Weil ich neue CI/CD-Techniken selbst ausprobieren wollte, nutze ich diese App als Lernprojekt und over-engineere dafür den Deployment-Prozess. Ich hätte auch WordPress oder eine Hello-World-App verwenden können, aber das hier macht deutlich mehr Spaß.
Den PHP-Code habe ich fast vollständig selbst geschrieben. Auch Bibliotheken für Authentifizierung/Autorisierung, Templates, Formularverarbeitung usw. habe ich selbst gebaut; nur fürs Versenden von E-Mails habe ich eine PEAR-Bibliothek verwendet. Das Frontend bestand aus reinem HTML, JavaScript gab es kaum, und für die Medienwiedergabe kam Flash zum Einsatz. 2006 hat man meistens so gebaut.
Die 19 Jahre alte App wieder zum Laufen zu bringen, hat nur etwa eine Stunde gedauert. Ich habe den alten PHP-
mysql-Treiber durchmysqliersetzt und das Schema sowie einige Queries an MySQL 8 angepasst. Im Wesentlichen ging es darum, Wörter, die inzwischen reserviert sind, in Backticks zu setzen und die strenger gewordenen Default-Werte von Spalten zu korrigieren. Das Einzige, was nicht mehr funktionierte, war Flash.Im Gegensatz dazu betreiben wir an meinem aktuellen Arbeitsplatz Dutzende Spring-Boot-Apps in Java 8, und die Listen von Schwachstellen aus Dutzenden Abhängigkeiten stapeln sich seitenweise. Wenn man eine aktualisiert, muss man reihenweise andere Bibliotheken mit anheben, und wegen transitiver Abhängigkeiten wird es zum Albtraum. Deshalb beheben wir nur die kritischsten Schwachstellen minimal und haben keinen realistischen Plan, alles insgesamt zu aktualisieren.
Das Komische ist: Was die PHP-App von 2006 gemacht hat und was die heutigen Spring-Boot-Apps tun, unterscheidet sich nicht groß. Am Ende ist alles CRUD; es hängt nur viel mehr Enterprise-Dekoration und Tooling drumherum.
Man zieht nicht für jeden Punkt auf der Checkliste eine eigene Abhängigkeit herein. Es kann zwar Doppelarbeit entstehen, aber der Upgrade-Pfad wird deutlich einfacher.
scpdeployt, ist das meist kein Problem.Standardisierte Kommunikationsnachrichtenformate wie FHIR oder HL7 zum Beispiel sind bereits komplex; die komplette Standarddefinition will man nicht selbst implementieren.
Kryptografische Funktionen selbst zu schreiben bedeutet meistens ebenfalls, sich ins eigene Knie zu schießen; die über Jahre entdeckten gravierenden Sicherheitsprobleme haben das bewiesen.
Heute ist es eine Zeit, in der ich mich lieber auf das Lösen von Geschäftsproblemen konzentriere als darauf, wie eine Lösung korrekt gebaut wird. Mit dem Aufkommen von AI ist das noch wichtiger geworden, weil sich jeder Code anfühlt, als sei er mit geschlossenen Augen zusammengeklebt worden.
Zeit darauf zu verwenden, alles selbst zu bauen, kann langfristig vorteilhaft sein, aber zuerst muss man den Wettbewerb überleben. Die Konkurrenz hat sich den Markt anfangs vielleicht schon mit schnellem, wegwerfbarem Code gesichert.
Wir sind kürzlich von Java 8, Spring Boot 2 und Swagger auf Java 17, Spring Boot 3.3 und OpenAPI 3 umgestiegen, und das war ziemlich painless.
Einige direkte und transitive Abhängigkeiten müssen wir noch weiter anheben, aber das größte Hindernis wurde durch die Migration beseitigt.
Aber solange man nicht gegen eine C-Bibliothek linkt, sind private transitive Abhängigkeiten völlig egal. Im selben Abhängigkeitsbaum kann man beliebig viele nicht SemVer-kompatible Versionen einer Crate haben, und bei Bedarf kann man sogar direkt von mehreren Versionen abhängen.
Java-artige große Massen-Upgrades sind nicht nötig; man aktualisiert einfach nur die eine verwundbare Abhängigkeit. Soweit ich weiß, hat C# ähnliche Funktionen, ist aber etwas wortreicher.
Stimme zu 100 % zu. NodeJS hat meine Karriere stark geprägt, aber NPM hat mir ein größeres Trauma beschert als eine chaotische Kindheit.
Man stelle sich einen neuen Programmierer vor, der eine neue App baut, um sie Freunden zu zeigen. Er will eine neue Abhängigkeit hinzufügen, aber sie passt nicht zu einer anderen Abhängigkeit, also beschließt er, alles zu aktualisieren. Im nächsten Moment funktioniert nichts mehr, und Babel schreit.
Man denkt trotzdem, irgendwie werde man es schon lösen, sieht aber bald offene Git-Issues, bei denen selbst Grundlegendes buchstäblich nicht funktioniert. Bei Expo gibt es zum Beispiel ein offenes Issue, dass ein frisch angelegtes React-Native-Standardprojekt unter Android nicht buildet.
Zur Hälfte wirkt es so, als kümmere es niemanden, und in solchen Fällen liegt die Lösung auch nicht im Node-Ökosystem, sondern irgendwo im Android-Ökosystem. Es ist Duct Tape bis ganz nach unten. Gleichzeitig gibt es einem auch das Selbstvertrauen, kein Hochstapler-Syndrom zu haben, wenn das eigene Side Project zur Hälfte nicht läuft — schließlich kann selbst ein milliardenschweres Projekt ein nicht funktionierendes Template ausliefern.
Ich begann, den Moment, in dem ich die erste Abhängigkeit hereinholte, als persönliches Scheitern zu betrachten. Denn in diesem Moment brauchte man plötzlich Packaging und Organisation, nur um eine
package.jsonzu verwenden, statt einfach normales JS per Script-Tag zu laden.Das war etwas, das mich beim Wechsel von Go ins Rust-Ökosystem überrascht hat. Ein ausgereiftes Go-Projekt, etwa das Produktions-Web-Backend eines Unternehmens, kann inklusive transitiver Abhängigkeiten nur 10 bis 20 Dependencies haben.
Wie im Artikel erwähnt, ist es sehr wahrscheinlich, dass selbst kleine Rust-Projekte deutlich mehr haben; bei asynchroner Arbeit ist es fast garantiert.
Ich bin mir nicht sicher, ob das an der Kultur liegt oder an Sprachfeatures. In Go werden Interfaces zum Beispiel implizit erfüllt, man muss also nichts importieren, nur um zu sagen, dass man etwas implementiert.
Dinge, die in Go in der Sprache oder Standardbibliothek enthalten sind, für die man in Rust aber eine Dependency braucht, sind unter anderem Green Threads, Channels, reguläre Ausdrücke, HTTP-Client, HTTP-Server, Zeit, Kommandozeilen-Flags, Logger sowie Lesen/Schreiben animierter GIFs.
Ich mag die Go-Sprache selbst nicht besonders, aber bei Tools und Standardbibliothek ist sie führend und eindeutig das Beste, was ich bisher benutzt habe.
Der Kernthese stimme ich weitgehend zu. Es gibt viele versteckte Kosten, wenn man zulässt, dass die Abstraktionen anderer Libraries außerhalb der eigenen Library sichtbar werden. Das heißt nicht, dass man es niemals tun sollte, aber zum Zeitpunkt der Entscheidung muss man die künftigen Kosten ausgewogen betrachten.
Wenn ein umhüllendes Package sein Design oder seine Ziele häufig ändert, entsteht Instabilität. Was ursprünglich die einfachste Form von Garantie im Software Engineering war, verschwindet oder wird aufgespalten.
Außerdem erschwert es Besitzern, die zwar keine langjährigen Open-Source-Contributors sind, aber ein bestimmtes Fachgebiet gut kennen, die Teilnahme am Ökosystem. Wenn aus „Ich habe eine saubere Methode gebaut, um X zu tun“ wochenlange Diskussionen, Verhandlungen und Politik werden, weil „X unter Y passen muss, da Leute vielleicht Z mögen könnten“, verschwendet das die Zeit aller.
Eine Sache, die ich im Leben gelernt habe: Das Einfachste überlebt am längsten. Minilithen und Monolithen sollten häufiger gelobt werden. Das ist auch kein reines Rust-Problem; ich habe es in vielen Sprachen gesehen. Open-Source-Communities treiben oft Pakete in atomarer Größe stark voran, und ich frage mich häufig, ob es dabei nicht eher ums Abstecken von Claims oder um Eigentumsübertragung geht als um die Interessen der tatsächlichen Nutzergemeinschaft.
Ich bin eher zufällig zu der Sichtweise gelangt, dass es 2025 schneller ist, ChatGPT oder Cursor dependency-freie Implementierungen gängiger Funktionen schreiben zu lassen, und stimme dem zunehmend zu. Besonders nach der Erfahrung mit der Dependency Hell in einer großen React-App.
Man muss auch Dependencies auf SaaS- oder Drittanbieter-Services bedenken. Viele davon sind gängige Muster und bereits gelöste Probleme, die ein LLM schnell nachbauen kann.
Für klar begrenzte Probleme funktioniert das ziemlich gut, und ich kann mir von AI eine vollständigere und robustere Implementierung bauen lassen, als ich sie selbst schnell schreiben würde.
Selbst wenn ich später doch eine Library hinzufüge, entsteht eine natürliche Kapselung. Ich muss nur die Implementierung meiner Funktion so ändern, dass sie die Library nutzt, und nicht zwingend alle Verwendungsstellen anfassen. Wenn die Zeit gekommen ist, lassen sich auch mehrere Libraries leichter ausprobieren.
Ironischerweise ist das ziemlich interessant. Armin ist der ursprüngliche Autor des Python-Web-Frameworks Flask. Etwa zur gleichen Zeit gab es mit Bottle eine sehr ähnliche Library.
Die Funktionen waren fast identisch, aber während Flask sehr populär wurde, habe ich immer Bottle bevorzugt. Es bestand aus einer einzelnen Datei und hatte keine Dependencies, sodass man es sehr einfach direkt in ein Projekt kopieren konnte.
Es war auch leicht umzubauen, und irgendwann verstand ich es komplett. Für Server und WebSockets habe ich Gevent angebunden, aber auf diese Weise ließen sich auch ziemlich schwere Projekte bauen.
Noch heute verspüre ich bei kleinen Webprojekten einen starken Impuls, Bottle zu verwenden. Schade ist nur, dass es viele der modernen Praktiken, die Python über die Jahre eingeführt hat, nicht wirklich mitgegangen ist und inzwischen etwas alt wirkt.
Selbst etwas zu bauen erfordert solide Engineering-Kompetenz. Wenn man nur Engineers hat, die in Ökosystemen wie NPM oder PyPI immer nur Libraries heranziehen, werden sie Schwierigkeiten haben, Lösungen für viele Probleme selbst zu entwickeln. Besonders dann, wenn die Lösung langfristig Bestand haben und die nötige Flexibilität bieten soll.
„Sich nicht selbst in eine Sackgasse programmieren“ braucht viel Übung.
Ich habe oft Fälle gesehen, in denen man es leicht besser machen kann als bestehende Libraries. In einem Projekt habe ich einen Parser für eine Markdown-Variante implementiert, bei der oben in der Datei Metadaten stehen. Wir nutzten eine kleine Syntax, und mit weniger als einem Bildschirm Code hatten wir einen Parser.
Die Frontend-Library, die dieselbe Datei parste, war dagegen viel schlechter als erwartet. Sie brach, wenn ein Metadaten-Identifier einen Bindestrich enthielt; wie sich herausstellte, verwendete sie den Metadaten-Identifier direkt als Objekt-Member-Namen. Statt ein simples JSON-Objekt zu verwenden, beschränkte sie die Namenswahl künstlich und sorgte dafür, dass Namen mit Bindestrich wie
something-somethingkaputtgingen.Am Ende wurde mein Parser verworfen, mit der Begründung, man müsse ihn „warten“. Aber er funktionierte einfach und hätte sich leicht an Syntaxänderungen anpassen lassen. Mit ein wenig Parser-Wissen war daran auch nichts schwer zu verstehen.
Kaum zu glauben, aber ich glaube, außer mir hatte im Team niemand je mit einer Parser-Generator-Library einen Parser geschrieben. Solche Beispiele gibt es noch viele mehr.
Wenn man nur eine einzige Funktion nutzt, dafür aber Hunderte kompilieren muss, sollten die Warnlampen angehen. Vor etwa einem Jahr habe ich an einem Projekt gearbeitet, bei dem Third-Party-Abhängigkeiten aktualisiert wurden.
Eine davon war eine umfangreiche Mathematikbibliothek mit allen möglichen mathematischen Funktionen. Nach ein bisschen Nachforschen stellte sich heraus, dass wir daraus nur eine einzige Methode verwendeten: den Median einer Liste zu finden.
Ich verwies den zuständigen Engineer auf die Wikipedia-Seite und bat ihn, die Abhängigkeit zu entfernen und eine einzelne Methode zu schreiben, die diese mathematische Operation ausführt.
Das eigentliche Problem ist allerdings nicht die Nutzung von Third-Party-Abhängigkeiten an sich, sondern dass es ein Konzept braucht, nur einen schmalen Ausschnitt einer Bibliothek einzubinden. Wenn man nur einen kleinen Teil einer riesigen Bibliothek braucht, warum muss man dann die ganze Bibliothek mitnehmen? Ich habe einmal von einem Vorschlag gehört, dafür „Mikroframeworks“ zu verwenden.
Leider scheint es nicht so, als würden die Leute die Standard-Feature-Sets ihrer Abhängigkeiten sorgfältig prüfen und aktiv verkleinern. Die Syntax zum Hinzufügen zusätzlicher Features ist einfach, aber um standardmäßig aktivierte optionale Features zu entfernen, muss man
no-default-featuresangeben und dann die gewünschten Standardfeatures trotzdem wieder einzeln hinzufügen, was umständlicher ist.Noch schlimmer ist: Wenn eine Bibliothek es ermöglichen soll, bei ihren eigenen Abhängigkeiten unnötige Features herauszuschneiden, muss sie eigene Features erstellen und diese auf die Features der jeweiligen Abhängigkeiten mappen. Wenn man zum Beispiel 5 Abhängigkeiten hat und jede davon 1 Pflichtabhängigkeit und 4 optionale Abhängigkeiten mitbringt, müsste man in der eigenen Bibliothek 20 Mapping-Features anlegen, damit Nutzer die transitiven Features vollständig kontrollieren können. Features, die man aus Rücksicht auf nachgelagerte Nutzer für den eigenen Code bauen würde, sind darin noch gar nicht enthalten.
Ich komme immer mehr zu der Ansicht, dass die Usability rund um Features, insbesondere beim Reduzieren unnötiger Aufblähung, so schlecht ist, dass sie ein Katalysator für Rusts Compile-Time-Probleme ist. Wenn solche Themen aufkommen, scheinen sie nicht breit diskutiert zu werden; vielleicht sollte ich meine deutliche Meinung dazu in einem Blogpost festhalten, damit man darauf verweisen kann, falls der Status quo anhält.
Wenn das Vertrauenssystem gut ist, halte ich das einerseits nicht für ein Problem.
Ich mag bis zu einem gewissen Grad auch den Ansatz von Shadcn: Wenn einem eine Komponente gefällt, kopiert man sie in die eigene Bibliothek. Aber wenn eine Schwachstelle entsteht, weiß ich nicht, ob ich betroffen bin.
Bei manchem Code ist das in Ordnung, bei anderem Code muss man sich wirklich darauf verlassen, dass möglichst viele Augen darauf schauen.
Median(list) { let len = length(list) if len % 2 == 0 { let x = floor(len/2) return (list[x] + list[x+1]) / 2 } return list[len/2] }Allerdings unter der Annahme, dass die Liste bereits sortiert ist. Der Versuchung, hier
is_oddaufzurufen, habe ich widerstanden.