Objekte#

Objektorientierte Programmierung (OOP) ist ein Programmierparadigma das annimmt, dass ein Programm ausschließlich aus Objekten besteht, die miteinander kooperativ interagieren. Jedes Objekt verfügt über Attribute (Eigenschaften) und Methoden. Die Attribute definieren dabei über ihre Werte den Zustand eines Objektes, die Methoden die möglichen Zustandsänderungen (Handlungen) eines Objektes.

Die Objektorientierung löst dabei einige Problem im Umgang mit sich oft wiederholenden Datenstrukturen in großen Programmen.

Herausforderungen mit sich stark wiederholenden Datenstrukturen#

Das syntaktische Problem#

Objekte werden verwendet, um festzulegen wie sich stark wiederholende Datenstrukturen gespeichert werden. Hierbei geht es darum, dass der Syntax der Datenstruktur eindeutig ist.

Wächst das Programm an, so wächst auch die Menge der Variablen und Datenstrukturen

  • zur Speicherung von Daten

  • zur Kontrolle des Programmflusses

  • zum Abspeichern von Zuständen

  • zum Verarbeiten von Ein- und Ausgaben

Dabei basieren die zugrundeliegenden Elemente meist auf sich wiederholenden Datenstrukturen. Analysiert man z.B. Baupläne oder Karten so verwaltet man viele Punkt-Koordinaten. Hierbei kann man allerdings eine Koordinate unterschiedlich z.B. als Tupel oder als Liste ausdrücken.

point_1 = (54.083336, 12.108811)
point_2 = [12.094167, 54.075211]

Will man diese Punkte verarbeiten so ergeben sich ggf. Probleme da beide unterschiedliche Datentypen besitzen. Das ist das syntaktische Problem im Umgang mit Variablen in großen Programmen. Hier will man den Syntax der Datenstruktur definieren können.

Das semantische Problem#

Objekte werden auch verwendet, um die Semantik von Werten einer Datenstrukturen eindeutig zu definieren.

Haben wir uns z.B. darauf geeinigt, dass wir ein Punkt syntaktisch durch ein Tupel repräsentieren, so ist die Bedeutung der Werte dennoch nicht bekannt.

point_1 = (54.083336, 12.108811)
point_2 = (12.094167, 54.075211)

Ein anderer Programmierer wird hier aber ggf. nicht die semantische Bedeutung verstehen. In diesem Beispiel kann man vermuten, dass der erste Wert die x-Koordinate ist und der zweite die y-Koordinate. Vieleicht ist es aber auch andersherum. Ggf. handelt es sich aber nicht um ein kartesisches Koordinatensystem, sondern um ein radiales. Um solche Unklarheiten zu beseitigen, will man der Datenstruktur eine klare semantische Definition geben, wo eindeutig ist, was die Werte bedeuten.

Das Verhaltensproblem#

Objekte werden zudem verwendet, um die Funktionen zur Verarbeitung der Datenstrukturen direkt mit dieser zu bündeln, so dass die Datenstruktur nur jene Funktionen anbietet, welche auch sinnvoll anwendbar sind.

Definieren wir z.B. eine Funktion, um die Distanz zweier Punkte zu berechnen:

import math

def distance(a, b):
    return math.sqrt((a[0]-b[0])**2 + (a[1]-b[1])**2)

Diese Funktion kann man aufgrund der dynamischen Typisierung in Python ja auch auf andere Datenstrukturen anwenden, z.B. auf eine Line. Das führt zu semantischen oder logischen Fehlern. Um das zu vermeiden, müsste man für jede Variante eine andere Funktion definieren, diese zuordbar benennen und viele Datentyp-Tests vornehmen. Einfacher ist es, sicher zu stellen, dass eine solche Funktion nur für die entsprechende Datenstruktur verfügbar ist.

Objektorientierung#

Klassen deklarieren#

Anstatt unübersichtlich viele verstreute Datenstrukturen und Funktionen zu benutzen, gruppieren wir diese in Objekte. Da wir die Datenstrukturen normieren wollen, muss wir die Struktur dieser Objekte vor Verwendung definiert werden. Dies geschieht über Klassen, welche eine Art Bauplan für die damit normierten Objekte darstellt. Eine Klasse definiert:

  • welche Attribute (Eigenschaften) ein Objekt dieser Klasse besitzt

  • und welche Methoden (Funktionen) ein Objekt der Klasse bereitstellt

Der erste Schritt zu einem Objekt ist das Definieren einer neuen Klasse für den Typ des Objektes. Dies geschieht über den class-Kennwort auf welches der Klassenname folgt. Danach wird wie die Klassendefinition eingerückt. Diese legt fest welche Attribute und Methoden die Klasse besitzt.

class Klassenname:
    # Klassendefinition
    pass

Konstruktor#

Eine der wichtigsten Methoden einer Klasse ist der Konstruktor __init__(). Das ist eine spezielle Methode, die festlegt, wie eine neue Instanz der Klasse erzeugt werden kann. Er wird genutzt, um Attribute initial zuzuweisen als auch Initialisierungsschritte (Tests, Berechnungen, Konfiguration, etc.) durchzuführen.

Jede Klasse muss genau einen Konstruktor haben. Wird dieser nicht definiert, so wird ein leerer Konstruktor von Python erzeugt, der nichts macht, wie das folgende Beispiel.

class Klassenname:
    # Leerer Konstruktor
    def __init__(self):
        pass

Instanzattribute#

Der Konstruktor wird als Funktion __init__(self) mit dem Parameter self definiert. self ist dabei eine Selbst-Referenz auf die neue Instanz der Klasse. Sie dient dazu, dass man Instanzattribute direkt beim Erzeugen der Instanz zuweisen kann. Instanzattribute sind Attribute, die in jeder Instanz unterschiedlich sein können, also wenn es individuelle Werte der Eigenschaft gibt.

Definieren wir z.B. die Klasse eines Punktes mit x- und y-Koordinaten. Da jede Instanz diese beiden Koordinaten haben muss und sie auch für jede Instanz unterschiedlich sein können, weisen wir sie bereits im Konstruktor als Instanzattribute zu. Damit ist festgelegt, dass jede Instanz der Klasse diese Attribute hat.

class Point:
    # Konstruktor
    def __init__(self, x, y):
        self.x = x
        self.y = y

Die Zuweisung erfolgt hierbei über den Punkt-Syntax bei dem ein Punkt die Instanzvariable (self) von dem Attributnamen x trennt. self.x ist somit eine Referenz auf das Attribut x der Instanz self. Die Zuweisung self.x = x bedeutet, dass wir dem neuen Instanzattribut x der Instanz self den Wert der Variablen x zuweise. Obwohl beide gleich heißen, sind sie nicht die gleiche Variable, weil self.x ja ein Attribut der Instanz ist und x ein Parameter der Funktion ist und nur in dieser gültig ist.

Da __init__() eine Funktion ist, wenn auch besonders, kann man auch Defaults definieren. Wollen wir z.B. definieren wir, dass x und y mit 0 initialisiert werden, wenn sie nicht angegeben werden, so können wir auch dies als Defaultwerte deklarieren

class Point:
    # Konstruktor
    def __init__(self, x = 0.0, y = 0.0):
        self.x = x
        self.y = y

Klassenattribute#

Neben Instanzattributen gibt es auch Klassenattribute. Dies sind Attribute, welche für alle Instanzen einer Klasse den gleichen Wert haben sollen.

class Point:
    # Attribut aller Instanzen
    unit = "m"

Warning

Klassenattribute gelten für alle Instanzen. Insbesondere bei zusammengesetzten Datentypen bedeutet das, wenn eine Instanz den Wert ändert, so ändert er sich in allen anderen Instanzen auch.

Methoden#

Klassen definieren häufig auch eigene Methoden, also Funktionen die speziell nur auf Instanzen dieser Klasse angewendet werden sollen und nicht auf andere Objekte.

Methoden werden als Funktionen innerhalb der Einrückung der Klassendefinition deklariert. Diese Methoden sind dann in allen Instanzen verfügbar. Methoden besitzen immer self als ersten Parameter. Auch hier ist das eine Referenz auf die aktuelle Instanz. Dadurch kann man dann auf die Attribute oder andere Methoden zugreifen.

So können wir zum Beispiel die am Anfang definierte distance-Funktion als Methode definieren, dass sie nun die Distanz zweier Punkte self und other berechnen.

class Point:
    # Attribut aller Instanzen
    unit = "m"
    
    # Konstruktor
    def __init__(self, x, y):
        self.x = x
        self.y = y

    # Eigene Methode
    def distance(self, other):
        return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)

Klasseninstanzen#

Objekte selbst sind immer Instanzen einer Klasse (die Klasse ist ja nur ein Bauplan). Eine Klasse kann beliebig viele Instanzen haben oder gar keine. Alle Instanzen sind gleich aufgebaut, besitzen aber nicht unbedingt die gleiche Werte in den Attributen.

Mit der neuen Klasse Punkt können wir nun die Punkte am Anfang jetzt syntaktisch, semantisch und im Verhalten eindeutig definieren. Dabei erzeugen wir Instanzen der Klasse nicht direkt mit dem Konstruktor sondern indem wir den Klassennahmen wie eine Funktion aufrufen, mit den Parametern des Konstruktors. Dabei wird der self-Parameter weggelassen (er wird von Python zugewiesen).

Dabei können wir die Parameter auch bennenen und so semantische Unklarheiten umgehen.

point_1 = Point(x=54.083336, y=12.108811)
point_2 = Point(y=12.094167, x=54.075211) # bei bennanten Parametern können wir die Reihenfolge verändern

Auch die Werte des Objektes sind nun semantisch klar definiert. Wir können auf sie mit dem Punkt-Syntax zugreifen, wobei der Variablenname des Objektes links steht und der Attributname rechts. Um auf das Attribut x zuzugreifen schreiben wir

point_1.x
54.083336

Das funktioniert auch für die Klassenattribute. In voller Schönheit können wir dann z.B. schreiben

print(f"Der Punkt liegt bei x: {point_1.x} {point_1.unit}; y: {point_1.y} {point_1.unit}")
Der Punkt liegt bei x: 54.083336 m; y: 12.108811 m

In gleicher Weise können wir den Attributen neue Werte zuweisen.

point_1.x = 54.08
point_1.y = 12.11
print(f"Der Punkt liegt bei x: {point_1.x} {point_1.unit}; y: {point_1.y} {point_1.unit}")
Der Punkt liegt bei x: 54.08 m; y: 12.11 m

Mit dem Punkt-Syntax können wir auch die Methoden aufrufen. Wollen wir die Distanz von point_1 und point_2 berechnen, so schreiben wir

point_1.distance(point_2)
0.016541414993884864

Referenzen#

In Programmen stehen Objekte meist im Zusammenhang. Z.B. besteht eine Linie aus zwei Punkten. Diesen Zusammenhang zweier Objekt-Klassen bezeichnet man als Referenz. Diesen Zusammenhang zweier Objekt-Klassen bezeichnet man als Referenz.

Um Referenzen in Python zu erstellen, erstellt man einfach ein Attribut vom Datentyp des anderen Objektes. So kann eine Linie als Verbindung zweier Punkte definiert werden mit der Länge berechnet durch die zuvor definierte Methode distance.

class Line:
    def __init__(self, start: Point, end: Point):
        self.start = start
        self.end = end

    def length(self):
        return self.start.distance(self.end)
linie_1 = Line(start=point_1, end=point_2)
linie_1.length()
0.016541414993884864

Kapzelung von Attributen und Methoden#

Häufig möchte man in der Programmierung Kontrolle darüber haben ob und wie Attribute verändert werden und wer welche Methoden aufrufen darf.

Hierfür erlaubt Python die Unterscheidung in private, protected und public Attribute und Methoden. Wenn nicht anders festgelegt sind alle Attribute und Methoden public. Ein public Attribut oder Methode ist außerhalb der Klasse für alle sichtbar, lesbar und veränderbar. Das haben wir oben genutzt um oben point_1.x auszulesen und mit point_1.x = 54.08 einen neuen Wert zuzuweisen. Damit diese Attribute oder Methoden nicht mehr zugreifbar sind müssen wir sie als private oder protected deklarieren, womit sie nur innerhalb der Klasse (private) oder Unterklasse (protected) über self zugreifbar sind. Um ein Attribut oder Methode als privat zu deklarieren fügen wir dem Namen ein __ am Anfang hinzu oder ein _ für protected.

Zum Beispiel wollen wir sicherstellen, dass die Position des Objektes Point nach Erstellung nicht mehr veränderbar ist. Ein solches unveränderliches Objekt nennt man immutable. Ein Grund dafür kann sein, dass eine Änderung Inkonsistenzen und potenzielle Fehler erzeugen würden. Wollen wir z.B. ein Rechteck aus den Punkten erzeugen, so müssen wir in der Init-Funktion prüfen, ob alle Linien rechtwinklig sind. Wären die Punkte verschiebbar, so müssten wir bei einer Änderung entweder neu prüfen oder wir verbieten die Veränderung einfach.

Um sicher zu stellen, dass niemand mehr die Koordinaten ändern kann, müssen wir x und y als privat zu deklarieren, indem wir sie in __x und __y benennen. Jetzt kann aber niemand außerhalb der Instanz die Werte lesen. Um das zu ermöglichen definieren wir zwei public Getter-Methoden get_x() und get_y() die nur den Wert zurück geben. Sollten wir den Wert veränderbar machen, müssten wir auch noch Setter-Methoden definieren, welche die Werte setzen würden.

class ImmutablePoint:
    unit = "m"
    
    def __init__(self, x, y):
        self.__x = x
        self.__y = y

    def distance(self, other):
        return math.sqrt((self.__x - other.__x)**2 + (self.__y - other.__y)**2)

    def get_x(self):
        return self.__x

    def get_y(self):
        return self.__y

Definieren wir nun wieder unsere zwei Instanzen

point_1 = ImmutablePoint(54.083336, 12.108811)

und versuchen den Wert __x oder __y zu lesen, so erhalten wir nun eine Fehlermeldung.

print(f"Der Punkt liegt bei x: {point_1.__x} {point_1.unit}; y: {point_1.__y} {point_1.unit}")
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[21], line 1
----> 1 print(f"Der Punkt liegt bei x: {point_1.__x} {point_1.unit}; y: {point_1.__y} {point_1.unit}")

AttributeError: 'ImmutablePoint' object has no attribute '__x'

Zum Auslesen der Werte müssen wir stattdessen die Getter-Methode benutzen.

print(f"Der Punkt liegt bei x: {point_1.get_x()} {point_1.unit}; y: {point_1.get_y()} {point_1.unit}")
Der Punkt liegt bei x: 54.083336 m; y: 12.108811 m

Wir können die Werte auch nicht verändern. Wir können es zwar versuchen:

point_1.__x = 54.08
point_1.__y = 12.11

aber es hat keine Auswirkung.

print(f"Der Punkt liegt bei x: {point_1.get_x()} {point_1.unit}; y: {point_1.get_y()} {point_1.unit}")
Der Punkt liegt bei x: 54.083336 m; y: 12.108811 m

Vererbung#

Vererbung ist eine der wichtigsten Eigenschaften von Objektorientierung. Ziel der Vererbung ist es Attribute und Methoden, die in vielen ähnlichen Klassen vorkommen nur einmal definieren zu müssen.

Hierfür unterteilt man die Klassen in Oberklassen (Eltern) welche Attribute und Methoden an Unterklassen (Kinder) vererben. Das heißt jede Unterklasse (Kind) besitzt alle Attribute und Methoden der Oberklasse (Eltern), wir müssen sie also nicht noch einmal definieren. Neue Attribute und Methoden, die für eine Unterklasse definiert werden, werden jedoch nicht an die Eltern oder Geschwister übertragen. Deshalb spricht man bei den Unterklassen von einer Spezialisierung und den Oberklassen von einer Generalisierung, da sie für mehrere Klassen gelten.

Zum Beispiel stellen wir fest, dass verschiedenen geometrischen Objekte wie Dreieck, Viereck, Fünfeck, etc. alle Polygone darstellen, welche sich aus mit Linien verbundene Punkte darstellen. Dreieck, Viereck, Fünfeck sind hierbei Spezialisierungen der Klasse Polygon. Wir definieren also zuerst die generische Klasse Polygon welche als Instanzattribut eine list von mehr als 2 ImmutablePoint akzeptiert und als privates Attribut __points abspeichert. Wir definieren weiterhin ein optionales Attribut name welches public und veränderbar ist.

from typing import List

class Polygon:
    def __init__(self, points: List[Point], name="Polygon"):
        if not isinstance(points, list):
            raise TypeError("points not of type list")
        if not len(points) > 2:
            raise TypeError("points need to contain at least 2 Points")
        for point in points:
            if not isinstance(point, ImmutablePoint):
                raise TypeError("Point not of type ImmutablePoint")
        self.__points = points
        self.name = name

    def get_points(self):
        return self.__points

    # Fläche des Polygons entsprechend der Gaußsche Trapezformel
    def area(self):
        n = len(self.__points) # of corners
        area = 0.0
        for i in range(n):
            j = (i + 1) % n
            area += self.__points[i].get_x() * self.__points[j].get_y()
            area -= self.__points[j].get_x() * self.__points[i].get_y()
        area = abs(area) / 2.0
        return area

    # Überschriebene Standardmethode zur Erzeugung eines Strings
    def __str__(self):
        description = f"{self.name} has an area of {self.area()} and is defined by\n" # \n ist ein Zeilenumbruch
        for i,point in enumerate(self.__points):
            description += f"  Point {i} at x: {point.get_x()} {point.unit}; y: {point.get_y()} {point.unit}\n"
        return description

Hierbei überschreiben wir auch die Standardmethode __str__(self) eines Objektes, welche immer aufgerufen wird, wenn das Objekt in einen String konvertiert werden soll. Die Funktion wird auch von der print()-Anweisung genutzt, weshalb wir diese lesbarer gestalten können.

Wir definieren auch die Funktion area() welches die Fläche des Polygons (konkav und ohne Löcher) mit der Gaußchen Trapezformel berechnet.

Jetzt wollen wir die Untertypen Dreieck, Viereck und Fünfeck definieren. Hierzu geben wir beim Deklarieren der neuen Klasse die Oberklasse in Klammern mit an. Wir können dann auch einen neuen Konstruktor definieren (sonst wird der der Oberklasse genommen). Dabei müssen wir immer den Konstruktor der Oberklasse mit aufrufen, welche wir mit der Funktion super() erhalten. Wir definieren z.B Konstruktoren welche genau die richtige Anzahl an Punkten akzeptiert.

class Triangle(Polygon):
    def __init__(self, p1, p2, p3, name= "Triangle"):
        super().__init__(points=[p1, p2, p3], name=name)

class Tetragon(Polygon):
    def __init__(self, p1, p2, p3, p4, name= "Tetragon"):
        super().__init__(points=[p1, p2, p3, p4], name=name)

class Pentagon(Polygon):
    def __init__(self, p1, p2, p3, p4, p5, name="Pentagon"):
        super().__init__(points=[p1, p2, p3, p4, p5], name=name)

Das entscheidende bei der Vererbung ist nun, dass alle Instanzen von Triangle, Tetragon oder Pentagon die Attribute __points (es ist allerdings nicht sichtbar), name und die Methoden get_points und __str__ haben.

Definieren wir z.B. ein Dreieck und wenden darauf print() an, so wird intern die geerbte Funktion __str__ der Oberklasse Polygon angewendet.

dreieck_1 = Triangle(ImmutablePoint(0,0), ImmutablePoint(1,1),ImmutablePoint(2,0), "Dreieck")
print(dreieck_1)
dreieck_1.area()
Dreieck has an area of 1.0 and is defined by
  Point 0 at x: 0 m; y: 0 m
  Point 1 at x: 1 m; y: 1 m
  Point 2 at x: 2 m; y: 0 m
1.0

Das Gleiche funktioniert auch für Vierecke und Fünfecke:

viereck_1 = Tetragon(ImmutablePoint(0,0), ImmutablePoint(0,1),ImmutablePoint(1,1), ImmutablePoint(1,0), "Viereck")
print(viereck_1)
viereck_1.area()
Viereck has an area of 1.0 and is defined by
  Point 0 at x: 0 m; y: 0 m
  Point 1 at x: 0 m; y: 1 m
  Point 2 at x: 1 m; y: 1 m
  Point 3 at x: 1 m; y: 0 m
1.0
fuenfeck_1 = Pentagon(ImmutablePoint(0,0), ImmutablePoint(-1,1),ImmutablePoint(1,2), ImmutablePoint(2,1), ImmutablePoint(2,0), "Fünfeck")
print(fuenfeck_1)
fuenfeck_1.area()
Fünfeck has an area of 4.0 and is defined by
  Point 0 at x: 0 m; y: 0 m
  Point 1 at x: -1 m; y: 1 m
  Point 2 at x: 1 m; y: 2 m
  Point 3 at x: 2 m; y: 1 m
  Point 4 at x: 2 m; y: 0 m
4.0

Objektorientierter Softwareentwurf mit UML#

Im objektorientierten Softwareentwurf, wird ein Programm so entworfen, dass es nur aus Objekten besteht. Hierbei verwendet man meist die Modellierungssprache UML können Klassen auch visuell entworfen werden. Man modelliert im Entwurf wie:

  • diese Objekte in Form von Klassen definiert sind,

  • welche Attribute und Methoden sie besitzen,

  • wie diese Klassen aufeinander aufbauen (Vererbung),

  • als auch wie sie miteinander in statischer Beziehung stehen (Referenzen)

  • und wie sie dynamisch interagieren (Verhalten)

Klassen, Attribute und Methoden#

Eine Klasse ist hierbei nur ein Viereck mit drei Ebenen. In der ersten Ebene schreibt man den Klassennamen. In der zweiten Ebene listet man alle primitiven Attribute einer Klasse (welche keine Referenzen darstellen). Die dritte Ebene enthält alle Methoden der Klasse. Für den zuvor definierten Point mit den Attributen x und y und der Methode distance() eines anderen Punktes sieht die Klasse in UML so aus:

Referenzen#

Eine sehr wichtige Arbeit mit UML ist die Visualisierung von Abhängigkeiten zwischen Klassen in Form von Referenzen. Man unterscheidet in UML die Referenzen in der Art der Besitzverhältnisse, also ob ein Objekt Teil eines anderen ist und ohne ihn existieren kann (Aggregated) oder nicht ohne diesen existieren kann (Composition) oder komplett unabhängig ist (Assoziation). Hierfür definiert UML unterschiedliche Pfeiltypen. Hierbei kann man an den Linien die Häufigkeit (Multiplizität) der Ursprungs- und Zielklasse angeben.

Die zuvor definierte Klasse Line beinhaltet den start- und end-Punkt. Das stellt in UML eine Aggregation dar, weil die Punkte ja zur Linie gehören, allerdings durchaus auch separat existieren können (wir brauchen ja keine Linie um einen Punkt zu definieren). Die Multiplizität ist hierbei, dass je 1 Instanz einer Line je 2 Instanzen der Klasse Point besitzt.

Vererbung#

Ein wichtiger Aspekt in der Arbeit mit UML ist die Vererbung. Die Vererbung wird in UML durch eine Referenz mit einem gefüllten Dreick “▲” bei der Oberklasse (es ist kein Pfeil) gezeichnet. Um zum Beispiel auszudrücken, dass Triangle, Tetragon und Pentagon Unterklassen des Polygons sind, können wir zeichnen.

Fügen wir die einzelnen Elemente zusammen, so sieht das UML-Diagramm unseres Klassen-Modells wie folgt aus:

Auf Basis dieses UML-Diagramms, würde ein Programmierer die Zusammenhänge der Klassen verstehen und könnte diese entsprechend implementieren. Sie eignen sich besonders gut, um solche Datenstrukturen zu definieren und zu kommunizieren.

Dieses Modell können wir jetzt weiter verfeinern und weitere spezifischer Klassen für Dreiecke einführen wie rechtwinklige (Right), spitzwinklige (Acute) und stumpfwinklige (Obtuse) Dreiecke die unregelmäßig (Scalene), gleichschenklig (Isosceles) oder gleichseitig (Equilateral) sein können. Genauso können wir Vierecke klassifizieren in Trapeze, Parallelogramme und Drachen (Kite) mit den Spezialisierungen Rhombus, Rechteck und Quadrat (Square).

Jede Instanz dieser speziellen Klassen erbt dabei die Funktionen area() und __str__(). Wir haben also mit der einen generischen Implementation von area() zur Flächenberechnung eines Polygons, nur eine einzige Methode implementieren müssen, die die Flächenberechnung aller geometrischen Grundelemente abdeckt, ohne eine weitere Zeile Code für diese schreiben zu müssen.

Entlang der Vererbungshierarchie könnte man jetzt weitere Attribute und Methoden definieren, welche spezifische Berechnungen durchführen können oder z.B. testen ob die Punkte, Linien und Winkel den entsprechenden Bedingungen entsprechen. Dies ist jedoch außerhalb des Rahmens dieses Beispiels.