scheibenkaes.org

Das Leben, Software und der ganze Rest

Meine Demente Tante Objektorientierung

Call-by-reference und call-by-value, wer sich über diese Unterscheidung schon einmal Gedanken machen musste kennt sie vielleicht, Informatikers demente Tante “Objektorientierung”.

Sie vergisst gerne Dinge, hat kaum Gespür für die Zeit und ist immer irgendwie etwas umständlich. Trotz ihrer Schwächen ist sie jedoch in der IT-Branche allgegenwärtig. Es erschließt sich mir zwar heute kaum warum das so ist, aber man hat sich wohl über die Jahre mit ihren Eigenarten arrangiert. Stellt man jedoch in der weiteren Verwandtschaft Nachforschungen an, so fällt auf, dass sich dort deutlich zuverlässigere Personen finden lassen, auch wenn diese einem zu Beginn evtl. fremd erscheinen. Da gibt es bspw. noch Onkel “Funktionale Programmierung”. Er ist alt und wirkt auch ein wenig eigen, jedoch nur so lange bis man hinter die Fassade blickt.

Conways Game of Life

Ein schönes, weil einfaches Beispiel für die Gegenüberstellung von Tante “Objektorientierung” und Onkel “Funktionale Programmierung” ist Conways Game of Life. Eine Simulation in einer zweidimensionalen Welt mit überschaubaren Regeln. Jede Zelle im System ist zu einem Zeitpunkt entweder tot oder lebendig.

  • Eine tote Zelle mit genau drei lebenden Nachbarn erwacht zum nächsten Zeitpunkt zum Leben.
  • Eine lebende Zelle mit weniger als zwei lebenden Nachbarn stirbt.
  • Eine lebende Zelle mit zwei oder drei lebenden Nachbarn bleibt am Leben.
  • Eine lebende Zelle mit mehr als drei Zellen stirbt.

Umständlichkeit

Geht man nun Tantchens Weg, so könnte man folgende Entität definieren, um einen Punkt in der Spielwelt zu beschreiben:

Ein Punkt in C#
1
2
3
4
5
class Point {
public int X;
public int Y;
}

Wäre doch toll wenn man nun folgendes tun könnte:

Vergleich zweier Punkte in C#
1
2
3
4
5
6
7
8
var p1 = new Point{ X = 1, Y = 1};
var p2 = new Point{ X = 1, Y = 1};
if (p1 == p2)
{
// .. da sind wir doch tatsächlich an genau dem selben Punkt in unserer kleinen Welt,
// obwohl er an verschiedenen Stellen im Speicher beschrieben wird.
}

Da wir jedoch nicht den == Operator überladen haben, geht das leider nicht. Nicht nur, dass wir der Tante sagen müssen, was einen Punkt ausmacht, wir müssen ihr auch mitteilen, wann ein Punkt gleich einem anderen ist. Würde sie doch nur mehr auf die Werte, anstatt auf Speicheradressen achten…

Onkel “Funktionale Entwicklung” tut sich da bedeutend leichter, er könnte zwei Punkte in Clojure bspw. so vergleichen:

Zwei Punkte und deren Vergleich in Clojure
1
2
3
4
5
(def p1 [1 1])
(def p2 [1 1])
(= p1 p2)
; Ergibt true

Er weiß nämlich, dass es, wie so oft, auf die (inneren) Werte ankommt.

Die Zeit, die Zeit …

Die Zeit schreitet auch in Conways Modell beständig voran. Um den Zustand zum nächsten Zeitpunkt zu berechnen, würde Tantchen nun vielleicht so vorgehen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void Step(IList<Point> activePoints)
{
// Für jeden möglichen Punkt in der die Anzahl der Nachbarn bestimmen;
// Den Punkt der 'activePoints'-Liste hinzufügen oder entfernen
}
// Alternativ
class World
{
public IList<Point> LivingCells {get; set;}
public void Step()
{
// Vorgehen wie oben
}
}

Nach Abschluss der Berechnungen hätte sie den neuen Zustand der Welt, aber jedoch auch ‘nur’ diesen. Der Zustand vorher ist ohne eine vorherige manuelle Kopie unwiederbringlich verloren. Und wer macht schon gerne Kopien von Objekten? Würde sie nun gerne feststellen können, ob eine Welt nur noch aus statischen oder oszillierenden Objekten besteht, müßte sie in der Lage sein, die Zustände der Welt über die Zeit hinweg zu vergleichen. Was schwerfällt, wenn man seinem “Gedächtnis” bei jedem Durchgang die Erinnerung an zuvor beraubt.

In der funktionalen Entwicklung würde man eine Funktion definieren, der man die aktuelle Spielewelt übergibt und die die Welt zum nächsten Zeitpunkt berechnet und zurückgibt. Das versetzt den Entwickler in die Lage den neuen Zustand der Welt zu berechnen, ohne dabei den/die vorherigen zu verlieren. Ein Ansatz, der zusätzlich den Code viel leichter zu testen macht.

Vergesslichkeit

Da Tantchen sich prinzipiell lieber Speicheradressen als Werte merkt, vergisst sie gerne mal was. Besser gesagt sie überschreibt sich gerne mal was selbst. Gerade wenn sie Dinge in mehreren Threads ausführt. Sie muss sich ständig aufwändig mit Locks und diversen anderen Mechanismen behelfen, um nicht völlig die Kontrolle zu verlieren. Es ist teilweise unmöglich nachzuvollziehen, wer während der Laufzeit des Programms noch Referenzen auf kritische Objekte vorhält. Berechnet sie bspw. Conways Game Of Life in einem und die Anzeige in der UI in einem anderen Thread, so tut sie gut daran Zugriffe auf gemeinsam genutzte Daten zu synchronisieren. Ein irrtümlich gerenderter Punkt ist in unserem Beispiel sicher kein Problem, nur in der Medizinbranche oder im Bankenwesen sollte man sich schon auf angezeigte Daten verlassen können.

Onkel hingegen konzentriert sich auf Funktionen im mathematischen Sinn. Ihr Rückgabewert hängt ausschließlich von den Werten der Eingabeparameter ab. Es werden keine Änderungen an Referenzen vorgenommen, die an anderer Stelle zu Problemen führen können. Eine Funktion im mathematischen Sinne läßt sich einfach testen und sicher in mehreren Threads ausführen. Clojure zum Beispiel verfügt hier über ein tolles Modell. Die Sprache trennt die “Wahrnehmung” von Werten von deren Berechnung. Derefernziert man einen synchronisierten Datentyp, so erhält man immer den zuletzt gültigen Wert, ohne Locks setzen zu müssen. Das beschleunigt nicht nur den Programmablauf, da keine Locks benötigt werden, es stellt auch sicher, dass man valide Werte erhält, die sich nicht zu einem späteren Zeitpunkt plötzlich ändern.

TL;DR

Die Objektorientierung macht es einem nicht leicht:

  • Werte zu vergleichen,
  • ohne Weiteres zeitliche Änderungen im Programmablauf darzustellen,
  • parallele und konkurrierende Programme zu schreiben.

Die funktionale Programmierung:

  • Behebt nicht nur diese Probleme,
  • sie verbessert zudem noch deutlich die Testbarkeit

Gedanken zur Wirtschaftlichkeit

Einen großen Anteil an den Kosten individuell entwickelter Software haben die Personalkosten. Auf der anderen Seite macht es einem die Objektorientierung stellenweise sehr schwer alltäglichste Dinge zu tun. Ein Beispiel dazu ist der Vergleich zweier Punkte von oben. Während der OO-Entwickler noch Methoden schreibt, um zu bestimmen, ob zwei Punkte gleich sind, ist ein Entwickler in einer funktionalen Programmiersprache schon dabei, sich Gedanken darüber zu machen, welchen Algorithmus er verwendet um Muster zu erkennen. Mir persönlich wirft sich da der Gedanke auf, ob man es sich eigentlich noch leisten kann objektorientiert zu entwickeln?

Zukünftig

In Zukunft werden Entwickler zunehmend gezwungen sein, immer mehr von Multiprozessor-Architekturen Gebrauch zu machen. java.util.concurrent aus Java Version 6 und die Task-Parallel-Library zeigen zwar, dass die Hersteller verstanden haben, dass sie den Entwickler in Zukunft an dieser Stelle besser unterstützen müssen, können jedoch nicht darüber hinwegtäuschen, dass den “Mainstream-Sprachen” elementare Dinge in Sachen Multithreading fehlen. Vielleicht ist jetzt ein guter Zeitpunkt gekommen, an dem man sich, anstatt aufwändig um Schwächen in objektorientierten Sprachen herum zu programmieren, lieber eingesteht, dass für viele Zwecke OO grundsätzlich das falsche Paradigma ist.

Warum also nicht beim nächsten Mal Clojure oder F#?