Rene Weiß
22.12.2020
Türchen #22 im arcvent(s)kalender 2020
Als eines der letzten Türchen im Arcvent(s)kalender 2020 möchte ich euch eine weitere Refactoring Möglichkeit vorstellen (Wieso weitere? Weil mein Kollege Falk in Türchen # 7 die Mikado Methode vorgestellt hat)
Ihr kennt das vielleicht: Es gibt technische Schulden, die sich über einen längeren Zeitraum aufgebaut haben. Mangels gefahrloser Refactoring-Optionen, fehlender Tests oder auch mangels Wissen der aktuellen EntwicklerInnen lassen sich die Schulden nicht so einfach beheben.
Ich möchte euch mit dem “Scientist” eine Bibliothek vorstellen, die es ermöglicht, den Output einer neuen Implementierung einer Komponente mit dem einer Alten zu vergleichen, um so schnell und einfach eine neue Version in Produktion zu testen. Damit lässt sich eine neue Version gefahrlos so lange ausprobieren bis man mit dem Ergebnis zufrieden ist und somit technische Schulden einfach und effizient beheben.
Gleichzeitig ist das Vorgehen mit dem “Scientist” (Library) ein schönes Beispiel für eine “temporäre” Fitness Function (eine Einführung zu Fitness Functions findet ihr z.B. hier). Die Idee und auch die erste Implementierung einer Scientist Library ist bei GitHub entstanden. GitHub hat damit den Merge-Algorithmus in der laufenden Proudktion ausgetauscht (GitHub - Move fast and fix things).
Den vollständigen Source Code zum verwendeten Beispiel des Artikels findet ihr am Ende des Artikels verlinkt.
Angenommen ihr habt eine wenig od. gar nicht (automatisiert) getestete Komponente in eurem Softwaresystem. Oft ist etwas über Jahre entstanden (“es ist gewachsen”) und funktioniert soweit ganz gut, aber ist schwer oder gar nicht mehr wartbar. Aber niemand traut sich mehr, den bestehenden Source-Code anzugreifen, um etwas anzupassen oder zu erweitert. Das kennt wohl der eine oder andere, irgendwie. Ist die besagte Komponente halbwegs gut gekapselt, sodass man sie mittels Input- und Output-Parametern arbeiten kann, habe ich eine Idee wie ihr mit dem Problem umgehen könnt.
Normalerweise würde man nun beginnen Tests zu erstellen, das “defekte Bauteil” (= unsere Komponente) damit absichern und sich dann bei hinreichend guter Testabdeckung an das Refactoring od. den Austausch wagen. Aber gerade die Erstellung der hohen Testabdeckung ist ja meist aufwändig oder nicht praktikabel, da ihr die reale Welt mit den unzähligen Kombinationsmöglichkeiten einfach nicht gut genug abgedeckt bekommt.
Genau an dieser Stelle kommt Scientist in’s Spiel. Ein Framework mit dem ich gefahrlos in Produktion eine Parallelversion testen kann. Ohne ewig lange Tests schreiben zu müssen - es wird eine neue Version einfach in Produktion als “Experiment” ausprobiert.
Anhand eines kleinen und sehr überschaubaren Beispiels möchte ich euch das Scientist Framework vorstellen.
In meinem Beispiel habt ihr eine einfache Hauptanwendung, die eine “Calculation Library” verwendet (die Calculation Library macht sehr komplizierte und wichtige Berechnungen ;-)). Die Komponente, die auszutauschen wäre, ist diese Calculation Lib; die Bibliothek hat eine saubere Schnittstelle doch die Implementierung kann kaum noch gewartet werden. Tests gibt es leider auch keine.
Hier ein schnelles Überblicksbild über unsere Komponenten:
Es beginnt nun also jemand eine neue Version der Calculation Library zu schreiben und setzt dabei alles nach besten Wissen und Gewissen um (… diesmal natürlich auch gut gekapselt und mit Unit und evt. auch sinnvollen Integrationstests!). Und genau diese Version wollen wir jetzt in Produktion auf Herz & Nieren testen. Als gefahrloses Experiment.
Dafür verwenden wir zwischen der “Business Logic” und unseren 2 Calculcation Library Versionen den Scientist, der es uns ermöglicht dieses “Experiment” gefahrlos durchzuführen. Die neue Version, die wir testen möchten, ist “Calculation Lib. V2”. Schematisch sieht dies dann so aus:
Wenn jetzt ein Aufruf der Business Logic auf die Calculation Lib erfolgt, führt der Scientist den Aufruf auf beiden Versionen der Calculation Lib durch, vergleicht deren Ergebnis (und misst auch deren Performance, die Laufzeit des Aufrufs) und ermöglicht uns mit diesem Ergebnis etwas zu machen. Zum Beispiel alles fein säuberlich in einem Log zu vermerken.
In der Businesslogik wird aber weiterhin das originale Ergebnis von Library V1 verwendet, so wie bisher auch. Das ermöglicht uns, die neue Version mit echten real-world-use-cases besser zu machen. Eventuell bestehende Bugs in der alten Version fallen durch die Neuimplementierung jetzt auch auf.
Aber sehen wir uns einmal die Implementierung in dem Beispiel genauer an. Nachdem ich immer noch stärker in der .NET Welt zu Hause bin, ist mein Beipsiel auch in C# gehalten. An dieser Stelle sei erwähnt, dass es die Scientist Library in praktisch allen gängigen Programmiersprachen gibt. Die Syntax, wie genau dann so ein Experiment formuliert wird, ist dann sprachspezifisch und je nach Portierung unterschiedlich - aber ich denke der nötige Source-Code ist relativ einfach und für euch sicher kein Problem, das in eurer Sprache nachzubauen. ;-)
Anbei einmal als Überblick die beiden Versionen der Calculation Lib. Für die Einfachheit des Beispiels habe ich die Berechnung einfach in eine Klasse gesteckt und diese verfügt auch nur über einen Parameter (“int context”). In echten Projekten ist dies natürlich etwas komplizierter…
Hier ein Auszug aus der komplizierten Originalversion (Version 1), die wir ersetzten möchten:
public int GetResult(int context)
{
if (context == 1)
{
int result;
// Do other complicated stuff
Thread.Sleep(2);
int sum1 = context;
int sum2 = context;
result = sum1 + sum2;
return result;
}
if (context == 2)
{
int result;
//...
Hier die Version 2, die wir jetzt in Produktion testen möchten (ist ja schon viel kompakter geworden…):
public int GetResult(int context)
{
switch (context)
{
case 1:
return context + context;
case 2:
return context * context;
case 3:
return context + (context + 1) * context;
default:
return context;
}
}
Diese 2 Versionen werden jetzt in einem Scientist Experiment zusammengeschalten und verwendet. Das geht mit wenigen Code-Zeilen:
private static int GetResultWithScientist(int input)
{
int result = Scientist.Science<int>("First experiment",
experiment =>
{
experiment.AddContext("Input Data", input);
experiment.Use(() => new ComplicatedCalc_V1()
.GetResult(input)); // Old Version
experiment.Try(() => new ComplicatedCalc_V2()
.GetResult(input)); // New Version
});
return result;
}
Aber alles “der Zeile nach”.
Als Resultat der Methode wird weiterhin das Ergebnis von ComplicatedCalc_V1 verwendet und somit in unserer Business-Logik verwendet.
Zu guter Letzt müssen wir noch die Ergebnisse der beiden Versionen verarbeiten und sichtbar machen. Das passiert, wie es sich für ein simples Beispiel gehört, natürlich auf der Konsole. In einem echten Projekt ist es aber ein Leichtes das Ergebnis dorthin zu schreiben wo es für euch am passendsten ist (Logfile, Datenbank, CSV-Datei, …).
Im Fall der .NET Version des Scientists, den ich verwende, erstellt man einen so genannten “ResultPublisher”.
Hier meine Version des Publishers, der auf die Konsole schreibt und im Falle unterschiedlicher Ergebnisse zwischen V1 und V2 die Farbe des Outputs auf rot stellt:
internal class MyResultPublisher : IResultPublisher
{
public Task Publish<T, TClean>(Result<T, TClean> result)
{
if (result.Candidates.Count > 0)
{
if (result.Mismatched)
{
Console.ForegroundColor = ConsoleColor.Red;
}
Console.WriteLine(
string.Format("Result: {0}, Candidate: {1}",
result.Control.Value,
result.Candidates.First().Value));
Console.WriteLine(
string.Format("Duration: {0}, Candidate: {1}",
result.Control.Duration,
result.Candidates.First().Duration));
var performanceDiff =
result.Control.Duration -
result.Candidates.First().Duration;
Console.WriteLine(
string.Format("Performance improvement: {0}",
performanceDiff));
Console.ForegroundColor = ConsoleColor.Black;
}
return Task.FromResult(0);
}
}
In einem echten Projekt könnt ihr hier natürlich die für euch passende Output-Möglichkeit nutzen.
Mit dem Scientist und der Möglichkeit Experimente in Produktion durchzuführen, könnt ihr relativ schnell eine neue Version einer Bibliothek, einer “Klasse” oder auch Methode live und “in echt” testen ohne viel Zeit in zusätzliche Testfälle zu investieren. Das lohnt vor allem bei komplizierten Logiken, die gut gekapselt sind bzw. die ihr mit überschaubarem Aufwand gut kapseln könnt.
Anbei noch ein paar ergänzende Kommentare zu Voraussetzungen, um den Scientist gut in euren Projekten verwenden zu können:
Unterschiedliche Scientist Versionen sind unter folgenden Links verfügbar:
Den vollständigen Source-Code des hier verwendeten Beispiels findet ihr hier.
Nachdem dies mein letzter Arcvent(s)kalender Beitrag ist - wünsche ich euch viel Spaß beim Ausprobieren und allen schöne und geruhsame Feiertage!
Feedback natürlich wie immer gerne per E-Mail oder Twitter.