Objektorientierte Programmierung

Was ist das?

Nun begeben wir uns an ein ausgesprochen fortgeschrittenes Thema, das es schon seit einigen Jahren gibt. Heutzutage ist die 'Objektorientierte Programmierung' zur Norm geworden. Sprachen wie Java und Python beinhalten dieses Konzept so stark, dass du eigentlich recht wenig tun kannst, um nicht irgendwie mit Objekten in Berührung zu kommen. Um was geht es denn dabei?

Die besten Einführungen sind , meiner Meinung nach:

Dies ist eine Steigerung in Tiefe, Umfang und akademischer Exaktheit, wenn du diese Liste abwärts gehst. Für die meisten nicht-professionellen Belange ist das erste Werk ausgelegt. Für eine mehr aufs Programmieren zugeschnittene Einführung schau dir mal Object Oriented Programming von Timothy Budd (zweite Ausgabe) an. Dies habe ich nicht persönlich gelesen, aber es gibt davon sehr gute Beurteilungen von Leuten, deren Meinung ich beachte. Schliesslich gibt es noch eine ganze Menge an Info über alle OO-Themen auf der Web-Link-Seite: http://www.cetus.org

Da ich annehme, dass du weder die Zeit noch das Bedürfnis hast, diese Bücher und Links sofort zu studieren, werde ich hier einen kurzen Überblick über das Konzept geben. (Anmerkung: Einige Leute finden OO schwer zu begreifen, andere kriegen es 'gut auf die Reihe'. Aber keine Sorge, wenn du zu der vorletzten Kategorie zählst, du kannst trotzdem Objekte verwenden ohne wirklich dahinter zu sehen.)

Ein letzter Punkt noch, wir werden hier in diesem Teil ausschließlich Python verwenden, da weder BASIC noch Tcl die Verwendung von Objekten unterstützen. Es ist durchaus möglich ein objektorientiertes Design in eine Nicht-OO-Sprache mit Hilfe von Kodiervereinbarungen zu implementieren, aber dies ist eher eine Möglichkeit, die zu allerletzt in Frage kommt, als eine empfohlene Strategie. Wenn dein Problem gut mit OO-Techniken zusammenpasst, ist es am Besten, auch eine OO-Sprache zu verwenden.

Daten und Funktionen - zusammengenommen

Objekte sind Sammlungen von Daten und Funktionen, die mit diesen Daten operieren. Sie sind so aneinander gebunden, dass du ein Objekt aus einem Teil deines Programmes übertragen kannst und sie dennoch nicht nur automatisch den Zugriff auf die Attribute der Daten haben, sondern auch ihre Operationen zugänglich sind.

Zum Beispiel wird ein String-Objekt eine Zeichenkette beinhalten, aber auch die Methoden, die mit diesem String operieren - Suchen, Gross- oder Kleinschrift-Umwandlung, Längenberechnung usw.

Objekte benutzen einen 'Botschaftsübermittlungs-Ausdruck', wobei ein Objekt eine Botschaft zu einem anderen Objekt passieren lässt und das Zielobjekt seinerseits antwortet durch die Ausführung von einem seiner Operationen, einer Methode. Solch eine Methode wird beim Erhalten der entsprechende Botschaft aufgerufen, die dem Objekt angehört. Es gibt verschiedene Schreibweisen, dies darzustellen. Die allgemein übliche ahmt den Feldzugriff in Records nach - eine Periode. Dies erfolgt hier nun für eine fiktive 'Bildchen-Klasse':

b = bildchen() # erzeugt neue Instanz, b, von bildchen
b.malen()  # sendet die Botschaft 'malen' zu ihr

Dies bewirkt das Aufrufen der Malen-Methode des Bildchen-Objektes.

Definieren von Klassen

So wie Daten unterschiedlichen Typen angehören können, so können auch Objekte von einem unterschiedlichen Typ sein. Solche Ansammlungen von Objekten mit identischer Charakteristik sind allgemein üblich als Klassen bekannt. Wir können Klassen definieren und Instanzen von diesen erzeugen, die die aktuellen Objekte sind. Wir können zu diesen Objekten Referenzen in Variablen innerhalb unserer Programme speichern.

Lass uns ein konkretes Beispiel betrachten, um zu sehen, ob wir dies damit besser erklären können. Wir werden eine Meldungs-Klasse erzeugen, die einen string enthält - die Text-Meldung - und eine Methode, die Meldung auszugeben.

class Meldung:
    def __init__(self, einString):
        self.text = einString
    def druckEs(self):
        print self.text

Anmerkung 1: Eine der Methoden dieser Klasse wird __init__ genannt und ist eine besondere Methode, die auch Konstruktor genannt wird. Die Ursache für diesen Namen ist der, dass er aufgerufen wird, wenn eine neue Objektinstanz erzeugt oder konstruiert wird. Jede innerhalb dieser Methode zugewiesene ( und damit in Python erzeugte ) Variable wird einzigartig in der neuen Instanz sein. Es gibt eine Vielzahl an speziellen Methoden wie dieser in Python; sie sind fast alle durch das Namensformat __xxx__ gekennzeichnet.

Anmerkung 2: Beide definierte Methoden besitzen self als ersten Parameter. Der Name ist reine Konvention, aber er zeigt die Objekt-Instanz an. Wie wir sehen werden, wird dieser Parameter erst zur Laufzeit vom Interpreter gefüllt und nicht durch den Programmierer. Dieses Print wird ohne Argumente aufgerufen: m.druckEs().

Anmerkung 3: Wir nennen die Klasse Meldung mit dem Großbuchstaben 'M'. Dies ist reine Konvention, aber diese Schreibweise ist weit verbreitet, nicht nur in Python sondern auch in anderen OO-Sprachen. Eine verwandte Konvention besagt, dass Methodenamen mit Kleinbuchstaben beginnen sollen und angehängte Wörter mit Großbuchstaben. So würde eine Methode "berechne momentanes Gewicht" geschrieben werden als: berechneMomentanesGewicht.

Du solltest dir noch einmal das 'Daten' -Kapitel besuchen und dir noch einmal die'benutzerdefinierten Typen' anschauen. Das Python-Adressenbeispiel sollte jetzt ein wenig klarer sein. Eigentlich ist der einzige benutzerdefinierte Type in Python eine Klasse. Eine Klasse mit Attributen, aber ohne Methoden (ausgenommen __init__ , das effektiv gleichwertig zu einem Record in BASIC ist).

Verwendung von Klassen

Nachdem wir eine Klasse definiert haben, können wir nun Instanzen von unserer Meldungsklasse erzeugen und diese manipulieren:

m1 = Meldung("Hallo Welt")
m2 = Meldung("Tschüss, es war kurz aber süss")
notiz = [m1, m2] # bringt die Objekte in eine Liste
for msg in notiz:
    msg.druckEs() # druckt jede Meldung sofort aus

Letztendlich hast du die Klasse nun so behandelt, als wäre es ein Standard-Datentyp von Python, und diese Erkenntnis war der eigentliche Zweck der Übung!

Gleiche Sache, andere Sache

Jetzt haben wir also die Möglichkeit, unsere eigenen Typen (Klassen) und deren Instanzen zu definieren und diesen Variablen zu zuorden. Wir können Meldungen an diese Objekte übergeben, die die Methoden auslösen, die wir definiert haben. Aber es gibt noch ein letztes Element des OO-Stoffs, und ist auf vielerlei Arten der wichtigste Aspekt von allen.

Wenn wir zwei Objekte unterschiedlicher Klassen haben, die aber den gleichen Satz an Mitteilungen unterstützen, jedoch mit ihren eigenen zugehörigen Methoden, dann können wir diese Objekte zusammen nehmen und sie identisch in unserem Programm behandeln, aber die Objekte werden sich unterschiedlich verhalten. Diese Eigenschaft, sich auf die gleiche Nachricht unterschiedlich zu verhalten, ist als Polymorphismus bekannt.

Typischerweise könnte das dazu verwendet werden, eine Anzahl unterschiedlicher Grafik-Objekte sich selbst nach Erhalt einer 'malen'- Meldung zeichnen zu lassen. Ein Kreis wird ganz unterschiedlich zu einem Dreieck gezeichnet, aber beide sind mit der gleichen Malmethode ausgestattet. Als Programmierer kannst du nun einfach den Unterschied ignorieren und sie einfach als 'Formen' betrachten.

Betrachten wir ein Beispiel, indem wir anstelle des Formenzeichnens ihre Flächen berechnen:

Zuerst erzeugen wir Quadrat- und Kreis-Klassen:

class Quadrat:
    def __init__(self, seite):
        self.seite = seite
    def berechneFlaeche(self):
        return self.seite**2

class Kreis:
    def __init__(self, radius):
        self.radius = radius
    def berechneFlaeche(self):
        import math
        return math.pi*(self.radius**2)

Jetzt können wir eine Liste mit Formen (entweder Kreise oder Quadrate) erstellen und dann ihre Flächen ausgeben:

liste = [Kreis(5),Kreis(7),Quadrat(9),Kreis(3),Quadrat(12)]

for form in liste:
    print "Die Fläche ist: ", form.berechneFlaeche()

Wenn wir jetzt diese Erkenntnisse mit Modulen verknüpfen, so erhalten wir einen äusserst leistungsfähigen Mechanismus zur Wiederverwendung von Code. Bringe die Klassendefinitionen in ein Modul - sagen wir einmal 'formen.py' und importieren wir dann einfach dieses Modul, wenn wir mit Formen arbeiten wollen. Dies ist genau das, was mit den Python Standardmodulen gemacht wurde; desshalb sieht das Zugreifen auf Objektmethoden fast genau so aus, wie das Verwenden von Funktionen in einem Modul.

Vererbung

Vererbung ist ein oft verwendeter Mechanismus, um Polymorphismus zu implementieren. In der Tat ist in vielen OO-Sprachen dies der einzige Implementierungsweg für Polymorhismus. Das geht wie folgt:

Eine Klasse kann beides vererben, Attribute und Operationen von einer Eltern- oder Super - Klasse. Das heißt, dass eine neue Klasse, die identisch zu einer anderen Klasse ist, in den den meisten Gesichtspunkten nicht notwendigerweise alle Methoden der existierenden Klasse reimplementieren muss, hingegen kann es diese Fähigkeiten erben und dann diejenigen aufheben, die sie anders verwenden möchte (wie die Malmethode im obigen Fall).

Am Besten sollte wieder ein Beispiel dies illustrieren. Wir werden eine Klassenhierarchie von Bankkonten anlegen, wo wir Geld hinterlegen, die Bilanz erhalten und Abbuchungen durchführen können. Einige dieser Konten erwirtschaften Zinsen ( da sie nach unserem Wunsch die Bilanz nach jeder einzelnen Buchung berechnen - eine interessante Innovation in der Welt der Banken!) und andere verbuchen Gebühren bei Auszahlungen.

Die BankKonto-Klasse

Schauen wir uns an, wie das aussehen würde. Zuerst vereinbaren wir alle Eigenschaften und Operationen eines Bankkontos auf der allgemeinsten (oder abstrakten) Ebene.

Gewöhnlicherweise ist es am Besten, die Operationen festzulegen und dann die Eigenschaften, die benötigt werden, um diese Operationen zu unterstützen .

So können wir bei einem Bankkonto:

Zur Unterstützung dieser Operationen werden wir eine Kontonummer (für die Transfer-Operation) und den aktuellen Saldo benötigen.

Wir können eine Klasse erzeugen, die das unterstützt:

SaldoError = "Entschuldigung, Sie haben nur $%6.2h auf Ihrem Konto"

class BankKonto:
    def __init__(self, initialBetrag):
       self.saldo = initialBetrag
       print "Konto erzeugt mit Saldo von %5.2h" % self.saldo

    def einzahlung(self, betrag):
       self.saldo = self.saldo + betrag

    def auszahlung(self, betrag):
       if self.saldo >= betrag:
          self.saldo = self.saldo - betrag
       else:
          raise SaldoError % self.saldo

    def checkSaldo(self):
       return self.saldo
       
    def transfer(self, betrag, konto):
       try: 
          self.auszahlung(betrag)
          konto.einzahlung(betrag)
       except SaldoError:
          print SaldoError

Anmerkung 1: Wir überprüfen den Saldo vor einer Abbuchung und auch den Gebrauch von Ausnahmen zur Fehlerbehandlung. Leider gibt es keinen Fehler des Typs SaldoError und wir müssen deshalb einen erzeugen - es ist eine einfache String-Variable!

Note 2: Die transfer - Methode verwendet die BankKonto - Einzahlung/Auszahlungs - Member-Funktionen oder Methoden um den Transfer durchzuführen. Dies ist sehr üblich in OO und ist bekannt als "self messaging". Es bedeutet, dass abgeleitete Klassen ihre eigenen Versionen von Einzahlung/Auszahlung implementieren können, aber die transfer - Methode die gleiche für alle Konto-Typen bleiben kann.

Die KontoZins-Klasse

Jetzt werden wir die Vererbung verwenden, um ein Konto so auszustatten, dass es Zinsen bei jeder Einzahlung addiert ( nehmen wir 3% an). Das wird identisch sein mit der Standard-Bankkonto-Klasse, ausser bei der Einzahlungs-Methode. Also übernehmen wir diese ganz einfach:

class KontoZins(BankKonto):
   def einzahlung(self, betrag):
       BankKonto.einzahlung(self,betrag)
       self.saldo = self.saldo * 1.03
       

Und das wars. Wir fangen an die Leistungsfähigkeit der OOP zu erkennen, alle anderen Methoden wurden von BankKonto (durch Einsetzen von BankKonto innerhalb zweier Klammern hinter dem neuen Klassennamen) vererbt. Beachte auch, dass Einzahlung die Superklassen-Einzahlungsmethode aufruft, anstatt dass der Code kopiert wird. Wenn wir jetzt die BankKonto - Einzahlung so modifizieren, dass sie eine Art von Fehlerüberprüfung beinhaltet, wird die Sub-Klasse diese Änderungen automatisch unterstützen.

Die KontoKosten-Klasse

Dieses Konto ist wieder identisch mit einer Standard-BankKonto-Klasse, ausser dass diesmal eine Gebühr von 5$ für jede Auszahlung erhoben wird. Wie bei KontoZins können wir eine Klasse erzeugen, die von BankKonto vererbt wird und bei der dann die Auszahlungsmethode verändert wird.

class KontoKosten(BankKonto):
    def __init__(self, initialBetrag):
        BankKonto.__init__(self, initialBetrag)
        self.gebuehr = 3
        
    def auszahlung(self, betrag):
        BankKonto.auszahlung(self, betrag+self.gebuehr)

Anmerkung 1: Wir speichern die Gebühr als eine Instanz-Variable, so dass wir sie später ändern können, wenn dies notwwendig wird. Beachte, dass wir das geerbte __init__ genau wie eine andere Methode aufrufen können.

Anmerkung 2: Wir addieren einfach die Gebühr zur angeforderten Auszahlung dazu und rufen BankKonto auf, um die richtige Arbeit zu tun.

Note 3: Wir führen hiermit einen Seiteneffekt ein, bei dem eine Gebühr auch automatisch bei einem Transfer erhoben wird, aber da wir das wahrscheinlich auch so möchten, ist das dann auch OK.

Testen unseres Systems

Um zu überprüfen, ob alles funktioniert, versuche die Ausführung des folgenden Code-Stückes (entweder durch Eingabe in das Python-Prompt oder durch Erzeugen einer separaten Test-Datei).

from BankKonto import *

# First a standard BankKonto
a = BankKonto(500)
b = BankKonto(200)
a.auszahlung(100)
# a.auszahlung(1000)
a.transfer(100,b)
print "A = ", a.checkSaldo()
print "B = ", b.checkSaldo()


# Jetzt ein KontoZins
c = KontoZins(1000)
c.einzahlung(100)
print "C = ", c.checkSaldo()


# Dann KontoKosten
d = KontoKosten(300)
d.einzahlung(200)
print "D = ", d.checkSaldo()
d.auszahlung(50)
print "D = ", d.checkSaldo()
d.transfer(100,a)
print "A = ", a.checkSaldo()
print "D = ", d.checkSaldo()


# Zuletzt ein Transfer der Kontokosten zu den Zinsen.
# Die Unkosten werden abgezogen und der Zins zum
# Zins addiert
print "C = ", c.checkSaldo()
print "D = ", d.checkSaldo()
d.transfer(20,c)
print "C = ", c.checkSaldo()
print "D = ", d.checkSaldo()

Nun kommentiere die Zeile a.auszahlung(1000) weg, um die Ausnahme bei der Arbeit zu sehen.

Das ist alles. Ein wahrscheinlich einfaches Beispiel, aber es zeigt wie die Vererbung eingesetzt werden kann, um schnell einen einfachen Programmrahmen sehr schnell mit leistungsstarken Eigenschaften auszustatten.

Wir haben gesehen, wie wir das Beispiel in Stufen aufbauen können und wie wir ein Testprogramm anhängen können, um zu überprüfen, ob es geht. Unsere Tests waren nicht vollständig in der Weise, dass wir jede Möglichkeit abgedeckt haben und da gibt es noch mehr Checks, die wir einbauen könnten - was wäre zu tun, wenn ein Konto mit einem negativen Betrag erzeugt wird...

Aber hoffentlich hat dir dies einen Geschmack von der Objektorientierten Programmierung gegeben und du kannst mit einigen anderen Online-Lehrgängen weitermachen, oder eines der am Anfang erwähnten Bücher lesen, um weitere Informatinern und Beispiele zu erhalten.


Zurück  Weiter  Inhalt


Im Falle von Fragen oder Hinweisen zu dieser Seite sende Bitte eine Nachricht

in Englisch an: alan.gauld@yahoo.co.uk oder in Deutsch an bup.schaefer@freenet.de