This post originated from an RSS feed registered with Ruby Buzz
by Michael Neumann.
Original Post: Klassen, Objekte, Attribute und Methoden
Feed Title: Mike's Weblog
Feed URL: http://www.ntecs.de/blog-old/index.rss?cat=ruby&count=7
Feed Description: Blogging about Ruby and other interesting stuff.
In diesem Artikel versuche ich mal die im Titel genannten Begriffe zu
erklären. Diese Begriffe bilden die Grundlage nicht nur der
Objektorientierten Programmierung, sondern finden vielmehr auch Anwendung
in anderen Disziplinen wie etwa der Biologie, und dies schon Jahrhunderte
bevor die Entwicklung der Rechenmaschinen begann (ja, ich spreche von
Computern ;-).
Im folgenden sind die einzelnen Begriffe erklärt:
Klasse (Schema/Schablone)
Eine Klasse beschreibt das Verhalten und die Eigenschaften all seiner
Objekte. Zum Beispiel, dass jedes Haus ein Dach besitzt oder die
Eigenschaft das Häuser in der Regel durch ihre Anzahl von Stockwerken
beschrieben werden können.
Eine Klasse beschreibt dagegen nicht aus wievielen Stockwerken nun
ein bestimmtes Haus besteht (das Nachbarhaus z.B.). Ein ganz bestimmtes
Haus ist nämlich schon eine Ausprägung dieser Klasse "Haus", ein
Objekt also. Man sollte hierbei gut aufpassen nicht die Klasse
"Haus" mit dem Objekt "mein Haus" zu verwechseln.
Klassen sind abstrakte Beschreibungen der Objekte. Genauso gut könnte man
jedes einzelne Objekt aufs neue beschreiben. Da viele Objekte sich jedoch
sehr ähnlich sind ("jedes Haus hat ein Dach"), fasst man ähnliche
Objekte in Klassen zusammen und beschreibt diese nur einmal.
Objekte (Ausprägung)
Objekte hingegen sind, anders als Klassen, nicht abstrakt, sie existieren
wirklich! Objekte einer Klasse besitzen diesselben Verhaltensweisen
(Methoden), jedoch hat jedes Objekt seine eigenen Attributwerte die es von
den anderen Objekten unterscheidet. So ist z.B. jeder Mensch in Deutschland
eindeutig durch die konkreten Werte von Geburtsdatum, Geburtsort usw.
identifizierbar. Dies sind seine Attributwerte. Klassen beschreiben nur,
welche Attribute Objekte besitzen können, nicht jedoch deren aktuelle Werte
(z.B. Berlin als Geburtsort).
Attribute (Zustand)
Objekte derselben Klasse unterscheiden sich oft nur in ihren
Attributwerten. Jedes Objekt hat seine eigenen Attributwerte, die jedoch
mit denen eines anderen Objektes durchaus übereinstimen können. So gibt es
viele Objekte der Klasse Mensch (man sagt auch "vom Typ Mensch"),
die in Berlin (Attributwert des Attributs Geburtsort) geboren sind.
Ein weiteres wichtiges Attribut von Lebewesen ist der sogenannte
"genetische Fingerabdruck", oder kurz, die DNS. Wieder muss
unterschieden werden zwischen der Beschreibung, dass jedes Lebewesen einen
genetischen Fingerabdruck besitzt (das geschieht in der Klasse), und des
konkreten Wertes, der wiederum Bestandteil des zugehörigen Objektes ist.
Methoden (Verhalten)
Methoden beschreiben wie sich Objekte verhalten (können). Dabei wird dieses
Verhalten für alle Objekte einer Klasse beschrieben, ist jedoch abhängig
von den Attributwerten des konkreten Objektes.
Methoden beschreiben meist gewisse Aktionen. So hat zum Beispiel die Aktion
geh_nach_hause eines Objektes der Klasse Mensch zur Folge, das
dieser nach Hause geht. Logisch ist, das dieses Verhalten sehr stark von
dem Attributwert Wohnort des Objektes abhängt. Zwei Menschen
verhalten sich demnach anders, wenn der eine in Bonn und der andere in
Berlin wohnt und sie aufgefordert werden nach Hause zu gehen. Anstatt von
"Aufforderung" spricht man hier auch vom "Aufrufen einer
Methode eines Objektes". In unserem Fall wird die Methode
geh_nach_hause beider Menschen aufgerufen.
Prinzipien der Objektorientierten Programmierung
Kapselung
Was ich noch erwähnen sollte - und einige werden mir jetzt sicherlich
widersprechen - ist, dass die Werte der Attribute nur dem Objekt selbst
bekannt sind, solange es diese nicht preisgibt. Preisgeben kann es diese
nur, wenn dafür eine spezielle Methode definiert wird. Das leuchtet ein
wenn man sich einmal in Gedanken vorstellt wie die Polizei versucht die
Identität einer Wasserleiche herauszufinden. Die Polizei kann ja auch nicht
einfach die Werte der Attribute "Name" oder "Wohnort"
abfragen. Selbst wenn die "Leiche" noch leben würde, hätte sie
(die Leiche) die volle Kontrolle darüber, ob sie nun der Polizei verrät wo
sie wohnt und wie sie heisst oder ob sie dies nicht tut oder gar lügt. Da
eine Leiche jedoch in aller Regel tot ist (so die Definition ;-)), fällt
diese Möglichkeit eh weg.
Also nochmal zum mitschreiben: Die einzigste Möglichkeit mit Objekten zu
interagieren besteht darin, dessen Methoden aufzurufen, also dem Objekt
mitzuteilen was wir von ihm wollen. Wir kommunizieren im Prinzip mit dem
Objekt, daher sprechen wir auch häufig vom "Senden einer
Nachricht" an ein Objekt anstelle von "Methodenaufruf".
Dieses Prinzip, also dass die Attributwerte nur dem Objekt bekannt sind,
führt auch sofort zu einem der Grundpfeiler der Objektorientierten
Programmierung (OOP), nämlich der Kapselung. Der Zustand (also die
Attributwerte) eines Objektes kann ausschliesslich durch Methoden verändert
bzw. abgefragt werden. Leider findet dieses ungemein wichtige Prinzip in
vielen Programmiersprachen nicht die Bedeutung die es verdient. Anders in
Ruby, Smalltalk, Eiffel und einigen anderen Sprachen.
Ach wie wär's schön wenn man den Zustand von Objekten direkt abfragen
könnte. Das würde nämlich bedeuten, das es keine Lügen und keine
Missverständnisse mehr gäbe. Aber leider gäbs dann auch keine Kommunikation
mehr!
Vererbung
Ein weiterer wichtiger Aspekt der OOP neben der Kapselung ist die
Vererbung. Vererbung ist sehr leicht zu erklären und zu verstehen
wie ich meine. Klassen können von anderen Klassen erben. So könnten wir zum
Beispiel die Klasse Katze von der Klasse Lebewesen erben lassen um
auszudrücken, dass jede Katze ein Lebewesen ist und somit diesselben
Eigenschaften wie ein Lebenwesen besitzt. Das spart uns ne Menge Zeit, da
wir ja auch noch Hunde und Fische definieren wollen, die allesamt von der
Klasse Lebenwesen erben. Das ist genauso wie in der Biologie! Natürlich
können wir dann noch zusätzliche Eigenschaften für Katzen definieren und
sogar die von der Klasse Lebewesen geerbten Eigenschaften durch eigene
überschreiben (letzteres ist das dritte wichtige Prinzip der OOP, die
Polymorphie, auf die ich aber hier nicht weiter eingehen werde).
Polymorphie
Nein, das erklär ich jetzt nicht! Polymorphie (griechisch für
"vieldeutig") ist selbst ein sehr vieldeutiger Begriff. So gibt
es u.a. statische und dynamische Polymorphie. Aber das interessiert uns
hier nicht weiter.
Der Praktische Teil
Wir wissen nun also ungefähr was Klassen und Objekte sind und kennen auch
deren Eigenschaften wie Attribute und Methoden. Nun wollen wir aber, um es
besser verstehen zu können, das ganze anhand einem Beispiel vertiefen, und
zwar indem wir die Tasten schwingen und gleich richtig in die
Objektorientierte Programmierung (OOP) einsteigen. Und was wäre dafür
besser geeignet als Ruby? Nein wirklich, schreibt diese Beispiele von mir
aus in einer anderen Sprache und gebt es einem Neuling und lasst ihn
entscheiden...
Und Los gehts!
Wir bauen uns ein Lebewesen... Ein Lebewesen soll ein Attribut haben, und
zwar seine DNS.
class Lebewesen
def init(dns)
@dns = dns
end
end
Wir haben also oben die Klasse Lebewesen definiert, mit einer Methode
init. Diese Methode dient dazu, den Zustand des Objektes initial,
also quasi gleich nach der "Geburt" zu definieren, da andernfalls
das Objekt mit keinem oder nur mit einem undefinierten Zustand leben muss,
was wir schliesslich nicht wollen.
In unserem Fall bezeichnet @dns das Attribut "DNS".
Attribute, also die Variablen in denen der Attributwert gespeichert wird,
beginnen in Ruby immer mit einem at-Zeichen (@). Damit kann man sie nicht
mit lokalen Variablen verwechseln.
Wird nun die init Methode aufgerufen, so weisen wir der
Zustandsvariablen @dns den Wert zu, den wir der Methode beim
Aufruf als Parameter übergeben haben, hier ist das der Wert des Parameters
dns.
Um nun ein neues Lebewesen Objekt zu erzeugen dessen Zustand initial leer
ist, rufen wir die allocate Methode der Klasse Lebewesen auf. Das
geht in Ruby, da dort Klassen in Wirklichkeit auch Objekte sind,
Klassen-Objekte eben ;-). Und wen ich jetzt total verwirrt habe, der denke
sich einfach eine spezielle Operation, mit der man von einer Klasse ein
"frisches", d.h. leeres Objekt erzeugen kann (z.B. den
new Operator aus C++ oder Konstruktoren).
# wir erzeugen ein neues "frisches" Lebewesen
neuesLebewesen = Lebewesen.allocate
Nun haben wir ein frisches Lebewesen erschaffen und haben es in der
Variablen neuesLebewesen vorerst zwischengespeichert. Jetzt wollen
wir ihm noch seine DNS zuweisen. Dazu müssen wir seinen Zustand, genauer
seine Zustandsvariable @dns, ändern. Da wir aber
selbstverständlich nicht direkt auf den Zustand eines Objektes zugreifen
können (Gott sei Dank! Wer will schon gerne das sich seine DNS so einfach
ändern lässt), müssen wir das Objekt höftlich fragen "es solle doch
seine DNS auf den von uns genannten Wert setzen". Klar, dafür ist ja
auch die init Methode gedacht:
# und initialisieren seinen internen Zustand
neuesLebewesen.init("AA-B-AC-DA")
Wenn du das allocate oben nicht verstanden hast, egal! Es ist eine
vordefinierte Methode, die jedes Klassenobjekt automatisch
"erbt". Lebewesen oben im Quellcode ist nämlich das
Objekt das die Klasse "Lebewesen" beschreibt. Und auf Objekte
können wir bekanntermassen Methoden anwenden, nicht auf Klassen, da Klassen
abstrakte Dinge sind die so nicht existieren (ausser in unseren Gedanken
oder in Form von Objekten die wiederrum Klassen beschreiben). Auf keinen
Fall jetzt durcheinander kommen, das ist alles nicht soo wichtig!
Schön wäre es doch nun, ein Objekt bei dessen Erschaffung gleich mit seinem
Zustand initialisieren zu können. Das ist sogar noch einfacher in Ruby und
geht so:
class Lebewesen
def initialize(dns)
@dns = dns
end
end
# neues Lebewesen erzeugen und initialisieren
neuesLebewesen = Lebewesen.new("AA-B-AC-DA")
Wieder ist new eine bereits vordefinierte Methode in Ruby, die im
Prinzip allocate aufruft um ein neues Objekt zu erzeugen. Danach
ruft sie die initialize Methode des neu erzeugten Objektes auf und
gibt ihr all die Parameter mit die sie selbst bekommen hat. Es ist also
ungefähr äquivalent zu:
l = Lebewesen.allocate
l.initialize("AA-B-AC-DA")
Nur funktioniert das nicht ganz, da initialize standardmässig von
Ruby als private deklariert wird, d.h. sie kann nicht von ausserhalb des
Objektes aufgerufen werden, ähnlich wie die Attribute nach aussen hin
abgeschottet sind. Aber das nur am Rande.
Warum gerade initialize? Nun, das ist einfach Konvention.
new ruft eben diese Methode auf! Aber wie wir gesehen haben, sind
wir nicht gezwungen diese Methode zu definieren.
Klonen Leichtgemacht
Tja, wer hätte es geahnt, aber wir in Ruby können schon über 10 Jahre lang
Lebewesen klonen, und zwar ohne das dabei Frankensteins entstehen ;-) Oder
etwa doch?
frank = Lebewesen.new('AA-BB-CC')
frankenstein = frank.clone
p frank.id == frankenstein.id # => false
p frank == frankenstein # => true
Das p in den letzen zwei Zeilen ist eine Kurzform für
print. Es stellt sich heraus, das wir zwei Objekte erzeugt haben
(sie haben unterschiedliche ids), die jedoch ansonsten identisch
sind.
Dieser Einschub war aber eher spasseshalber gedacht.
Es regnet Hunde und Katzen
Soweit so gut. Jetzt wollen wir uns paar kleine Kätzchen definieren. Eine
Katze hat neben der DNS noch einen Namen.
class Katze < Lebewesen
def initialize(dns, name)
super(dns)
@name = name
end
def miau
print "miau, ich bin " + @name
end
end
punkti = Katze.new("AA-BB", "Punkti")
flocki = Katze.new("AC-DC", "Flocki")
punkti.miau # => "miau, ich bin Punkti"
flocki.miau # => "miau, ich bin Flocki"
Neu ist hier die Verwendung von super. Dies ruft die Methode der
Superklasse, in diesem Fall also initialize der Klasse Lebewesen
auf. Klar sollte sein, dass die Klasse Katze von der Klasse Lebewesen erbt.
Zusätzlich wollen wir jetzt aber den Namen der Katze abfragen können. Wir
definieren also eine Methode name, die ganz einfach das Attribut
@name zurückliefert.
class Katze
def name
@name
end
end
Zu beachten gilt, dass in Ruby der letzte Wert einer Methode als
Rückgabewert zurückgegeben wird, @name also im Beispiel oben.
Jetzt brauchen wir noch Hunde. Hunde haben der Einfachheit halber keinen
Namen (och, bin ich fies ;-). Dafür können sie bellen und man kann sie
Katzen jagen lassen.
class Hund < Lebewesen
def wau
print "wau wau"
end
def jage(katze)
print "ich jage " + katze.name
end
end
Wir müssen auch gar keine initialize Method definieren da diese
aus der Klasse Lebewesen geerbt wird.
Dann lasst uns mal spielen ;-)
bello = Hund.new("AA-ZZ") # auch Hunde haben eine DNS
punkti.miau # => "miau, ich bin Punkti"
bello.wau # => "wau wau"
bello.jage(punkti) # => "ich jage Punkti"
Und was passiert wenn wir eine Katze bellen lassen? Einfach ausprobieren:
punkti.wau
# => NoMethodError: undefined method `wau' for #<Katze:0x81e9748>
Da Ruby weiss dass punkti eine Katze ist, und Katzen keine Methode
wau definieren, teilt es uns mittels einer Ausnahme (Exception)
mit, dass diese Methode nicht existiert. Genauso gut können wir einen Hund
einen anderen Hund jagen lassen, oder ein beliebiges anderes Objekt. Da ein
Hund jedoch keine Methode name hat, wird Ruby uns wieder eine
Ausnahme melden. Das heisst also, dass wir die Methode jage mit
einem Objekt als Parameter aufrufen müssen, für das die Methode
name definiert ist. Diese Vorgehensweise nennt man auch liebevoll
Duck Typing. Dave
Thomas hat diesen Begriff geprägt, wenn ich mich recht erinnere.
"Everything that walks like a duck, quacks like a duck, is a duck"
Gemeint damit ist, dass wir nicht überprüfen ob ein Objekt der Klasse
Ente angehört wie dies in Java, C++ und vielen anderen Sprachen
der Fall ist (das nennt man übrigens Tag-typing), sondern einfach nur
testen ob dieses Objekt die Methoden watschel und quack
definiert. Wenn ja, dann handelt es sich für uns um eine Ente, ansonsten
nicht.