OOP vertieft - Theorie Lektion 7
In Lektion 16 hast du einen ersten Eindruck von objektorientierter Programmierung (abgekürzt OOP) bekommen. Die prozedurale Programmierung im Spiel Tic-Tac-Toe führte dazu, dass wir eine Methode nach der anderen implementierten, die aber alle irgendwie zusammengehören. In Lektion 17, der Abschlußlektion des Spiels, wollen wir gemeinsam den bisherigen Rubycode für Tic-Tac-Toe in einen objektorientierten Code umschreiben.
Dazwischen füge ich diese Theorielektion ein, damit wir etwas Zeit haben uns mit dem Neuen vertraut zu machen.
Du programmierst ein Wohnhaus auf objektorientierte Art und Weise. Formulieren wir zunächst etwas vereinfacht, was ein Wohnhaus überhaupt ist.
Diesen beschreibenden Text eines Wohnhauses gilt es nun in ein objektorientiertes Programm umzuwandeln. Du musst irgendwie herausfinden, welche Objekte hier im Spiel sind, damit du die entsprechenden Klassen implementieren kannst und welche Methoden diese Objekte haben sollen. Dafür gibt es eine klassische Vorgehensweise:
- Markiere alle Substantive im Text, das sind die Klassen!
Ein Wohnhaus ist ein Gebäude mit einer Haustür und mehreren Fenstern, einem Dach mit Schornstein. Innen hat das Wohnhaus mehrere Zimmer und Möbel, einen Kachelofen.Die Tür und die Fenster kann man öffnen und schließen, der Schornstein raucht, solange der Kachelofen angeschürt ist.
- Markiere alle Verben im Text, das sind die Methoden!
Ein Wohnhaus ist ein Gebäude mit einer Haustür und mehreren Fenstern, einem Dach mit Schornstein. Innen hat das Wohnhaus mehrere Zimmer und Möbel, einen Kachelofen.
Die Tür und die Fenster kann man öffnen und schließen, der Schornstein raucht, solange der Kachelofen angeschürt ist.
- Das Programm funktioniert dann so als würden die Substantive die Verben anderer Substantive aufrufen.
Wir werden nicht alle Klassen implementieren, sondern nur folgende:
- Tür
- Fenster
- Kachelofen
- Wohnhaus
Fangen wir mit der Klasse Tür an.
class Tuer
def initialize(farbe, material)
@farbe = farbe
@material = material
@zustand = :geschlossen
end
end
Im Konstruktor initialize werden drei Attribute der Klasse (das sind die Variablen mit dem @-Zeichen) angelegt: farbe, material und zustand. Wozu das Attribut farbe dient ist ziemlich klar. Im Attribut material wird gespeichert, aus welchem Material die Tür besteht (Holz, Metall, ...) und im Attribut zustand, ob sie offen oder geschlossen ist.
Der Konstruktor bekommt von außen die gewünschte Farbe und das Material übergeben. Der Zustand
wird nicht übergeben, sondern immer auf :geschlossen gesetzt. Statt :geschlossen könnten wir
auch "geschlossen" verwenden. Wir müssen uns nur für eine Schreibweise entscheiden und diese
dann später beim Ändern des Zustandes beibehalten.
tuer1 = Tuer.new("schwarz", :holz)
tuer2 = Tuer.new("blau", :metall)
puts tuer1
puts tuer2
Lassen wir das Programm laufen, liefert es folgendes:
C:\entwicklung>ruby theorie_07.rb
#<Tuer:0x2c7f9b4>
#<Tuer:0x2c7f98c>
Naja, sieht ja nicht gerade informativ aus. Schöner wäre doch, wenn der puts Befehl auch die Inhalte der Attribute ausgeben würde. Das geht so einfach nicht, weil der puts Befehl nichts von den Attributen einer Tür weiß. Es gibt aber eine Möglichkeit: der puts Befehl versucht immer eine Methode to_s (englisch abgekürzt für to string) am Objekt aufzurufen. Wird sie gefunden, dann wird der Rückgabewert dieser Methode ausgegeben, andernfalls macht puts was es will. Eine schöne to_s Methode wäre vielleicht die folgende:
class Tuer
def initialize(farbe, material)
@farbe = farbe
@material = material
@zustand = :geschlossen
end
def to_s
"Tuer[zustand=#{@zustand}, farbe=#{@farbe}, material=#{@material}]"
end
end
Damit sieht die Ausgabe gleich viel besser aus:
C:\entwicklung>ruby theorie_07.rb
Tuer[zustand=geschlossen, farbe=schwarz, material=holz]
Tuer[zustand=geschlossen, farbe=blau, material=metall]
Ganz zufrieden sind wir aber immer noch nicht. Oft werden wir auf ein einzelnes Attribut zugreifen wollen. Innerhalb der Klasse können wir das ja immer über den Variablennamen mit dem @-Zeichen machen. Aber von außen geht das nicht (Stichwort Kapselung, du erinnerst dich?).
tuer1 = Tuer.new("schwarz", :holz)
puts tuer1.farbe
Versuchen wir es, erhalten wir einen NoMethodError Fehler.
C:\entwicklung>ruby theorie_07.rb
theorie_07.rb: undefined method `farbe' for #<Tuer:0x2c7f838> (NoMethodError)
Es braucht also noch eine Methode, die uns den Inhalt des Attributes nach außen liefert:
class Tuer
def initialize(farbe, material)
@farbe = farbe
@material = material
@zustand = :geschlossen
end
def farbe
@farbe
end
def to_s
"Tuer[zustand=#{@zustand}, farbe=#{@farbe}, material=#{@material}]"
end
end
Dann funktioniert der Aufruf von puts tuer1.farbe, aber irgendwie unschön ist das doch, für jedes Attribut so eine Methode anbieten zu müssen. Ruby hält dafür eine Abkürzung bereit:
class Tuer
attr_reader :farbe, :material, :zustand
attr_writer :zustand
def initialize(farbe, material)
@farbe = farbe
@material = material
@zustand = :geschlossen
end
def to_s
"Tuer[zustand=#{@zustand}, farbe=#{@farbe}, material=#{@material}]"
end
end
Mit attr_reader legen wir fest, welche Attribute von außen gelesen werden dürfen. Mit attr_writer legen wir fest, welche Attribute von außen beschrieben werden dürfen. Oben haben wir festgelegt, dass nur der Zustand gelesen und beschrieben, also geändert werden darf. Die Attribute farbe und material sind unveränderbar. Sie behalten den Wert, den sie beim Konstruktoraufruf (Methode initialize) erhalten haben. Das sind readonly (engl. nur lesen) Attribute.
Wir geben uns mit der Klasse zunächst einmal zufrieden und machen mit der Klasse Fenster weiter. Die ist ziemlich ähnlich aufgebaut:
class Fenster
attr_reader :farbe, :material, :zustand
attr_writer :zustand
def initialize(farbe, material)
@farbe = farbe
@material = material
@zustand = :geschlossen
end
def to_s
"Fenster[zustand=#{@zustand}, farbe=#{@farbe}, material=#{@material}]"
end
end
Womit wir auch schon bei der Klasse Kachelofen angelangt wären, die sich auch nur wenig von den bisherigen unterscheidet:
class Kachelofen
attr_reader :farbe, :zustand
attr_writer :zustand
def initialize(farbe = "gelb")
@farbe = farbe
@zustand = :aus
end
def to_s
"Kachelofen[zustand=#{@zustand}, farbe=#{@farbe}]"
end
end
Beim Kachelofen ist aber etwas neu. Der Konstruktor bekommt genau einen Parameter übergeben: farbe. Der Parameterwert wird aber dabei auf den Wert “gelb” gesetzt. Das ist ein sogenannter Default, also ein Standardwert. Falls beim Anlegen des Objektes keine Farbe für den Kachelofen festgelegt wird, dann ist er immer gelb.
k1 = Kachelofen.new
puts k1
k2 = Kachelofen.new("braun")
puts k2
C:\entwicklung>ruby theorie_07.rb
Kachelofen[zustand=aus, farbe=gelb]
Kachelofen[zustand=aus, farbe=braun]
Kommen wir zur letzten Klasse Wohnhaus. Jetzt wird es erst wirklich interessant. Ein Objekt der Klasse Wohnhaus verwaltet all die anderen Objekte Tuer, Fenster, Kachelofen.
class Wohnhaus
attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
def initialize(farbe, farbe_dach)
@farbe = farbe
@farbe_dach = farbe_dach
@kachelofen = Kachelofen.new
@tuer = Tuer.new("schwarz", :holz)
f = Fenster.new("braun", :holz)
@fenster = [f, f, f]
end
def to_s
res = "Wohnhaus[\n"
res << " farbe = " + @farbe + "\n"
res << " dach = " + @farbe_dach + "\n"
res << " kachelofen = " + @kachelofen.to_s + "\n"
res << " tuer = " + @tuer.to_s + "\n"
res << " schornstein = " + @schornstein.to_s + "\n"
res << " fenster = " + "\n"
for f in @fenster
res << " " + f.to_s + "\n"
end
res << "\n"
res << "]"
res
end
end
Ein Objekt der Klasse Wohnhaus hat zu Beginn (d.h. so wie es vom Konstruktor initialize ausgeliefert wird) eine bestimmte @farbe, das Dach hat eine vielleicht andere @farbe_dach. Es besitzt einen @kachelofen, genau eine @tuer und drei @fenster.
Die to_s Methode ist etwas umfangreicher, weil wir hier viele Details des Hauses ausgeben möchten. Wir nutzen dabei die to_s Methoden der jeweiligen Objekte.
Auch hier ist wieder eine Kleinigkeit neu. Den Zugriff auf die Attribute der Klasse Wohnhaus legen wir diesmal in der Zeile
...
attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
...
fest. attr_accessor bedeutet, dass wir alle so festgelegten Attribute sowohl lesen als auch beschreiben können. Alternativ hätten wir auch ausführlich es so festlegen können:
...
attr_reader :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
attr_writer :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
...
Das Haus verändern
In der Beschreibung eines Wohnhauses haben wir gesagt, dass man die Tür und die Fenster öffnen und schließen kann und der Schornstein raucht, solange der Kachelofen angeschürt ist.
Fenster und Türen öffnen und schließen
Fenster und Türen haben einen Zustand, den wir im Konstruktor immer mit :geschlossen
festgelegt haben. Wollen wir eine Tür oder eine Fenster öffnen, muss dieser Zustand
den Wert :offen erhalten. Wir brauchen dafür eine Methode, die wir von außen aufrufen,
und die dann den Zustand der Tür oder der Fenster ändert.
Das Öffnen und Schließen der Tür übernehmen die Methoden tuer_auf und tuer_zu.
class Wohnhaus
attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
...
def tuer_auf
@tuer.zustand = :offen
end
def tuer_zu
@tuer.zustand = :geschlossen
end
...
end
Bei den Fenster ist etwas mehr zu tun, weil es nicht nur ein Fenster gibt.
class Wohnhaus
attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
...
def fenster_auf
for f in @fenster
f.zustand = :offen
end
end
def fenster_zu
for f in @fenster
f.zustand = :geschlossen
end
end
...
end
Den Kachelofen anschüren
Auch der Kachelofen hat eine Zustand, der anzeigt, ob er aus oder angeschürt ist. Das Ändern dieses Zustandes übernehmen die beiden Methoden ofen_an und ofen_aus.
class Wohnhaus
attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
...
def ofen_an
@kachelofen.zustand = :an
@schornstein = :raucht
end
def ofen_aus
@kachelofen.zustand = :aus
@schornstein = :aus
end
...
end
Die Methoden ofen_an bzw. ofen_aus überwachen also sowohl den Zustand des Kachelofens als auch des Schornsteins. So können wir sicher stellen, dass beide Zustände immer gleichzeitig in übereinstimmender Weise geändert werden. Es kann so nicht passieren, dass der Ofen aus geht, aber der Schornstein noch weiter raucht. Oder doch?
Hier nochmal alle Klassen zusammen. Ganz unten ein paar Zeilen Code, die ein Wohnhausobjekt anlegen und es verändern.
class Tuer
attr_reader :farbe, :material, :zustand
attr_writer :zustand
def initialize(farbe, material)
@farbe = farbe
@material = material
@zustand = :geschlossen
end
def to_s
"Tuer[zustand=#{@zustand}, farbe=#{@farbe}, material=#{@material}]"
end
end
class Fenster
attr_reader :farbe, :material, :zustand
attr_writer :zustand
def initialize(farbe, material)
@farbe = farbe
@material = material
@zustand = :geschlossen
end
def to_s
"Fenster[zustand=#{@zustand}, farbe=#{@farbe}, material=#{@material}]"
end
end
class Kachelofen
attr_reader :farbe, :zustand
attr_writer :zustand
def initialize(farbe = "gelb")
@farbe = farbe
@zustand = :aus
end
def to_s
"Kachelofen[zustand=#{@zustand}, farbe=#{@farbe}]"
end
end
class Wohnhaus
attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
def initialize(farbe, farbe_dach)
@farbe = farbe
@farbe_dach = farbe_dach
@kachelofen = Kachelofen.new
@schornstein = :aus
@tuer = Tuer.new("schwarz", :holz)
f = Fenster.new("braun", :holz)
@fenster = [f, f, f]
end
def tuer_auf
@tuer.zustand = :offen
end
def tuer_zu
@tuer.zustand = :geschlossen
end
def fenster_auf
for f in @fenster
f.zustand = :offen
end
end
def fenster_zu
for f in @fenster
f.zustand = :geschlossen
end
end
def ofen_an
@kachelofen.zustand = :an
@schornstein = :raucht
end
def ofen_aus
@kachelofen.zustand = :aus
@schornstein = :aus
end
def to_s
res = "Wohnhaus[\n"
res << " farbe = " + @farbe + "\n"
res << " dach = " + @farbe_dach + "\n"
res << " kachelofen = " + @kachelofen.to_s + "\n"
res << " tuer = " + @tuer.to_s + "\n"
res << " schornstein = " + @schornstein.to_s + "\n"
res << " fenster = " + "\n"
for f in @fenster
res << " " + f.to_s + "\n"
end
res << "\n"
res << "]"
res
end
end
w = Wohnhaus.new("gelb", "rot")
w.tuer = Tuer.new("blau", :holz)
w.kachelofen = Kachelofen.new("braun")
# Ein weiteres Fenster einbauen
w.fenster << Fenster.new("weiss", :kunststoff)
puts w
w.tuer_auf
w.fenster_auf
puts w
w.ofen_an
w.tuer_zu
w.fenster_zu
puts w
Verletzliche Kapselung
Zurück zu obiger Frage: Kann es passieren, dass der Ofen aus geht, der Schornstein aber weiter raucht?
w = Wohnhaus.new("gelb", "rot")
w.ofen_an
w.kachelofen.zustand = :aus
puts w
Liefert folgende Ausgabe
C:\entwicklung>ruby theorie_07.rb
Wohnhaus[
farbe = gelb
dach = rot
kachelofen = Kachelofen[zustand=aus, farbe=gelb]
tuer = Tuer[zustand=geschlossen, farbe=schwarz, material=holz]
schornstein = raucht
fenster =
Fenster[zustand=geschlossen, farbe=braun, material=holz]
Fenster[zustand=geschlossen, farbe=braun, material=holz]
Fenster[zustand=geschlossen, farbe=braun, material=holz]
]
Der Ofen ist also aus, der Schornstein raucht aber noch. Wie konnte das passieren? Die Klasse Wohnhaus ist nicht sicher genug gekapselt. Sie lässt es zu, dass man direkt mit w.kachelofen auf den Kachelofen zugreifen kann. Dann kann man dort den Zustand ändern und das Wohnhaus bekommt von dieser Änderung nichts mit.
Wie können wir das Problem beheben und die Kapselung hier wieder sicher machen? Mindestens zwei Möglichkeiten bieten sich an.
- Wir lassen den direkten Zugriff auf das Attribut kachelofen nicht mehr zu:
Dann müssen wir dem Erschaffer des Hauses aber einen weiteren Parameter im Konstruktor anbieten, der es erlaubt, die Farbe des Ofens festzulegen.class Wohnhaus attr_accessor :farbe, :farbe_dach, :tuer, :fenster ... end - Wir belassen den Zugriff auf das Attribut kachelofen wie bisher, überschreiben
jedoch die Lesemethode mit einer ausführlichen Methode. In dieser Methode
übergeben wir nicht das Attribute @kachelofen nach außen zurück, sondern stets
eine Kopie davon.
Mit der Kopie kann der Aufrufen dann machen, was er will, das Attribut @kachelofen innerhalb der Klasse wird sich dadurch nicht ändern.class Wohnhaus attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster ... # Attribut kachelofen auslesen, dabei gegen Änderung schützen, indem # nur eine Kopie nach außen gegeben wird. def kachelofen Kachelofen.new(@kachelofen.farbe) end ... end
Üben mit Peter und Livia
Livia: Es ist aber trotzdem immer noch möglich, den Kachelofen aus zu machen und den Schornstein weiter rauchen zu lassen. Wie muss die Klasse Wohnhaus weiter angepasst werden, damit auch dieser Fehler behoben ist?
Peter: Füge für Schornstein noch eine eigene Klasse mit den notwendigen Attributen hinzu.
