OOP vertieft - Theorie Lektion 7

Erstellt von Frithjof Mon, 24 Dec 2007 12:40:00 GMT

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.

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.

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:

  1. 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.

  2. 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.


  3. 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.

Die Klasse Tuer funktioniert schon, wie das folgende Beispiel zeigt. Es werden zwei Objekte der Klasse Tuer angelegt.

  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.

  1. Wir lassen den direkten Zugriff auf das Attribut kachelofen nicht mehr zu:
    
    class Wohnhaus
      attr_accessor :farbe, :farbe_dach, :tuer, :fenster
      ...
    end
    
    Dann müssen wir dem Erschaffer des Hauses aber einen weiteren Parameter im Konstruktor anbieten, der es erlaubt, die Farbe des Ofens festzulegen.
  2. 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.
    
    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
    
    Mit der Kopie kann der Aufrufen dann machen, was er will, das Attribut @kachelofen innerhalb der Klasse wird sich dadurch nicht ändern.

Ü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.

Trackbacks

Verwenden Sie den folgenden Link zur Rückverlinkung von Ihrer eigenen Seite:
http://www.rubykids.de/trackbacks?month=12&year=2007&article_id=oop-vertieft-theorie-lektion-7&day=24

Meine Nachricht

Einen Kommentar hinterlassen

Comments