Als die Architektur der Bug war: Refactoring von Legacy-C++ zu modernen Value Semantics

4.5.2026
Zwei Bugs. Zwei ganze Tage verloren. Die Lösung war nicht der nächste Patch, sondern die Architektur sauber aufzuräumen. Eine Fallstudie.

von Tim Varelmann

Dieser Bericht wird mit Genehmigung von Lucky Data geteilt.

Ergebnisse auf einen Blick

  • Eine ganze Klasse wiederkehrender Bugs ist jetzt architektonisch unmöglich. Weniger böse Überraschungen bei der Entwicklung, planbare Software-Releases.
  • Weniger und einfacherer Code bedeutet ab hier günstigere und schnellere Entwicklung.
    • Fast die Hälfte des fehleranfälligsten Codes ist weg, bei gleichzeitig mehr Funktionalität. Zusätzlich deutliche Vereinfachungen im nachgelagerten Code.
    • Eine ganze Gruppe von Hilfsklassen ist obsolet geworden und wurde komplett entfernt. Laufender Wartungsaufwand, der schlicht nicht mehr anfällt.
  • Der einfachere Code ist ohne weiteres bereit für Streaming vom Backend als Ersatz für die derzeitigen Bulk-Loads, die Nutzer ausbremsen.

Über den Kunden

Lucky Data ist ein deutsches IT-Unternehmen, das IT-Dienstleistungen anbietet. Darüber hinaus entwickelt Lucky Data Logistik-Dispositionssoftware, die modernisiert, wie Disponenten die Logistik in der Bauindustrie und in Binnenhäfen planen. Bluebird Optimization unterstützt diese Entwicklung.

Dieser Artikel beschreibt ein Refactoring von stark pointer-basiertem Legacy-C++ zu modernen Value Semantics: eine Änderung, die eine ganze Kategorie von Bugs eliminiert, den Umfang des fehleranfälligsten Codes fast halbiert und die Grundlage für eine schnellere, reaktivere Nutzererfahrung gelegt hat. Für ein Unternehmen, dessen Software täglich operative Entscheidungen mit hohem Einsatz trifft, verkürzt diese Kombination (weniger Fehler, leichter änderbar, schneller für den Nutzer) den Weg von neuen Kundenanforderungen zu ausgelieferten Features.

--

Über Monate hinweg lief die Entwicklung rund. Neue Features gingen in regelmäßigen Releases raus. Nichts Dramatisches, nichts brennt an. Ruhiges Fahrwasser, welches erlaubt, zu planen was als Nächstes kommt, statt sich den Kopf zu zerbrechen über das, was gerade kaputt ist.

Anfang März war das vorbei.

Der erste Bug

Er tauchte in Code auf, der sich noch in Entwicklung befand: ein hartnäckiger Bug mit reproduzierbarem Symptom und ohne offensichtliche Ursache. Ich habe einen ganzen Tag damit verbracht, ihn zu jagen, und hatte am Abend nichts als einen Patch: ein Stück Code, das das Symptom eliminierte, ohne es zu erklären. Mit einem Release am Horizont und anderen noch offenen Aufgaben habe ich entschieden, die Ursachensuche aufzuschieben.

Der Abend war unruhig. Ein ganzer Tag war weg, ohne etwas Vorzeigbares außer einem Fix, den ich nicht vollständig rechtfertigen konnte. Wenn man nicht erklären kann, warum ein Bug auftrat, kann man auch nicht wirklich sicher sein, dass er weg ist.

Der zweite Bug

Eine Woche später, der nächste. Andere Oberfläche, gleiches Gefühl: schwer einzugrenzen, Symptome, die nicht sauber zu dem Code passten, der sie eigentlich hätte produzieren sollen. Wieder fast ein ganzer Tag, wieder ein Patch, wieder Symptome entfernt ohne sie zu verstehen. Der Release stand unmittelbar bevor. Der Patch ging rein, und der Release ging raus.

Nach dem Release war endlich Zeit zum Durchatmen: und zum Ursachen finden.

Ein Schritt zurück

Ohne den Druck wurde schnell klar: Die beiden Bugs waren nicht unabhängig. Ihre Ähnlichkeit deutete auf die Architektur selbst: ein Design rund um Pointer, mit geteiltem Zugriff auf Daten, der sich über große Teile des Codes zog. Jeder Bug hatte seinen eigenen unmittelbaren Auslöser, aber alle hatten dieselbe tiefere Ursache.

Die unbequeme Schlussfolgerung: Diese Architektur hatte keine echte Rechtfertigung für das, was die Software tatsächlich tut. Sie wurde in der Vergangenheit ohne Überlegung implementiert und nie hinterfragt. Und sie produzierte Bugs, die unter anderen Namen immer wiederkommen würden, solange die darunter liegende Struktur unverändert blieb.

Kurzer Exkurs: Stack, Heap und warum Pointer so schwer nachzuvollziehen sind

Um zu erklären, was sich geändert hat, ein kurzer Abstecher dazu, wie Programme Daten speichern:
Computerprogramme halten Daten im Speicher, und der Speicher hat zwei Hauptbereiche: den Stack und den Heap.
Daten auf dem Stack gehören einem bestimmten Stück Code  der Funktion, die sie erzeugt hat. Nur diese Funktion kann sie lesen oder verändern.
Daten auf dem Heap leben für sich. Jedes Stück Code mit einem Pointer (im Wesentlichen eine Adresse, die dem Programm sagt, wo die Daten liegen) kann sie lesen oder verändern. Dasselbe Stück Heap-Daten kann viele Pointer haben, die von vielen Stellen im Programm aus darauf zeigen.
Was daran ist nun problematisch? Auf den ersten Blick klingt das doch in Ordnung: Wenn zwei Teile eines Programms dasselbe Datum verändern, wird jeder seinen guten Grund dafür haben. Und individuell betrachtet stimmt das meistens auch. Das Problem ist nicht die einzelne Änderung. Das Problem ist, dass Entwickler nicht mehr lokal denken können.
Ein Beispiel: Man stelle sich Code vor, der entscheidet, ob LKW 42 um 15 Uhr verfügbar ist. Er liest die Daten: "LKW 42, frei": und beginnt mit dem Zusammenstellen eines Dispositionsauftrags. Zwischen dem Moment des Lesens und dem Moment der echten Disposition markiert ein anderer Teil des Programms, ebenfalls aus einem völlig validen Grund, LKW 42 als in Wartung. Der dispositionierende Code hat keine Möglichkeit, davon zu erfahren. Der LKW wird trotzdem zugewiesen.
Für sich betrachtet sind beide Codeteile korrekt. Der Bug lebt im Raum zwischen ihnen. Und Debugging heißt, jeden Teil des Programms zu verfolgen, der möglicherweise einen Pointer auf LKW 42 hält: in einer größeren Codebasis können das Dutzende oder Hunderte von Stellen sein.
Das heißt nicht, dass Pointer schlecht sind. Jeder, der schon einmal eine Browser-Extension, ein Microsoft-Office-Add-in oder ein Spielemod installiert hat, hat von ihnen profitiert: Die ganze Kategorie "ein laufendes Programm um etwas erweitern, das es ursprünglich nicht kannte" beruht auf pointer-artigen Mechanismen. Pointer sind an der richtigen Stelle die richtige Wahl.
Für den zentralen Datenspeicher, um den es hier geht: das Stück, das die gerade angezeigten Daten und ihre zeitliche Umgebung hält: gab es aber keinen solchen Grund. Die Komplexität der Pointer war reiner Kostenfaktor, ohne Nutzen.

Was wir im Legacy-Code vorfanden

Drei Dinge stachen heraus.

Zu viele Pointer, keiner davon gerechtfertigt

Die Codebasis griff zu Pointern als Standardwerkzeug, in einem Teil des Codes, für den es keinen Anwendungsfall dafür gab. Das Ergebnis war genau der oben beschriebene Kostenfaktor: geteilter Datenzugriff ohne klaren Besitzer, Lebenszyklen, die Stück für Stück rekonstruiert werden mussten, und der Raum zwischen je zwei Zugriffen als potenzieller Bug.

Mutexes als Narbengewebe

Mutexes sind Koordinationsmechanismen für parallel laufenden Code: Sie verhindern, dass zwei Ausführungs-Threads sich gegenseitig in die Daten pfuschen. Aber zurzeit läuft dieser Teil der Software auf einem einzigen Thread. Jeder Mutex darin war irgendwann in der Vergangenheit als Patch zu einem Bug hinzugefügt worden, der wie eine Race Condition aussah. Sie waren Überbleibsel alter Feuerwehreinsätze und bremsten den Code bis heute aus.

Vererbung, wo keine nötig war

Der Code nutzte eine abstrakte Oberklasse als einzigen Zugang zu den Daten für die zwei Arten von Dingen, die die Software verfolgt: Assignments (die geplanten Termine eines Dispositionsplans) und Entities (die Dinge, die an diesen Terminen beteiligt sind: LKWs, Betonmischer, Facharbeiter, Hilfsarbeiter, auszuliefernde Produkte). Vererbung ist ein Mechanismus, der unterschiedlichen Dingen eine übergeordnete Kategorie gibt. Sie ist nützlich, wenn man diese einheitliche Behandlung wirklich braucht, aber in C++ zwingt sie Entwickler praktisch zu Pointern: verschiedene Typen über eine gemeinsame Kategorie zu behandeln erfordert Pointer. Zusätzlich kostet Vererbung Laufzeit und macht den Code schwerer nachvollziehbar.

Das Refactoring

Zwei Entscheidungen haben den größten Teil der Arbeit gemacht.

Der zentrale Datenspeicher hält seine Daten jetzt per value

Wenn ein Teil der Anwendung ein Assignment oder eine Entity braucht, fragt er über eine eindeutige ID an und bekommt eine Kopie. Die Kopie gehört dem Aufrufer (und liegt auf dessen Stack). Niemand sonst kann hineingreifen und etwas ändern. Wenn der Aufrufer fertig ist, verschwindet die Kopie einfach: keine Buchführung, keine Lecks, keine Überraschungen.

Komposition statt Vererbung

Es gibt einen alten Grundsatz im Software-Design: prefer composition over inheritance. Statt dass Assignments und Entities einen gemeinsamen Vorfahren teilen, enthalten sie jetzt jeweils ein kleines gemeinsames Stück (die Eigenschaften, die sie tatsächlich teilen) und sind ansonsten unabhängige Typen. Die übergeordnete Kategorie, die früher der einzige Zugang zu diesen Daten war, steht weiterhin zur Verfügung für Code, der beide Arten wirklich einheitlich behandeln muss: sie ist jetzt mit einem modernen C++17-Mechanismus namens Visitor-Pattern umgesetzt, von dem Entwickler nicht einmal wissen müssen, um ihn zu nutzen. Entscheidend aber: sie ist nicht mehr die einzige Tür. Teile der Anwendung, die wissen, dass sie nur mit Assignments oder nur mit Entities zu tun haben, können jetzt gezielt danach fragen. Weniger "über alles iterieren und anschließend filtern". Direkter, lesbarer.

Was sich in der Praxis geändert hat

  • Eine ganze Klasse von Bugs ist weg. Nicht seltener, unmöglich. Lifetime-Probleme, stille Datenkorruption durch geteilten Zustand, Symptome, die wie Parallelitäts-Bugs in nicht-parallelem Code riechen: sie alle brauchten die alte Architektur, um überhaupt zu existieren. Sie haben nirgendwo mehr einen Landeplatz.
  • Fast die Hälfte der Codezeilen in der Implementierung des zentralen Datenspeichers ist verschwunden, bei gleichzeitig gewachsenem Funktionsumfang. Was bleibt, ist Code, den neue Entwickler lesen und verstehen können, ohne sich zuerst eine mentale Karte davon aufzubauen, wer sonst noch woran rührt.
  • Nachgelagerter Code wurde einfacher. Der Data Provider, der eine zentrale Kalenderansicht speist, hat rund ein Viertel seines Codes verloren. Andere UI-Modelle folgten dem gleichen Muster in kleinerem Umfang.
  • UI-Updates wurden zielgerichteter. Benachrichtigungen über Änderungen an Assignments sind jetzt getrennt von Benachrichtigungen über Änderungen an Entities. Komponenten, die nur Assignments interessieren, müssen Updates über Entities nicht mehr abonnieren und anschließend rausfiltern. Weniger Rauschen, schnellere Reaktion.
  • Eine ganze Hilfsklassen-Hierarchie wurde gelöscht. Das frühere Design brauchte spezielle Klassen nur dafür, inkrementelle Daten-Updates durch die Oberfläche zu transportieren. Die sind weg: die am einfachsten zu wartenden Klassen sind die, die es nicht gibt :)
  • Schnellere Launches sind jetzt in Reichweite. Mit dem Wegfall der Pointer-Koordination ist die Anwendung bereit, Datenströme vom Backend zu konsumieren. Sobald die Backend-Seite ausgeliefert ist, sehen Nutzer ihren ersten Bildschirm mit echten Daten fast sofort, statt auf das Laden einer großen initialen Datenmenge zu warten.

Kurz zusammengefasst bedeutet das: Wir haben Fehler behoben und für die Zukunft vermieden, das Programm ist leichter, schneller und günstiger wart- und weiterentwicklungsbar, vorher undenkbare neue Features sind plötzlich kurz vor fertig. Und all das hat weniger als 2 Wochen fokussierter Arbeit gebraucht.

Rolf Ruß/Patrick Wolff, CEO von Lucky Data, bewertet diese Entwicklung wie folgt:

"Zitat von Patrick/Rolf"

Patchen ist geliehene Zeit

Zwei verlorene Tage kurz vor einem Release waren teuer. Einen Bug zu patchen, ohne ihn zu verstehen, fühlt sich zunächst produktiv an: Das Symptom verschwindet, der Release geht raus, die Liste offener Punkte wird kürzer. Aber tatsächlich leiht man nur Zeit bis zum nächsten Bug. Der zweite Bug, eine Woche nach dem ersten, war die Quittung.

Die eigentliche Arbeit bestand darin, die Architektur ernst zu nehmen als Quelle von vielen möglichen (und zu leicht zu programmierenden) Bugs. Die unpassenden Aspekte einer Architektur zu identifizieren und ihnen die Zeit zu geben, die sie brauchen, um ersetzt zu werden: das war der Schlüssel.

Wenn dir etwas davon in deiner eigenen Codebasis bekannt vorkommt: Bugs, die unter verschiedenen Gestalten immer wiederkommen, Stellen im Code, vor denen alle ein bisschen Respekt haben: lass uns sprechen. Melde dich direkt, oder abonniere Bluebird Briefings, um zu Optimierungs- und Softwareentwicklungs-Themen wie diesem auf dem Laufenden zu bleiben.

Weitere Beiträge

Erfolgsgeschichte: Optimierung von Lagerbestand unter Unsicherheit

Als enge Deadlines auf komplexe Unsicherheit in einem wegweisenden Lageroptimierungsprojekt trafen, wurde aus der Herausforderung durch Vertrauen und Innovation eine Chance. Das Ergebnis: eine siebenstellige Reduzierung der Lagerkosten bei gleichzeitiger Verbesserung der Materialverfügbarkeit.

8.11.2025
Schnellere Lösungen, süße Belohnungen: Mein Solver-Tuning-Erfolg beim Gurobi Summit

Beim Gurobi Summit in Wien wurde eine Solver-Tuning-Challenge zu einem Wettlauf um Performance – und um eine Sachertorte. Wie gezieltes Parametertuning, mir Kuchen und wertvolle Optimierungserkenntnisse brachte.

5.11.2025

Starten Sie Ihr Projekt noch heute

Moderne Software und mathematische Präzision bringt Sie Ihren Zielen näher.
Jetzt Projektanfrage stellen