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.

Lektion 16 - Objekte - na endlich!

Erstellt von Frithjof Sat, 17 Nov 2007 00:13:00 GMT

In den vergangenen Lektionen drehte sich alles um das Spiel Tic-Tac-Toe. Wir haben wie verrückt Ruby Code getippt und – so hoffe ich – fleißig Tic-Tac-Toe gespielt, zu zweit am und gegen den Computer. Der ist im Verlauf unserer Tätigkeiten ziemlich stark geworden. Irgendwie macht es dann mit der Zeit keinen Spaß mehr, weil man ja sowieso schon weiß, dass es wieder mal auf ein Unentschieden hinaus läuft.

Hier nochmal alle Lektionen zu dem Spiel im Überblick.

Und jetzt? Wir schauen uns in dieser Lektion etwas an, wovon sicher viele meinen, ich hätte es dir schon eher sagen sollen.

Machen wir uns ein wenig darüber Gedanken, wie wir bisher programmiert haben. Schau dir mal die letzte Datei tictactoe.rb an. Sie besteht aus zahlreichen Methoden (es müssten 16 Stück sein), die immer mit def gefolgt von einem Namen und einer Liste von Übergabeparametern anfangen und mit end abgeschlossen werden. Jede Methode ist eine Art kleines Progrämmchen, das einen kleinen Teil der Arbeit des gesamten Programms übernimmt. Sie wird von irgendwo aufgerufen, erledigt ihre Teilaufgabe und gibt möglicherweise ein Ergebnis an den Aufrufer zurück. Statt Methode sagen viele auch Prozedur dazu. Prozedur leitet sich vom Lateinischen Wort für vorwärts gehen ab.

Du stellst dir eine Methode (oder Prozedur) somit als eine Menge von aufeinander folgenden Programmanweisungen vor, die vom Rubyinterpreter der Reihe nach abgearbeitet werden. Der Rubyinterpreter geht gewissermaßen vorwärts durch die Codezeilen und macht, was sie ihm befehlen.

Wirf nochmal einen Blick in die Datei tictactoe.rb, insbesondere die ersten Zeilen der Methoden:


  def spielfeld(out, zuege)
  def print_zeile(out, zeile, zuege)
  def print_feld(spalte, zeile, zuege)
  def zug_hinzu(wer, spalte, zeile, zuege)
  def nummer_in_spalte_zeile(num)
  def nummer_aus_spalte_zeile(spalte, zeile)
  def the_winner_is(zuege)
  def ist_beendet?(zuege)
  def play_2_spieler(out, ein, zuege)
  def zufalls_zug(zuege, spieler, wer)
  def naiver_zug(zuege, spieler, wer)
  def freie_felder(zuege)
  def reihen_status(zuege, reihe)
  def intelligenter_zug(zuege, spieler, wer)
  def computer_zug(zuege, spieler, wer)
  def play_gegen_computer(out, ein, zuege)

Fällt dir etwas auf? Stichwort: Wiederholungen? Von den 16 Methoden, haben 14 die Variable zuege in ihrer Liste der Übergabeparameter! Nur auf zwei Methoden nummer_in_spalte_zeile und nummer_aus_spalte_zeile trifft das nicht zu.

Die Menge der Methoden lässt sich somit in zwei Gruppen einteilen: Methoden, die von der Liste der im Spiel gemachten zuege abhängen (die 14 Stück) und Methoden, die nicht von zuege abhängen (die 2 übrigen).

Die 14 abhängigen Methoden benötigen Wissen über die gemachten Spielzüge, daher muss man ihnen diese beim Aufruf mitgeben. Würden wir das Spiel noch weiter programmieren, beispielsweise mit einer grafischen Oberfläche versehen, dann hätten wir noch viel mehr Methoden zu implementieren – ziemlich alle wohl mit der Variablen zuege in der Liste der Übergabeparameter. Irgendwann fängt so eine Art Wiederholung an zu nerven. Dieses Phänomen ist sehr charakteristisch für prozedurale Programmierung, also Software, die nur mit isolierten Prozeduren arbeitet.

Irgendjemand hat sich irgendwann einmal überlegt, das zu ändern und die objektorientierte Programmierung erfunden. Das ist schon ziemlich lange her. Die Frage ist somit durchaus berechtigt, warum ich dir dies nicht von Anfang an erzählt habe und wir uns bis jetzt ausschließlich mit Prozeduren (oder Methoden) abgegeben haben?

Meine Antwort darauf: Man kapiert objektorientierte Programmierung einfach nicht von Anfang an! Das ging mir bei meinen ersten Programmierübungen so und das wird bei dir nicht anders sein. Über objektorientierte Programmierung zu sprechen lohnt sich wirklich erst, wenn man mit prozeduraler Programmierung sich einen Teil einer Programmiersprache erobert hat. Potenzrechnen versteht man erst, wenn man die Multiplikation verstanden hat, und diese wiederum erst, wenn Addition kein Fremdwort mehr ist. Also alles schön der Reihe nach.

Was ist nun aber objektorientierte Programmierung genau?

Schauen wir wieder auf unser Spiel Tic-Tac-Toe. Die Liste der zuege sind die wichtigsten Daten des Spiels. Die 14 von ihr abhängigen Methoden arbeiten immer mit oder auf diesen Daten. Die objektorientierte Programmierung bietet die Möglichkeit, solche Daten und ihre abhängigen Methoden dicht zusammenzuhalten (man sagt auch kapseln). In der prozeduralen Programmierung ist die Datei selbst das einzige Mittel, Daten und Methoden zusammenzuhalten. Das erste wichtige Merkmal der objektorientierten Programmierung (das schreibe ich jetzt zum letzten Mal aus, ab sofort verwende ich die gebräuchliche Abkürzung OOP) ist somit Kapselung.

Für das Spiel Tic-Tac-Toe könnte das etwa so aussehen:


class TicTacToe

  def initialize()
    @zuege = []
  end

  def spielfeld(out)
    # ...
  end

  def print_zeile(out, zeile)
    # ...
  end

  def print_feld(spalte, zeile)
    # ...
  end

  # ... restliche Methoden
end

Zwischen dem beginnenden class und dem zugehörigen end erfolgt die Kapselung von Daten (die zuege) und der Methoden, die auf den Daten arbeiten.

Warum aber erfolgt die Kapselung bei OOP mit dem Schlüsselwort class und nicht objekt (oder englisch object)? Wäre es nicht einleuchtender, wenn wir es so schreiben würden:


object TicTacToe

  def initialize()
    @zuege = []
  end

  # ... restliche Methoden
end

Zwischen einer Klasse (englisch class) und einem Objekt (englisch object) gibt es in OOP einen feinen Unterschied: Die Klasse ist der Bauplan der Objekte! Stelle es dir vor wie ein Architekt, der eine ganze Wohnsiedlung mit einem einzigen Haustyp baut. Er hat nur einen Bauplan eines Hauses. Mit diesem einen Bauplan kann er aber soviele Häuser bauen wie er möchte. Alle Häuser sehen gleich aus. Naja, nicht wirklich gleich. Das eine hat vielleicht rote Fensterläden, das andere grüne. Aber der Haustyp ist bei allen gleich, weil ihnen allen derselbe Bauplan zugrunde liegt.

Das OO Programmieren besteht also darin, Baupläne zu entwerfen. Den späteren Bau der Objekte sieht man nicht wirklich, der erfolgt nach diesen Bauplänen dann vom Rubyinterpreter. Lass dir ruhig etwas mit diesem Gedanken Zeit, wenn du es nicht sofort verstehst. Das ist normal. Mit jedem Stück OO Code wirst du es besser verstehen.

Schauen wir uns noch die anderen Besonderheiten der neuen Schreibweise an.

Konstruktor oder Geburtshelfer

In der Klasse TicTacToe haben wir eine Methode initialize definiert (abgeleitet vom lateinischen initio – anfangs), die wir bisher überhaupt nicht benötigten. Jede Klasse kann diese Methode besitzen, muss aber nicht. Initialize hat eine besondere Bedeutung: sie enthält die Anweisungen, die ganz am Anfang ausgeführt werden, wenn ein Objekt der Klasse das Licht der Welt erblickt. Daher nennt man diese Methode auch den Konstruktor der Klasse, also der Bereich, der dem Objekt alles für einen guten Start ins rauhe OO-Leben verpasst.


# Klasse definieren
class TicTacToe
  def initialize()
    puts "Hurra, ich bin da!" 
  end
end

# Ein Objekt der Klasse anlegen
spiel = TicTacToe.new

Führst du obiges Programm aus, erscheint als Ausgabe der Text Hurra, ich bin da! auf der Kommandozeile.

Die Methode initialize wird nie direkt aufgerufen, sondern indirekt von der Methode new. Innerhalb dieser Methode new wird irgendwann dann die Methode initialize aufgerufen und danach ist das Objekt für den weiteren Gebrauch fertig.

Wo aber kommt die Methode new denn nun schon wieder her, die haben wir doch nirgends definiert!

Methoden von Objekten und Klassen

Die Methode new ist eine weitere besondere Methode, die Ruby jeder Klasse automatisch verpasst. Jeder Klasse, sage ich! Nicht jedem Objekt der Klasse!

Eine Klassenmethode gibt es für alle Objekte einer Klasse immer nur ein einziges mal. Eine Objektmethode hat jedes Objekt dagegen für sich. Du erkennst den Unterschied daran, dass einer Klassenmethode der Name der Klasse vorangestellt wird, während vor einer Objektmethode der Name des Objekts steht—jeweils mit einem Punkt vom Methodennamen abgetrennt.

Attribute

Die Variable zuege ist aus allen Methoden verschwunden. Stattdessen steht sie direkt im Konstruktor mit einem @-Zeichen davor. Das @ markiert diese Variable als ein Attribut. Attribute sind die Variablen einer Klasse, die die Daten enthalten. Man nennt die Attribute auch Eigenschaften der Klasse. Für den außenstehenden sind diese Attribute nicht zugänglich, sie sind in der Klasse versteckt, verkapselt.

Beispiel für OOP

Am Beispiel der Klasse Haus wollen wir uns das bisher gehörte etwas genauer anschauen:


# Definition der Klasse 'Haus'
class Haus
  def initialize(f, fd)
    @farbe      = f
    @farbe_dach = fd
  end
  def to_s
    "Haus[farbe=#{@farbe}, farbe_dach=#{@farbe_dach}]" 
  end
end

# Anlegen von zwei Objekten der Klasse 'Haus'
h1 = Haus.new("gelb",  "rot")
h2 = Haus.new("braun", "rot")

puts h1.to_s
puts h2.to_s

Der Konstruktor wird von außen mit zwei Variablen aufgerufen: die Farbe des Hauses und die Farbe des Daches. Diese Angaben merkt sich jedes Objekt in seinen Attributen @farbe und @farbe_dach. Wird die Methode to_s für ein Objekt aufgerufen, so erhält man eine Zeichenkette, die diese Werte dieser Attribute enthält.


C:\entwicklung> ruby lektion_16.rb
Haus[farbe=gelb, farbe_dach=rot]
Haus[farbe=braun, farbe_dach=rot]

Du solltest nach dieser Lektion heute nicht einfach zur nächsten weitergehen, solange du die beiden Zeilen


puts h1.to_s
puts h2.to_s

noch nicht verstanden hast. Dir muss klar werden, warum in beiden Zeilen zwar dieselbe Methode to_s aufgerufen wird, aber beide Aufrufe nicht dasselbe Ergebnis liefern.

Es liegt daran, dass es sich hier eben nicht mehr um dieselben Methoden handelt! Es sind im Grunde zwei verschiedene Methoden, denen nur derselbe Bauplan aus der Klassendefinition zugrunde liegt.

Die erste Methode to_s gehört zum Objekt h1, die zweite zum Objekt h2. Beide Objekte haben unterschiedliche Attribute, d.h. beide Häuser haben unterschiedliche Farben. Somit liefern beide Methoden unterschiedliche Ergebnisse, obwohl sie denselben Namen haben. Eine Objektmethode arbeitet immer auf den Daten des jeweiligen Objektes.

Belassen wir es für heute dabei. Du solltest erkannt haben, wie sich OOP von dem prozeduralen Programmieren unterscheidet.