Online Magazine
Schreib sauberen Code mit den 5 SOLID-Prinzipien

Als Data Scientist hast du genug zu tun und solltest nicht auch noch deinen Code bereinigen müssen, oder? Vielleicht nicht, aber du und das gesamte Data-Science-Projekt profitieren von einer sauberen Codebasis. Ich zeige dir 5 einfache Prinzipien, die dir dabei helfen, eine solche zu erstellen.
von Anel Music

Es stimmt: Als Data Scientist hat man einen Haufen verschiedener Aufgaben zu erledigen: Man muss unordentliche Datensätze verarbeiten, den Daten einen Sinn geben, indem man die richtigen Fragen stellt, bewerten, welche Algorithmen und statistischen Methoden zur Modellierung dieser Daten geeignet sind, Experimente durchführen und schliesslich mit Engineers, Fachexpert*innen sowie Manager*innen und Kund*innen kommunizieren. Zumindest das Schreiben von sauberem Code und die "Produktionalisierung" deiner Jupyter Notebooks könnte also jemand anders übernehmen – z. B. Software- oder Machine-Learning-Engineers – oder?
Ja und nein. Natürlich sind ML-Engineers dafür verantwortlich, das Modell in die Produktion zu bringen. Es gibt jedoch mindestens 2 gute Gründe, warum du deine Kodierung im Laufe der Zeit trotzdem verbessern solltest:
- Als Data Scientist strebst du vielleicht danach, eines Tages selbst ML-Engineer zu werden – und sauberen Code zu schreiben dient als Vorbereitung auf diesen Schritt.
- Selbst wenn dies nicht der Fall ist, verkürzt sauberer Code, der nicht erst grundlegend umgeschrieben oder überarbeitet werden muss, die Zeit fürs Model Deployment sowie den Feedback Loop. Dies kommt letztlich allen zugute, denn es führt zu schnelleren Iterationen, schnellerem Deployment, schnellerer Verbesserung und schnellerer Kundenzufriedenheit.
Nun, da wir festgestellt haben, dass es sinnvoll ist, die Codierung zu verbessern, wie definieren wir überhaupt sauberen Code?
Kurz und bündig: Was ist sauberer Code?
Einfach ausgedrückt: Sauberer Code ist leicht zu lesen, leicht zu verwenden, leicht zu erweitern und leicht zu testen.
Es gibt 5 Softwaredesign-Prinzipien, die sogenannten SOLID-Prinzipien, die dir helfen, solchen Code zu schreiben. Anfangs musst du dich vielleicht zwingen, die entsprechenden Grundsätze zu befolgen, aber wenn du sie einmal verinnerlicht hast, wirst du sie ohne nachzudenken anwenden. Gehen wir also jedes der 5 SOLID-Prinzipien durch und finden heraus, was sie beinhalten.
SOLID-Prinzip Nr. 1: Das Single-Responsibility-Prinzip
Die Idee: Deine Klasse sollte nur eine Aufgabe haben.
Was das bedeutet: Vielleicht hast du schon einmal von dem so genannten «Gottobjekt» (en. god object) gehört. Das Gottobjekt ist die Instanz einer Klasse, die praktisch alles tun kann. Im Data-Science-Kontext könnte dies eine Klasse sein, die Daten liest, Vorverarbeitungen durchführt, Modelle trainiert und auswertet, neue Vorhersagen macht und möglicherweise sogar Nachverarbeitungen vornimmt. Obwohl dies nach einer erstaunlichen und sehr potenten Klasse klingt, hat die Konzentration der Verantwortlichkeiten für völlig unterschiedliche Aufgaben in einer Klasse viele Nachteile. Zum einen wird die Klasse sehr gross, und es wird schwierig, die Auswirkungen von Änderungen zu überblicken. Glücklicherweise ist das Refactoring von Gottklassen sehr einfach.
Beispiel: Nehmen wir an, du hast einen Klassifikator, wie in Abbildung 1 dargestellt. Der Klassifikator hat 2 Mitgliedsvariablen (Name und Leistung) und 2 Methoden zur Vorhersage und Aktualisierung eines einfachen Modellleistungs-Dashboards. Ich denke, wir sind uns alle einig, dass ein Klassifikator nicht für die Aktualisierung des Dashboards verantwortlich sein sollte.
Abbildung 1: Verstoss gegen das Single-Responsibility-Prinzip.
Die Lösung: Wir können den Verstoss gegen das Single-Responsibility-Prinzip recht einfach beheben, indem wir die Verantwortung für das Dashboard an eine separate Dashboard-Klasse delegieren (siehe Abbildung 2). Dazu führen wir einfach eine neue Klasse namens "Dashboard" ein und lassen sie via Aktualisierungsmethode das Dashboard aktualisieren. Durch die strikte Trennung der Zuständigkeiten werden unsere Klassen viel kürzer und sind leichter zu erklären und zu verstehen. Unter anderem deshalb sind Microservices als Architektur derzeit so beliebt.
Abbildung 2: Das Single-Responsibility-Prinzip ist wiederhergestellt.
SOLID-Prinzip Nr. 2: Das Open-Closed-Prinzip
Die Idee: Deine Klasse sollte offen für Erweiterungen, aber geschlossen für Änderungen sein.
Was das bedeutet: Jeder Code ist offen für Erweiterungen – du kannst also jederzeit neue Funktionen hinzufügen. Idealerweise solltest du jedoch in der Lage sein, dies zu tun, ohne den bestehenden Code in irgendeiner Weise zu ändern. Eine Änderung des bestehenden Codes birgt nicht nur die Gefahr, neue Fehler einzubringen, sondern kann auch dazu führen, dass du bereits bestehende Unit-Tests erweitern musst. Das kann schwierig sein – vor allem, wenn du nicht ganz verstehst, was die Funktion, um die du erweitert hast, tut.
Beispiel: Stell dir vor, du hast die in Abbildung 3 gezeigte store_data-Methode. Je nach storage_type speichert diese Funktion Daten entweder in einer SQL-Datenbank oder in einer CSV-Datei. Nun möchtest du eine neue Funktion hinzufügen, mit der du die Daten in einer MongoDB-Datenbank speichern kannst. Am einfachsten wäre es, der store_data-Methode eine weitere if-Bedingung hinzuzufügen, zu prüfen, ob (storage_type == "mongodb") und, wenn dies der Fall ist, einen //store to mongodb-Code auszuführen.
Dies würde einwandfrei funktionieren, würde jedoch gegen das Open-Closed-Prinzip verstossen, da das Hinzufügen deiner neuen Funktion (Erweiterung) die Änderung von bereits vorhandenem Code (Modifikation) erfordern würde.
Abbildung 3: Verstoss gegen das Open-Closed-Prinzip.
Die Lösung: Abbildung 4 zeigt eine Möglichkeit, den Verstoss gegen das Open-Closed-Prinzip zu beheben. Wie du siehst, hat der DatasetManager keine store_data-Methode mehr, sondern einen Member Storer vom Typ DataStorer – eine Schnittstelle, die definiert, was verschiedene Typen von DataStorern gemeinsam haben. In diesem Fall haben alle DataStorers eine store_data()-Methode.
Wenn du deine Daten in eine SQL-Datenbank schreiben willst, kannst du eine Klasse SQLStorer erstellen, die von der DataStorer-Schnittstelle erbt (die Schnittstelle DataStorer implementiert) und die Funktion store_data() implementiert.
Möchtest du eine neue Funktion zum Schreiben der Daten in eine CSV-Datei hinzufügen, musst du den bestehenden Code nicht ändern. Du kannst eine neue Klasse CSVStorer erstellen, die ebenfalls von DataStorer erbt und die Methode store_data() definiert. Wenn es darum geht, eine neue MongoDB-Funktion hinzuzufügen, musst du lediglich den Code erweitern, indem du eine neue MongoDBStorer-Klasse erstellst, ohne die vorhandenen Klassen oder Funktionen zu ändern.
Du hast wahrscheinlich erkannt, dass die DataStorer-Schnittstelle eine gemeinsame "Vorlage" für alle Arten von DataStorern (SQL, CSV, MongoDB, S3, BlobStorage usw.) bietet. Da unser DatasetManager von einem generischen Speicherobjekt vom Typ DataStorer abhängt, kannst du ihm ein beliebiges Objekt einer DataStorer-Unterklasse übergeben. Dies hat den Vorteil, dass du, wenn du die Art und Weise, wie du deine Daten speicherst, änderst (z. B. von CSV zu S3), einfach ein S3Storer-Objekt anstelle eines CSVStorer-Objekts an deinen DatasetManager-Konstruktor übergeben kannst, ohne den Client-Code zu verändern.
Im Allgemeinen sollten deine Klassen immer von Abstraktionen (Schnittstellen) und nicht von Implementierungen (konkreten Klassen) abhängen.
Abbildung 4: Das Open-Closed-Prinzip ist wiederhergestellt.
Schon von DataOps gehört?
Für Unternehmen ist es mittlerweile problemlos möglich, Unmengen an Daten anzuhäufen – weniger einfach ist es allerdings, daraus schnell und skalierbar Insights zu gewinnen. Ein möglicher Lösungsansatz ist DataOps.
Erfahre mehr dazu in diesem Artikel!
SOLID-Prinzip Nr. 3: Liskovsches Substitutionsprinzip
Die Idee: Du solltest in der Lage sein, ein Objekt einer übergeordneten Klasse durch ein beliebiges Objekt einer untergeordneten Klasse zu ersetzen, ohne die Korrektheit deines Codes zu beeinträchtigen.
Was das bedeutet: Wenn du von dieser formalen Definition des Liskovschen Substitutionsprinzips etwas verwirrt bist, keine Sorge – du bist nicht allein.
Auch wenn es auf den ersten Blick nicht so aussieht, ist das Liskovsche Substitutionsprinzip eigentlich recht einfach, da es mit konkreten Prüfpunkten arbeitet. Zwei Programmierer werden sich also niemals uneinig darüber sein, ob das Prinzip verletzt wird oder nicht (was z. B. beim Single-Responsibility-Prinzip vorkommen kann). Das bedeutet auch, dass moderne Linter wie mypy dabei helfen können, solche Verstösse zu erkennen. Um sie zu beheben, ist es aber trotzdem wichtig, die Idee hinter dem Prinzip zu verstehen.
Meiner Meinung nach würde eine einfache Vorher-Nachher-Darstellung hier nicht ausreichen. Daher werde ich ein wenig mehr ins Detail gehen.
Beispiel: Angenommen du möchtest für eine Bestellung bezahlen, die du gemacht hast. Eine einfache Umsetzung ist in Abbildung 5 dargestellt. Hier hast du eine ApplePay-Klasse, die für die Verarbeitung der Zahlung verantwortlich ist. Zu diesem Zweck verfügt die ApplePay-Klasse über eine pay()-Methode, welche die Bestellung und eine Telefonnummer entgegennimmt, die mit einer Art Verifizierungsverfahren innerhalb der pay()-Methode überprüft werden muss. Der Abschluss dieser Prozedur setzt den Status der Bestellung auf "bezahlt". Die PaymentProcessor-Schnittstelle dient als "Vorlage" und sollte von allen möglichen unterschiedlichen PaymentProcessors implementiert werden.
Auf der linken Seite siehst du den recht einfachen Client-Code. Zunächst instanziierst du die Bestell-Klasse und fügst eine Tastatur zu deiner Bestellung hinzu. Dann instanziierst du die ApplePay-Klasse und rufst die Methode pay() mit deiner Bestellung und Telefonnummer auf.
Abbildung 5: Klassen ohne Verstoss gegen das Liskovsche Substitutionsprinzip.
Nehmen wir nun an, du möchtest eine neue Funktion hinzufügen, die Zahlungen über PayPal ermöglicht. Du kannst die Schnittstelle PaymentProcessor implementieren und eine neue Klasse namens PayPalPay erstellen, wie in Abbildung 6 gezeigt. Für PayPalPay würdest du eine Art Verify-NR-Prozedur in der Methode pay() implementieren und den order.status auf «bezahlt» setzen. Der Client-Code ändert sich fast nicht. So weit, nichts Neues und auch keine Verletzung des Liskovschen Substitutionsprinzips.
Abbildung 6: Klassen ohne Verstoss gegen das Liskovsche Substitutionsprinzip.
Leider arbeitet PayPal nicht mit der Verifizierung von Telefonnummern. Stattdessen wird eine E-Mail-Adresse zur Verifizierung eines Kontos verwendet, wie in Abbildung 7 dargestellt:
Abbildung 7: Klassen ohne Verstoss gegen das Liskovsche Substitutionsprinzip.
Eine schnelle Lösung für dieses Problem ist in Abbildung 8 dargestellt. Anstatt die Telefonnummer im Client-Code payer.pay(order, '+491520000') zu übergeben, kannst du einfach eine E-Mail-Adresse pay.pay(order, 'abc@def.com') übergeben und – anstelle eines Telefonnummern-Verifizierungsverfahrens – ein E-Mail-Verifizierungsverfahren implementieren. Du musst lediglich darauf achten, dass der Parameter phone_nr keine Telefonnummer, sondern eine E-Mail-Adresse enthält. Leider kannst du den Parameternamen nicht von phone_nr in email_adress ändern, da du dich an die PaymentProcessor-Schnittstelle hältst.
Abbildung 8: Verstoss gegen das Liskovsche Substitutionsprinzip.
Dies würde auf jeden Fall funktionieren, die erwartete Ausgabe liefern und sollte keine Probleme verursachen, wenn ...
... du daran denkst, dass die phone_nr in der pay()-Methode der PayPalPay-Klasse eine E-Mail-Adresse ist,
... du diesen Parameter phone_nr für deine Zwecke in der E-Mail-Überprüfungsprozedur wie eine E-Mail-Adresse behandelst,
... du nicht vergisst, eine E-Mail-Adresse anstelle einer Telefonnummer an die Methode pay() im Client-Code zu übergeben, wenn du die PayPalPay-Klasse verwendest, und
... niemand versehentlich eine Telefonnummer an die Methode pay() eines PayPalPay-Objekts übergibt, was zu einem Fehler in der E-Mail-Überprüfungsprozedur der Methode pay() in der PayPalPay-Klasse führen würde.
Viel zu viele "Wenns" – wenn du mich fragst.
Wie du siehst, führt die Verletzung des Liskovschen Substitutionsprinzips selbst bei diesem einfachen Beispiel zu einer Vielzahl von Problemen. Diese sind im Wesentlichen dadurch entstanden, dass die Objekte deiner Unterklassen nicht austauschbar sind. Um genauer zu sein: Du kannst die Zahler-Objekte in den beiden obigen Client-Code-Schnipseln nicht austauschen, weil die Art und Weise, wie die pay()-Methode aufgerufen wird, davon abhängt, welche Klasse (ApplePay oder PayPalPay) du instanziierst.
Abbildung 9: Das Liskovsche Substitutionsprinzip ist wiederhergestellt.
Die Lösung: Abbildung 9 zeigt, wie der Verstoss behoben werden kann. Um den Parameter phone_nr nicht mehr als E-Mail-Adresse zu missbrauchen, entferne ihn aus der Methode pay() der Schnittstelle PaymentProcessor. Auf diese Weise sieht jeder Client-Code-Aufruf der Methode payer.pay(order) unabhängig von der Klasse, die du instanziierst, genau gleich aus, da ein zweiter Parameter (phone_nr/email_address) nicht mehr erforderlich ist. Ob die Verifizierung über eine E-Mail-Adresse oder eine Telefonnummer erfolgt, wird nun im Konstruktor selbst kodiert. ApplePay hat jetzt ein Mitglied phone_nr, und PayPalPay hat ein Mitglied email_addr. Die Verifizierungsprozedur innerhalb der Methode pay() greift also auf das entsprechende Mitglied (phone_nr/email_address) zu.
Du verstösst nun nicht mehr gegen das Liskovsche Substitutionsprinzip und kannst daher bei Bedarf das mit der ApplePay-Klasse erstellte Zahlerobjekt durch das mit der PayPalPay-Klasse erstellte Zahlerobjekt ersetzen. Dadurch kannst du die Methode pay(order) für jedes beliebige Zahlerobjekt aufrufen und machst keine Fehler, die zu Abstürzen führen könnten.
SOLID-Prinzip Nr. 4: Interface-Segregation-Prinzip
Die Idee: Es ist besser, mehrere spezifische Schnittstellen zu haben, als eine grosse allgemeine Schnittstelle.
Was das bedeutet: Wie bei dem oben erwähnten Gottobjekt klingt es zunächst praktisch, eine grosse Schnittstelle zu haben, die alle Methoden deklariert, welche Unterklassen möglicherweise implementieren wollen. Wenn man jedoch darüber nachdenkt, ist es das überhaupt nicht.
Beispiel: Abbildung 10 zeigt, was passiert, wenn man Schnittstellen hat, die zu allgemein sind: Die ImgSegmenter-Schnittstelle bietet eine gemeinsame "Vorlage" für alle ImgSegmenter-Unterklassen. Wenn du eine konkrete Klasse erstellen willst, die von ImgSegmenter erbt, musst du eine Implementierung für alle abstrakten Methoden (segment_semantics, segment_instances) bereitstellen, die in der ImgSegmenter-Schnittstelle deklariert sind. Andernfalls wird der Compiler (oder Interpreter) einen Fehler ausgeben, wenn du versuchst, ein Objekt zu erstellen.
Dies ist zum Beispiel bei DeepLab der Fall: DeepLab ist ein semantischer Segmentierungsalgorithmus. Du kannst also nur die Implementierung für die Methode segment_semantics() bereitstellen. Da die Schnittstelle vererbt wird, bist du jedoch gezwungen, auch die segment_instances()-Methode zu implementieren. Als Workaround kannst du einen Python-Pass verwenden oder (noch besser) eine Exception auslösen, um anzuzeigen, dass DeepLab nur für die semantische Segmentierung verwendet werden kann. (Im Gegensatz dazu kann der MaskRCNNSuper-Algorithmus sowohl Instanzsegmentierung als auch semantische Segmentierung durchführen, so dass du keine Ausnahmen generieren musst).
Abbildung 10: Verstoss gegen das Interface-Segregation-Prinzip.
Die Lösung: Eine bessere Entwurfslösung ist in Abbildung 11 dargestellt. Nach dem Refactoring haben wir zwei spezifische Schnittstellen (ImgSegmenter und InstanceSegmenter). Beide können von ihren jeweiligen Unterklassen (MaskRCNNSuper und DeepLab) vollständig implementiert werden. Es besteht keine Notwendigkeit, Ausnahmen zu erzeugen. MaskRCNNSuper kann für die semantische Segmentierung und die Instanzsegmentierung verwendet werden und implementiert daher die Schnittstelle InstanceSegmenter, die über die Methode segment_instances() verfügt und die Methode segment_semantics() von ihrer Elternklasse ImgSegmenter erbt. DeepLab hingegen arbeitet nur mit semantischer Segmentierung und implementiert daher nur die ImgSegmenter-Schnittstelle.
Da beide konkreten Klassen (MaskRCNNSuper und DeepLab) die gleiche Oberklasse ImgSegmenter haben (dank Polymorphismus), kannst du Objekte beider Klassen an den Konstruktor deiner Modellierungsklasse übergeben. An dieser Stelle möchte ich noch einmal betonen, dass deine Klassen immer von Abstraktionen (Interfaces) und nicht von einer Implementierung (konkrete Klasse) abhängen sollten.
Abbildung 11: Das Interface-Segregation-Prinzip ist wiederhergestellt.
SOLID-Prinzip Nr. 5: Dependency-Inversion-Prinzip
Die Idee: Klassen sollten von einer Abstraktion abhängen und nicht von konkreten Unterklassen.
Was das bedeutet: Wie bereits erwähnt: Klassen sollten immer von Abstraktionen abhängen und niemals von einer konkreten Implementierung. Versuchen wir nun zu verstehen, was wirklich hinter diesem Prinzip steckt.
Beispiel: Abbildung 12 zeigt eine Modellierungsklasse, die direkt von einer DeepNN-Klasse abhängig ist, weil sie einen Mitgliedsalgorithmus vom Typ DeepNN hat. Innerhalb ihrer fit_data()-Methode ruft sie die fit_deepNN()-Methode der DeepNN-Klasse auf.
Abbildung 12: Verstoss gegen das Dependency-Inversion-Prinzip.
Angenommen, die Anforderungen haben sich geändert (z. B. steht weniger leistungsfähige Hardware als erwartet zur Verfügung), und du musst ein wesentlich schnelleres Modell wie die logistische Regression verwenden. Hierfür kannst du eine neue Klasse LogReg erstellen, wie in Abbildung 13 gezeigt. Wenn du nun ein Objekt vom Typ LogReg an den Konstruktor der Modellierungsklasse übergibst, bricht der Code ab, da der Konstruktor der Modellierungsklasse einen Algorithmus vom Typ DeepNN erwartet. Ausserdem wird in seiner fit_data()-Methode fit_deepNN() aufgerufen, die nur in der DeepNN-Klasse und nicht in der LogReg-Klasse verfügbar ist.
Abbildung 13: Verstoss gegen das Dependency-Inversion-Prinzip.
Um das Problem zu beheben, kannst du den Modellierungskonstruktor so ändern, dass er einen Algorithmus vom Typ LogReg erwartet, wie in Abbildung 14 dargestellt. (Dies verstösst übrigens gegen das Open-Closed-Prinzip). Ausserdem musst du die Implementierung der fit_data()-Methode so ändern, dass sie algorithm.fit_LogReg() anstelle von algorithm.fit_deepNN() aufruft.
Abbildung 14: Verstoss gegen das Dependency-Inversion-Prinzip.
Auf den ersten Blick sieht diese kleine Änderung vielleicht nach keiner grossen Sache aus, aber denk mal darüber nach, was passieren würde, wenn sich die Anforderungen noch einmal verändern würden – wenn z. B. innerhalb des Datensatzes die Anzahl der Merkmale steigt, während die Anzahl der Beobachtungen sinkt. In diesem Szenario könnte ein SVM-Klassifikator besser geeignet sein. Du müsstest wiederum den Konstruktor der Modellierungsklasse ändern und ihre fit_data()-Methode die fit_svm()-Methode einer neuen SVM-Klasse aufrufen lassen.
Das Problem dabei ist, dass deine Modellierungsklasse von einer konkreten Implementierung (einer Unterklasse) abhängt. Das bedeutet, dass du jedes Mal, wenn du einen anderen Klassentyp (deepNN, logReg, SVM) an den Konstruktor übergibst, die Modellierungsklasse ändern müsstest. Wäre es nicht toll, wenn man dem Konstruktor der Modellierungsklasse Objekte übergeben könnte, ohne den Typ, den er erwartet, immer wieder zu ändern? Das können wir ganz einfach erreichen, indem wir die Abhängigkeit umkehren.
Die Lösung: Die refaktorisierte Lösung in Abbildung 15 kehrt die Abhängigkeit um, indem sie die Modellierung von einer Abstraktion (einer Schnittstelle) statt von einer konkreten Klasse (einer Implementierung) abhängig macht.
Auf diese Weise erwartet der Konstruktor ein Objekt vom Typ Model anstelle von DeepNN oder LogReg. DeepNN und LogReg sind nun konkrete Unterklassen, welche die Schnittstelle Model implementieren und Objekte erzeugen, die an die Modellierung weitergegeben werden können. Da beide konkreten Klassen (DeepNN und LogReg) der Schnittstelle Model folgen, müssen beide eine fit()-Methode implementieren. Das bedeutet auch, dass unabhängig vom Typ des Objekts, das an den Modellierungskonstruktor übergeben wird, der Inhalt der fit_data(data)-Methode unverändert bleibt (algorithm.fit()).
Abbildung 15: Das Dependency-Inversion-Prinzip ist wiederhergestellt.
Fazit
Um sauberen Code zu schreiben, kannst du dich an 5 Prinzipien halten (die so genannten SOLID-Prinzipien):
- Single-Responsibility-Prinzip: Deine Klasse sollte nur eine Aufgabe haben.
- Open-Closed-Prinzip: Deine Klasse sollte offen für Erweiterungen, aber geschlossen für Änderungen sein.
- Liskovsches Substitutionsprinzip: Du solltest in der Lage sein, ein Objekt der Oberklasse durch ein beliebiges Objekt der Unterklasse zu ersetzen, ohne die Korrektheit deines Codes zu verändern.
- Interface-Segregation-Prinzip: Es ist besser, mehrere spezifische Schnittstellen zu haben als eine grosse allgemeine Schnittstelle.
- Dependency-Inversion-Prinzip: Klassen sollten von der Abstraktion abhängen und nicht von konkreten Unterklassen.
Auch wenn diese Prinzipien zunächst einschüchternd wirken mögen, werden sie schnell zur zweiten Natur. Sie machen deinen Code modular, lesbarer, verständlicher und einfacher zu testen. Dies wiederum kommt allen Beteiligten eines Data-Science-Projekts zugute: Die Zeit für das Model-Deployment und die Feedback Loops verkürzen sich erheblich, was zu schnelleren Iterationen, schnellerem Deployment, schnellerer Verbesserung und schnellerer Kundenzufriedenheit führt.

HIER FINDEST DU WEITERE ARTIKEL UNSERER DATA & AI EXPERT*INNEN:

