In der Programmierung gibt es ein einfaches, aber wirkungsvolles Konzept, das noch immer viel zu selten genutzt wird: Unveränderlichkeit (Immutability).
Grundsätzlich ist ein Objekt unveränderlich, wenn sich sein Zustand nach der Erstellung nicht mehr ändert. Entsprechend ist eine Klasse unveränderlich, wenn ihre Instanzen unveränderlich sind.
Ein wichtiges Argument für unveränderliche Objekte lautet: Sie vereinfachen die nebenläufige Programmierung erheblich. Warum ist es so schwierig, korrekten Multithreading-Code zu schreiben? Weil der Zugriff mehrerer Threads auf Ressourcen – Objekte oder andere Betriebssystemressourcen – synchronisiert werden muss. Und warum ist diese Synchronisierung schwierig? Weil sich nur schwer garantieren lässt, dass zwischen mehreren Lese- und Schreiboperationen verschiedener Threads auf verschiedenen Objekten keine Race Conditions auftreten. Was wäre aber, wenn es überhaupt keine Schreibzugriffe mehr gäbe? Anders gesagt: Was wäre, wenn sich der Zustand der von Threads verwendeten Objekte nicht ändern würde? Dann wäre keine Synchronisierung mehr erforderlich!
Die Klasse des referenzierten Objekts ist unveränderlich, wenn sie keine öffentlichen Felder und keine Methoden besitzt, die ihre internen Daten verändern können, und wenn auch Methoden abgeleiteter Klassen keine Möglichkeit haben, diese internen Daten zu ändern. Da sich der Wert nicht ändern kann, lässt sich in allen Fällen dasselbe Objekt referenzieren. Ein Kopierkonstruktor oder Zuweisungsoperator ist nicht erforderlich. Deshalb empfiehlt es sich, Kopierkonstruktor und Zuweisungsoperator privat zu machen, von boost::noncopyable zu erben oder die neue C++11-Funktion „explizit als default bzw. deleted deklarierte spezielle Memberfunktionen“ zu verwenden.
Auch wenn eine C++-Klasse nicht als unveränderlich konzipiert wurde, kann man Variablen ihres Typs mit dem Schlüsselwort const versehen. Diese Lösung hat jedoch einige Nachteile:
- Sie müssen const bei jeder Variablendeklaration hinzufügen, die unveränderlich sein soll. Wird es einmal vergessen, können im Multithreading-Kontext unerwartete Fehler entstehen.
- Selbst wenn eine Variable als const deklariert ist, kann das Objekt innerhalb einer const-Methode unter Umständen weiterhin verändert werden, wenn bestimmte Felder als mutable deklariert sind. In diesem Fall lässt sich der Objektzustand weiterhin ändern – das Objekt ist also veränderlich.
Die String-Klasse ist beispielsweise in mehreren anderen Sprachen wie C#, Java und D dafür bekannt, unveränderlich zu sein. Strings werden naturgemäß sehr häufig verwendet, und ihre Unveränderlichkeit eignet sich besonders gut für Multithreading-Umgebungen. In C++ ist std::string nicht unveränderlich; als Alternative bleibt daher die Verwendung des Schlüsselworts const.
Domain-Driven Design und Unveränderlichkeit
Domain-Driven Design ist ein Ansatz für Softwaredesign, der auf zwei Grundannahmen basiert:
- Komplexe Domänenentwürfe sollten auf einem Modell basieren, und
- bei den meisten Softwareprojekten sollte der Schwerpunkt auf der Domäne und der Domänenlogik liegen – und nicht auf der konkreten Technologie, mit der das System implementiert wird.
Mit anderen Worten: Das Modell ist das Herzstück von DDD. Eine der ersten Aufgaben zu Beginn der Entwicklung besteht daher darin, dieses Modell zu definieren. Modell und Design sollten sich gegenseitig prägen. Das Modell soll das Wissen über die Geschäftsdomäne abbilden.
Im Allgemeinen betreffen zwischen Threads gemeinsam genutzte Daten Entitäten des Modells. Werden diese Entitäten unveränderlich gemacht, lassen sich Seiteneffekte vermeiden. Wes Dyer bringt es treffend auf den Punkt:
We all know that generally, it is not a good idea to use global variables. This is basically the extreme of exposing side-effects (the global scope). Many of the programmers who don’t use global variables don’t realize that the same principles apply to fields, properties, parameters, and variables on a more limited scale: don’t mutate them unless you have a good reason.(…)Eine Möglichkeit, die Zuverlässigkeit einer Einheit zu erhöhen, besteht darin, Seiteneffekte zu eliminieren. Dadurch lassen sich Einheiten wesentlich einfacher und robuster zusammensetzen und integrieren. Da sie frei von Seiteneffekten sind, verhalten sie sich unabhängig von der Umgebung immer gleich. Dies wird als referenzielle Transparenz bezeichnet.
Ein weiterer Vorteil unveränderlicher Klassen besteht darin, dass sie das Liskovsche Substitutionsprinzip (LSP) nicht verletzen können. Hier eine Definition des LSP, zitiert nach Wikipedia:
Liskov’s notion of a behavioral subtype defines a notion of substitutability for mutable objects; that is, if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g., correctness).Bei unveränderlichen Objekten kann sich der Zustand nicht ändern; folglich können auch die Eigenschaften nicht verändert werden.
Unveränderliche Klassen im C++-Quellcode erkennen
Unveränderlichkeit ist eine Eigenschaft, die bereits zur Kompilierzeit erzwungen werden kann – mit anderen Worten: Sie lässt sich durch Werkzeuge zur statischen Analyse überprüfen.CQLinq, which comes with the static analysis tool CppDepend, stellt die Bedingung IsImmutable für Typen bereit. Damit können Sie ermitteln, welche Typen in Ihrer Codebasis unveränderlich sind:
from t in Types where t.IsImmutable select tNeben der Bedingung IsImmutable für Typen können wir zwei weitere interessante Bedingungen verwenden:
ChangesObjectState und ChangesTypeState. Wie die Namen bereits andeuten, erfasst ChangesObjectState Methoden, die einem Instanzfeld ihrer Klasse einen Wert zuweisen, während ChangesTypeState Methoden erfasst, die einem statischen Feld ihrer Klasse einen Wert zuweisen.
Die Bedingung ChangesObjectState erfasst Methoden, die einem Instanzfeld ihres übergeordneten Typs einen Wert zuweisen.
Die Bedingung ChangesTypeState erfasst Methoden, die einem statischen Feld ihres übergeordneten Typs einen Wert zuweisen.
Mit diesen beiden Bedingungen lassen sich Methoden, die den Zustand des Programms verändern können, leicht identifizieren – also genau die Methoden, die Seiteneffekte verursachen.
Eine Methode, die keine Seiteneffekte verursacht, wird üblicherweise als reine Methode bezeichnet. Um reine Methoden mit CQLinq zu ermitteln, können Sie Folgendes schreiben:
from m in Methods
where !m. ChangesObjectState && !m. ChangesTypeState && !m.IsConstructor
select m
Fazit
Die Beseitigung von Seiteneffekten macht das Zusammensetzen und Integrieren von Einheiten wesentlich einfacher und robuster und vereinfacht zugleich die Multithreading-Programmierung. Unveränderliche Klassen und reine Methoden sind der Schlüssel zur Vermeidung von Seiteneffekten.
